การใช้ `std::make_heap` ของ libc++ ไม่สอดคล้องกันหรือไม่

แก้ไข: นี่ไม่ใช่การถามว่าจะทำอย่างไร std::make_heap วิธี O(n) แต่ถามว่าการใช้งานเฉพาะนี้เป็น O(n) จริงหรือไม่

วิธีการสร้างฮีปตามตำราเรียนในเวลา O(n) คือการสร้างฮีปอย่างต่อเนื่องจากล่างขึ้นบน แต่การใช้งาน std::make_heap บนเครื่อง Mac ของฉันใน libc++ คือ

template <class _RandomAccessIterator, class _Compare>
inline _LIBCPP_INLINE_VISIBILITY
void
make_heap(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp)
{
#ifdef _LIBCPP_DEBUG
    typedef typename add_lvalue_reference<__debug_less<_Compare> >::type _Comp_ref;
    __debug_less<_Compare> __c(__comp);
    __make_heap<_Comp_ref>(__first, __last, __c);
#else  // _LIBCPP_DEBUG
    typedef typename add_lvalue_reference<_Compare>::type _Comp_ref;
    __make_heap<_Comp_ref>(__first, __last, __comp);
#endif  // _LIBCPP_DEBUG
}

โดยที่ __make_heap ถูกกำหนดให้เป็น

template <class _Compare, class _RandomAccessIterator>
void
__make_heap(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp)
{
    typedef typename iterator_traits<_RandomAccessIterator>::difference_type difference_type;
    difference_type __n = __last - __first;
    if (__n > 1)
    {
        __last = __first;
        ++__last;
        for (difference_type __i = 1; __i < __n;)
            __push_heap_back<_Compare>(__first, ++__last, __comp, ++__i);
    }
}

template <class _Compare, class _RandomAccessIterator>
void
__push_heap_back(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp,
                 typename iterator_traits<_RandomAccessIterator>::difference_type __len)
{
    typedef typename iterator_traits<_RandomAccessIterator>::difference_type difference_type;
    typedef typename iterator_traits<_RandomAccessIterator>::value_type value_type;
    if (__len > 1)
    {
        __len = (__len - 2) / 2;
        _RandomAccessIterator __ptr = __first + __len;
        if (__comp(*__ptr, *--__last))
        {
            value_type __t(_VSTD::move(*__last));
            do
            {
                *__last = _VSTD::move(*__ptr);
                __last = __ptr;
                if (__len == 0)
                    break;
                __len = (__len - 1) / 2;
                __ptr = __first + __len;
            } while (__comp(*__ptr, __t));
            *__last = _VSTD::move(__t);
        }
    }
}

นี่ไม่ใช่แค่การแทรกลงในฮีปซ้ำ ๆ ดังนั้นจึงมีความซับซ้อนของเวลา O(n log n) หรือไม่? ฉันพูดถูกไหมว่านี่เป็นข้อผิดพลาด?


person Siyuan Ren    schedule 29.06.2014    source แหล่งที่มา
comment
ดูคำตอบนี้ กล่าวโดยสรุป อัลกอริธึมไม่ง่ายอย่างที่คิด   -  person WhozCraig    schedule 29.06.2014
comment
@WhozCraig: ฉันรู้อัลกอริทึม O(n) ฉันถามว่าการใช้งานเฉพาะนี้ (ของ libc ++) ล้มเหลวในการใช้สิ่งที่ถูกต้องหรือไม่   -  person Siyuan Ren    schedule 29.06.2014
comment
@WhozCraig: นี่ไม่ใช่คำถามที่ซ้ำกัน ฉันไม่ได้ถามว่าจะทำแบบ O (n) ได้อย่างไร แต่ฉันถามว่าการใช้งานนี้เป็น O(n) จริงหรือไม่   -  person Siyuan Ren    schedule 29.06.2014
comment
@Matthieu M.: ดูการแก้ไขใหม่ของฉันเกี่ยวกับสิ่งนี้ไม่ซ้ำกัน   -  person Siyuan Ren    schedule 29.06.2014
comment
ฉันไม่ได้ทำเครื่องหมายว่าซ้ำกัน ฉันเชื่อมโยงคำตอบนั้นสำหรับการวิเคราะห์อัลกอริทึมเท่านั้น (ซึ่งแม่นยำ) คำถามของคุณเกี่ยวข้องกับการใช้งานอัลกอริธึมนั้นโดยเฉพาะ ซึ่งแม้ว่าจะเกี่ยวข้องกัน แต่ก็ ไม่ ซ้ำ (imho) และไม่ควรถูกทำเครื่องหมายเช่นนั้น (ซึ่งเป็นสาเหตุที่ฉัน ไม่ได้ > ทำเช่นนั้น) โหวตให้เปิดใหม่ครับ   -  person WhozCraig    schedule 29.06.2014
comment
@C.R.: ฉันลังเลที่จะทำเครื่องหมายว่าซ้ำกันเช่นกัน และบอกตามตรงว่าฉันยังคงลังเลอยู่ ความจริงก็คือ อัลกอริธึมที่นี่ดูคล้ายกันมากกับอัลกอริธึมอีกอัน ซึ่งการวิเคราะห์พิสูจน์แล้วว่าเป็น O(n) แน่นอนว่ามีความแตกต่างอยู่บ้าง แต่สำหรับฉันแล้ว ดูเหมือนว่ามันไม่ได้เปลี่ยนเกมเลย   -  person Matthieu M.    schedule 29.06.2014
comment
@MatthieuM. ฉันเห็นด้วยเช่นกัน ฉันอยู่ในรั้วที่จะหลอกลวงสิ่งนี้จริงๆ แต่เลือกที่จะไม่ทำเพราะการดำเนินการนั้น แม้จะคล้ายกัน แต่ก็ไม่เหมือนกับคำถามนี้ คำตอบที่ฉันเชื่อมโยง (จากคำถามนั้นและฉันเชื่อว่าเป็นคำตอบที่ถูกต้องสำหรับสิ่งเดียวกันแม้ว่า OP ดูเหมือนจะเลือกอันอื่น) เป็นการวิเคราะห์อัลกอริทึมที่สวยงาม ฉันเชื่อว่าการวิเคราะห์ก็เหมาะกับการใช้งานนี้เช่นกัน และนั่นอาจเป็นเพราะว่า C.R. ทั้งหมดต้องการตรวจสอบความถูกต้องจริงๆ   -  person WhozCraig    schedule 29.06.2014
comment
นี่ดูเหมือนอัลกอริธึมเวอร์ชันที่ผิดจริงๆ และฉันได้วัดจำนวนการดำเนินการสำหรับขนาดอินพุตต่างๆ และดูเหมือนว่าจะพอดีกับเส้นโค้ง n log n   -  person n. 1.8e9-where's-my-share m.    schedule 29.06.2014
comment
ฉันมี Xcode 5.1.1 ที่ใช้งาน libc++ แบบเดียวกับที่คุณเป็น และการวิเคราะห์จากคำตอบที่เชื่อมโยงเกี่ยวกับอัลกอริทึมนั้นถูกต้อง ฉันเขียนจิ๊กทดสอบง่ายๆ ในการสร้างฮีปจากลำดับที่สร้างแบบสุ่มไปจนถึงความยาว 1024 ถึง 102400 เวอร์ชัน gcc สามารถ ดูได้ที่นี่. ดูได้ที่นี่ release-output จาก Mac ของฉัน เอาต์พุตประกอบด้วย N, NlogN, 3*N และการเปรียบเทียบจริงที่ดำเนินการ ไม่เคยเกิน 3N มาก่อนเลย (หรือถึงจุดนั้นด้วยซ้ำ) หวังว่ามันจะช่วยได้   -  person WhozCraig    schedule 30.06.2014
comment
แก้ไข: หลังจากอ่านความคิดเห็นของ n.m ด้านล่างแล้ว ฉันได้ปลอมแปลงล่วงหน้าและทดสอบกรณีที่แย่ที่สุดมากกว่าความซับซ้อนของตัวพิมพ์โดยเฉลี่ย และแน่นอนว่าเขาถูกต้อง ดูความคิดเห็นของฉันสำหรับข้อมูลเพิ่มเติม   -  person WhozCraig    schedule 30.06.2014


คำตอบ (2)


นี่เป็นการใช้งาน O(n log n) ที่ไม่เป็นไปตามข้อกำหนด

เมื่อเปรียบเทียบกับเวอร์ชัน "กรอง" ของ heapify จาก บทความ Wikipedia เกี่ยวกับ heapsort แสดงให้เห็นว่าโดยพื้นฐานแล้วมันเป็นอัลกอริทึมเดียวกัน . การทดสอบกับลำดับจำนวนเต็มที่เพิ่มขึ้น (กรณีที่เลวร้ายที่สุด) จะให้เวลาทำงานที่พอดีกับเส้นโค้ง n log n และจำนวนการเปรียบเทียบที่จำเป็นนั้นเกินกว่าตัวเลข 3n ที่ได้รับคำสั่งมาตรฐาน แม้แต่สำหรับ n ขนาดเล็กก็ตาม

แม้ว่าโดยเฉลี่ยแล้วอัลกอริทึมจะทำงานได้ดีภายในขีดจำกัด 3n แต่มาตรฐานจะกำหนดประสิทธิภาพในกรณีที่แย่ที่สุด ไม่ใช่ค่าเฉลี่ย

person n. 1.8e9-where's-my-share m.    schedule 30.06.2014
comment
@ซีอาร์ ขอบคุณสำหรับการแจ้งข้อผิดพลาด - person Marshall Clow; 30.06.2014
comment
+1 ขอบคุณสำหรับการทำสิ่งนี้ให้เสร็จ ฉันมักจะลืมคลาสอัลกอริทึมของฉันและการสร้างแบบจำลองข้อมูลที่แย่ที่สุดที่ เข้า ไปสู่อัลกอริธึมโดยเทียบกับไอออนที่คาดหวังของมาตรฐาน ค่าใช้จ่ายเฉลี่ยของการแจกแจงแบบสุ่มสม่ำเสมอดูเหมือนจะเหมาะสม แต่แน่นอนว่ามันเป็นปัญหาในการใช้งานในกรณีที่เลวร้ายที่สุด ขอบคุณอีกครั้ง. - person WhozCraig; 30.06.2014
comment
แก้ไขในการแก้ไข 213615 โดย David Majnemer - person Marshall Clow; 24.07.2014

ฉันเชื่อว่าการสนทนาที่นี่ดูเหมือนจะเข้าสู่เส้นสัมผัสกัน

คำตอบสำหรับคำถามคือ: ไม่; การใช้งาน std::make_heap ของ libc++ เป็นไปตามข้อกำหนดที่คำสั่งมาตรฐาน C++ กำหนดสำหรับรูทีนนั้น

การอ้างอิงจากมาตรฐาน C ++ 11 (มาตรฐาน C ++ 14 ที่กำลังจะมาถึงดูเหมือนจะไม่มีการเปลี่ยนแปลงสำหรับสิ่งนี้)

template<class RandomAccessIterator>
  void make_heap(RandomAccessIterator first, RandomAccessIterator last);
template<class RandomAccessIterator, class Compare>
  void make_heap(RandomAccessIterator first, RandomAccessIterator last, Compare comp);

* Effects: Constructs a heap out of the range [first,last).
* Requires: The type of *first shall satisfy the MoveConstructible requirements (Table 20) and the MoveAssignable requirements (Table 22).
* Complexity: At most 3 * (last - first) comparisons.

ข้อกำหนดด้านความซับซ้อนเพียงอย่างเดียวคือในแง่ของจำนวนการเรียกไปยังตัวดำเนินการเปรียบเทียบ ฉันได้ทำการทดสอบหลายครั้ง และสรุปว่าการใช้งานของ libc++ เป็นไปตามข้อกำหนดนี้ ฉันได้รับการเปรียบเทียบประมาณ 2.3*N สำหรับการดำเนินการ ฉันใช้การทดสอบที่ https://llvm.org/svn/llvm-project/libcxx/trunk/test/algorithms/alg.sorting/alg.heap.operations/make.heap/make_heap_comp.pass.cpp< /ก>. @nm คุณอ้างสิทธิ์เป็นอย่างอื่น; ฉันยินดีเป็นอย่างยิ่งที่ได้เห็นกรณีทดสอบของคุณ การทดสอบของฉันเสร็จสิ้นด้วยอาร์เรย์ ints ขนาดต่างๆ ที่สับเปลี่ยนโดยใช้ std::random_shuffle

คำถามที่ @WhozCraig เชื่อมโยงไปถึงแนะนำว่าอัลกอริทึมนี้สามารถใช้งานได้โดยใช้การเปรียบเทียบน้อยกว่า 3N อย่างมีนัยสำคัญ ฉันได้เพิ่มบทความนั้นในรายการเรื่องรออ่าน (น่าเศร้าและยาว) ของฉันเพื่อศึกษาเพิ่มเติมและการปรับปรุงการใช้งาน make_heap ของ libc++ ที่เป็นไปได้ (ขอบคุณ!)

person Marshall Clow    schedule 30.06.2014
comment
โปรดแสดงการทดสอบของคุณด้วยเพื่อที่ฉันจะได้ตรวจสอบได้ด้วยตัวเอง - person Siyuan Ren; 30.06.2014
comment
ที่ถูกสับโดยใช้ std::random_shuffle นั่นเป็นปัญหาจริงๆ อัลกอริทึม (อาจจะ) อยู่ที่ O(n) โดยเฉลี่ย แต่เป็น O(n log n) กรณีที่แย่ที่สุด ลองใช้ลำดับจากน้อยไปหามาก (นั่นคือกรณีทดสอบของฉัน) - person n. 1.8e9-where's-my-share m.; 30.06.2014
comment
@n.m. คุณพูดถูกมาก รหัสทดสอบเดียวกันที่ฉันแสดงในความคิดเห็นทั่วไปมีผลลัพธ์ที่แตกต่างกันอย่างมีนัยสำคัญในกรณีที่แย่ที่สุดเทียบกับค่าเฉลี่ย gcc build บน ideone (เห็นที่นี่) จะแสดงตัวเลขที่คุณคาดหวัง งานสร้างในเครื่องของฉันโดยใช้ toolchain และ lib ที่เหมือนกันกับ OP (เอาต์พุตที่เห็นที่นี่) ทำให้เกิดตัวเลขที่แตกต่างกันอย่างมาก อย่างง่ายดาย เกิน 3N และเข้าใกล้ O(NlogN) อย่างรวดเร็ว คุณควรโพสต์คำตอบอย่างจริงจังเพื่อให้ฉันสามารถ uptick ได้ ขอบคุณที่ทำให้ฉันซื่อสัตย์ ฉันเห็นด้วยอย่างยิ่งว่ามันเป็นการใช้งานที่ผิดพลาด - person WhozCraig; 30.06.2014
comment
@n.m. ฉันทดสอบ toolchain ที่เหมือนกันเพิ่มเติม โดยเปลี่ยนเฉพาะไลบรารีมาตรฐานที่ใช้จาก libc++ เป็น libstdc++ จาก gnu และตัวเลขดังกล่าวก็น่านับถืออีกครั้งและเทียบเท่ากับจากโพสต์ ideone ขอขอบคุณอีกครั้งที่เก็บสิ่งนี้ไว้เหนือน้ำ - person WhozCraig; 30.06.2014
comment
@n.m. ฉันเห็นมันแล้ว ขอบคุณ. - person Marshall Clow; 30.06.2014
comment
คำตอบนี้ขัดแย้งกับคำตอบที่ยอมรับ คุณเห็นด้วยกับการประเมินที่นั่น หรือยังอ้างว่า libc++ เป็นไปตามข้อจำกัดด้านความซับซ้อนที่กำหนดโดยมาตรฐาน - person user4815162342; 10.07.2014