วิธีการใช้อัลกอริทึมการเรียงลำดับแบบคลาสสิกใน C ++ ที่ทันสมัย?


331

std::sortอัลกอริทึม (และญาติของตนstd::partial_sortและstd::nth_element) จาก ++ ห้องสมุด C มาตรฐานในการใช้งานมากที่สุดควบซับซ้อนและไฮบริดของขั้นตอนวิธีการเรียงลำดับประถมศึกษามากขึ้นเช่นการเลือกเรียงลำดับการจัดเรียงแทรกรวดเร็วเรียงลำดับผสานเรียงลำดับหรือการจัดเรียงกอง

มีคำถามมากมายที่นี่และในเว็บไซต์น้องสาวเช่นhttps://codereview.stackexchange.com/ที่เกี่ยวข้องกับข้อบกพร่องความซับซ้อนและด้านอื่น ๆ ของการใช้งานของอัลกอริทึมการเรียงลำดับแบบคลาสสิกเหล่านี้ การนำไปใช้งานที่นำเสนอส่วนใหญ่ประกอบด้วยลูปดิบใช้การจัดการดัชนีและประเภทที่เป็นรูปธรรมและโดยทั่วไปจะไม่วิเคราะห์ในแง่ของความถูกต้องและมีประสิทธิภาพ

คำถาม : อัลกอริทึมการเรียงลำดับแบบคลาสสิกที่กล่าวถึงข้างต้นสามารถนำไปใช้งานได้อย่างไรโดยใช้ C ++ สมัยใหม่

  • ไม่มีลูปแบบดิบแต่รวมการสร้างแบบอัลกอริทึมของ Standard Library เข้าด้วยกัน<algorithm>
  • อินเทอร์เฟซตัววนซ้ำและการใช้เทมเพลตแทนการจัดการดัชนีและชนิดที่เป็นรูปธรรม
  • สไตล์ C ++ 14รวมถึงไลบรารี่มาตรฐานเต็มรูปแบบรวมถึงตัวลดสัญญาณเสียงวากยสัมพันธ์เช่นautoนามแฝงเทมเพลตตัวเปรียบเทียบโปร่งใสและแลมบ์ดา polymorphic

หมายเหตุ :

  • สำหรับการอ้างอิงเพิ่มเติมเกี่ยวกับการใช้งานของอัลกอริทึมการเรียงลำดับดูWikipedia , รหัส Rosettaหรือhttp://www.sorting-algorithms.com/
  • ตามอนุสัญญาของฌอนพาเรนต์ (สไลด์ 39) ห่วงดิบเป็นวงที่forยาวกว่าองค์ประกอบของสองฟังก์ชันกับตัวดำเนินการ ดังนั้นf(g(x));หรือf(x); g(x);หรือf(x) + g(x);ไม่ได้ลูปดิบและไม่เป็นลูปในselection_sortและinsertion_sortด้านล่าง
  • ฉันทำตามคำศัพท์ของ Scott Meyers เพื่อแสดง C ++ 1y ปัจจุบันเป็น C ++ 14 และเพื่อแสดง C ++ 98 และ C ++ 03 ทั้งสองเป็น C ++ 98 ดังนั้นอย่าจุดประกายฉัน
  • ตามที่แนะนำในความคิดเห็นโดย @Mehrdad ฉันให้การปรับใช้สี่แบบเป็นตัวอย่างสดเมื่อสิ้นสุดคำตอบ: C ++ 14, C ++ 11, C ++ 98 และ Boost และ C ++ 98
  • คำตอบนั้นถูกนำเสนอในรูปของ C ++ 14 เท่านั้น ในกรณีที่เกี่ยวข้องฉันแสดงถึงความแตกต่างทางไวยากรณ์และห้องสมุดที่เวอร์ชันภาษาต่างๆแตกต่างกัน

8
จะเป็นการดีถ้าเพิ่มแท็ก C ++ Faq ให้กับคำถามถึงแม้ว่าจะต้องเสียอย่างน้อยหนึ่งรายการ ฉันขอแนะนำให้ลบเวอร์ชัน (เนื่องจากเป็นคำถาม C ++ ทั่วไปโดยมีการติดตั้งใช้งานในเวอร์ชันส่วนใหญ่ที่มีการปรับบางอย่าง)
Matthieu M.

@TemplateRex เอาล่ะถ้าไม่ใช่คำถามที่พบบ่อยคำถามนี้กว้างเกินไป (เดาสิ - ฉันไม่ได้ลงคะแนนเลย) Btw ทำได้ดีมีข้อมูลที่เป็นประโยชน์มากมายขอบคุณ :)
BartoszKP

คำตอบ:


388

การสร้างอัลกอริทึม

เราเริ่มต้นด้วยการประกอบหน่วยการสร้างอัลกอริทึมจากไลบรารีมาตรฐาน:

#include <algorithm>    // min_element, iter_swap, 
                        // upper_bound, rotate, 
                        // partition, 
                        // inplace_merge,
                        // make_heap, sort_heap, push_heap, pop_heap,
                        // is_heap, is_sorted
#include <cassert>      // assert 
#include <functional>   // less
#include <iterator>     // distance, begin, end, next
  • เครื่องมือตัววนซ้ำเช่นไม่ใช่สมาชิกstd::begin()/ std::end()เช่นเดียวกับstd::next()พร้อมใช้งานตั้งแต่ C ++ 11 ขึ้นไป สำหรับ C ++ 98 จำเป็นต้องเขียนด้วยตนเอง มีสารทดแทนจาก Boost.Range อยู่ในboost::begin()/ boost::end()และจาก Boost.Utility boost::next()ใน
  • std::is_sortedอัลกอริทึมจะใช้ได้เฉพาะสำหรับ C ++ 11 และเกิน สำหรับ C ++ 98 สิ่งนี้สามารถนำไปใช้ในแง่ของstd::adjacent_findและฟังก์ชั่นวัตถุที่เขียนด้วยมือ Boost.Algorithm ยังboost::algorithm::is_sortedเป็นทางเลือกแทน
  • std::is_heapอัลกอริทึมจะใช้ได้เฉพาะสำหรับ C ++ 11 และเกิน

สารพัดเรื่อง

C ++ 14 ให้การเปรียบเทียบแบบโปร่งใสของรูปแบบstd::less<>ที่ทำ polymorphically บนอาร์กิวเมนต์ของพวกเขา วิธีนี้หลีกเลี่ยงการระบุประเภทตัววนซ้ำ สิ่งนี้สามารถใช้ร่วมกับอาร์กิวเมนต์เท็มเพลตฟังก์ชันดีฟอลต์ของ C ++ 11 เพื่อสร้างโอเวอร์โหลดเดียวสำหรับการเรียงลำดับอัลกอริทึมที่ใช้<เป็นการเปรียบเทียบและที่มีฟังก์ชั่นการเปรียบเทียบที่ผู้ใช้กำหนด

template<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});

ใน C ++ 11 เราสามารถกำหนดเทมเพลตนามแฝงที่ใช้ซ้ำได้เพื่อแยกประเภทค่าของตัววนซ้ำซึ่งเพิ่มความยุ่งเหยิงเล็กน้อยให้กับลายเซ็นอัลกอริทึมของการเรียงลำดับ:

template<class It>
using value_type_t = typename std::iterator_traits<It>::value_type;

template<class It, class Compare = std::less<value_type_t<It>>>
void xxx_sort(It first, It last, Compare cmp = Compare{});

ใน C ++ 98 เราจำเป็นต้องเขียน overloads สองรายการและใช้typename xxx<yyy>::typeไวยากรณ์verbose

template<class It, class Compare>
void xxx_sort(It first, It last, Compare cmp); // general implementation

template<class It>
void xxx_sort(It first, It last)
{
    xxx_sort(first, last, std::less<typename std::iterator_traits<It>::value_type>());
}
  • ความดีทางไวยากรณ์อื่น ๆ ก็คือ C ++ 14 ช่วยให้ผู้เปรียบเทียบที่ผู้ใช้กำหนดสามารถทำการตัดผ่านpolymorphic lambdas (พร้อมautoพารามิเตอร์ที่อนุมานได้ว่าเป็นฟังก์ชันเท็มเพลตอาร์กิวเมนต์)
  • C ++ 11 มีเพียง lambdas monomorphic value_type_tที่ต้องใช้แม่แบบนามแฝงดังกล่าวข้างต้น
  • ใน C ++ 98, หนึ่งทั้งความต้องการที่จะเขียนวัตถุฟังก์ชั่นสแตนด์อโลนหรือรีสอร์ทเพื่อ verbose std::bind1st/ std::bind2nd/ std::not1ประเภทของไวยากรณ์
  • Boost.Bind ปรับปรุงสิ่งนี้ด้วยboost::bindและ_1/ _2ไวยากรณ์ตัวยึด
  • C ++ 11 และนอกเหนือจากนี้ยังมีstd::find_if_notในขณะที่ C ++ 98 ความต้องการstd::find_ifที่มีstd::not1รอบวัตถุฟังก์ชั่น

สไตล์ C ++

ยังไม่มีสไตล์ C ++ 14 ที่ยอมรับโดยทั่วไป สำหรับดีขึ้นหรือแย่ลงผมอย่างใกล้ชิดต่อสกอตต์เมเยอร์ของร่างที่มีประสิทธิภาพสมัยใหม่ C ++และสมุนไพรซัทเทอปรับปรุง GotW ฉันใช้คำแนะนำรูปแบบต่อไปนี้:

  • สมุนไพรซัทเทอ"เกือบเสมออัตโนมัติ"และสกอตต์เมเยอร์สฯ"ชอบรถยนต์ที่จะประกาศประเภทเฉพาะ"ข้อเสนอแนะซึ่งความกะทัดรัดไม่มีที่เปรียบแม้ความชัดเจนบางครั้งจะโต้แย้ง
  • สกอตต์เมเยอร์ของ"ความแตกต่าง()และ{}เมื่อมีการสร้างวัตถุ"และสม่ำเสมอเลือกยัน-เริ่มต้น{}แทนการเริ่มต้นวงเล็บดีเก่า()(ในการสั่งซื้อไปทางด้านขั้นตอนทั้งหมดส่วนใหญ่รบกวน-แจงปัญหาในรหัสทั่วไป)
  • สกอตต์เมเยอร์สฯ"ชอบการประกาศชื่อแทน typedefs" สำหรับเทมเพลตนี่เป็นสิ่งที่ต้องทำต่อไปและใช้งานได้ทุกที่แทนที่จะtypedefประหยัดเวลาและเพิ่มความมั่นคง
  • ฉันใช้for (auto it = first; it != last; ++it)รูปแบบในบางสถานที่เพื่อให้สามารถตรวจสอบลูปค่าคงที่สำหรับช่วงย่อยที่เรียงลำดับแล้ว ในรหัสการผลิตการใช้งานwhile (first != last)และ++firstภายในวงอาจดีกว่าเล็กน้อย

เรียงลำดับการคัดเลือก

ตัวเลือกการเรียงลำดับO(N²)ไม่ได้ปรับให้เข้ากับข้อมูลในทางใดทางหนึ่งเพื่อให้รันไทม์อยู่เสมอ อย่างไรก็ตามการเลือกเรียงลำดับมีคุณสมบัติของการลดจำนวนของสัญญาแลกเปลี่ยน ในแอปพลิเคชันที่มีค่าใช้จ่ายในการแลกเปลี่ยนไอเท็มสูงการเรียงลำดับตัวเลือกที่ดีอาจเป็นอัลกอริทึมของตัวเลือก

ในการนำไปใช้โดยใช้ไลบรารีมาตรฐานให้ใช้ซ้ำ ๆstd::min_elementเพื่อค้นหาองค์ประกอบขั้นต่ำที่เหลืออยู่และiter_swapสลับไปที่:

template<class FwdIt, class Compare = std::less<>>
void selection_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last; ++it) {
        auto const selection = std::min_element(it, last, cmp);
        std::iter_swap(selection, it); 
        assert(std::is_sorted(first, std::next(it), cmp));
    }
}

โปรดทราบว่าselection_sortมีการประมวลผลช่วงแล้ว[first, it)เรียงเป็นวนไม่เปลี่ยนแปลง ข้อกำหนดขั้นต่ำคือส่งต่อตัวทำซ้ำเมื่อเทียบกับตัวstd::sortวนซ้ำของการเข้าถึงแบบสุ่ม

ละเว้นรายละเอียด :

  • การจัดเรียงตัวเลือกที่สามารถเพิ่มประสิทธิภาพด้วยการทดสอบในช่วงต้นif (std::distance(first, last) <= 1) return;(หรือไปข้างหน้า / iterators สองทิศทาง: if (first == last || std::next(first) == last) return;)
  • สำหรับตัววนซ้ำแบบสองทิศทางการทดสอบข้างต้นสามารถรวมกับการวนซ้ำในช่วงเวลา[first, std::prev(last))ได้เนื่องจากองค์ประกอบสุดท้ายจะรับประกันว่าเป็นองค์ประกอบที่เหลือน้อยที่สุดและไม่จำเป็นต้องสลับ

เรียงลำดับการแทรก

แม้ว่ามันจะเป็นหนึ่งในอัลกอริธึมการเรียงลำดับขั้นต้นที่มีO(N²)เวลากรณีที่แย่ที่สุดการเรียงลำดับการแทรกเป็นอัลกอริธึมที่เลือกเช่นกันเมื่อข้อมูลเกือบจะจัดเรียง (เพราะเป็นการปรับตัว ) หรือเมื่อขนาดของปัญหามีขนาดเล็ก ด้วยเหตุผลเหล่านี้และเนื่องจากมีความเสถียรการเรียงลำดับการแทรกจึงมักถูกใช้เป็นกรณีพื้นฐานแบบเรียกซ้ำ (เมื่อขนาดของปัญหาเล็ก) สำหรับอัลกอริทึมการเรียงลำดับการแบ่งและพิชิตที่สูงกว่าเช่นการเรียงแบบผสานหรือการเรียงแบบรวดเร็ว

หากต้องการนำไปใช้insertion_sortกับไลบรารีมาตรฐานให้ใช้ซ้ำ ๆstd::upper_boundเพื่อค้นหาตำแหน่งที่องค์ประกอบปัจจุบันจำเป็นต้องไปและใช้std::rotateเพื่อเลื่อนองค์ประกอบที่เหลือขึ้นไปในช่วงอินพุต:

template<class FwdIt, class Compare = std::less<>>
void insertion_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last; ++it) {
        auto const insertion = std::upper_bound(first, it, *it, cmp);
        std::rotate(insertion, it, std::next(it)); 
        assert(std::is_sorted(first, std::next(it), cmp));
    }
}

โปรดทราบว่าinsertion_sortมีการประมวลผลช่วงแล้ว[first, it)เรียงเป็นวนไม่เปลี่ยนแปลง การเรียงลำดับการแทรกยังทำงานกับตัววนซ้ำไปข้างหน้า

ละเว้นรายละเอียด :

  • การเรียงลำดับการแทรกสามารถปรับให้เหมาะสมได้ด้วยการทดสอบก่อนหน้าif (std::distance(first, last) <= 1) return;(หรือสำหรับตัววนไปข้างหน้า / สองทิศทาง:) if (first == last || std::next(first) == last) return;และวนรอบช่วงเวลา[std::next(first), last)เนื่องจากองค์ประกอบแรกรับประกันว่าจะอยู่ในสถานที่และไม่จำเป็นต้องหมุน
  • สำหรับตัววนซ้ำแบบสองทิศทางการค้นหาแบบไบนารีเพื่อค้นหาจุดแทรกสามารถถูกแทนที่ด้วยการค้นหาแบบเชิงเส้นย้อนกลับโดยใช้std::find_if_notอัลกอริทึมของไลบรารีมาตรฐาน

ตัวอย่างสดสี่รายการ ( C ++ 14 , C ++ 11 , C ++ 98 และ Boost , C ++ 98 ) สำหรับแฟรกเมนต์ด้านล่าง:

using RevIt = std::reverse_iterator<BiDirIt>;
auto const insertion = std::find_if_not(RevIt(it), RevIt(first), 
    [=](auto const& elem){ return cmp(*it, elem); }
).base();
  • สำหรับอินพุตแบบสุ่มสิ่งนี้ให้O(N²)การเปรียบเทียบ แต่สิ่งนี้จะปรับปรุงเป็นการO(N)เปรียบเทียบสำหรับอินพุตที่เรียงลำดับเกือบ การค้นหาแบบไบนารีจะใช้O(N log N)การเปรียบเทียบเสมอ
  • สำหรับช่วงอินพุตขนาดเล็กตำแหน่งหน่วยความจำที่ดีกว่า (แคชการดึงข้อมูลล่วงหน้า) ของการค้นหาเชิงเส้นอาจมีอิทธิพลเหนือการค้นหาแบบไบนารี่ (หนึ่งควรทดสอบสิ่งนี้แน่นอน)

จัดเรียงด่วน

เมื่อดำเนินการอย่างรอบคอบการเรียงลำดับอย่างรวดเร็วมีความแข็งแกร่งและO(N log N)คาดว่ามีความซับซ้อน แต่มีO(N²)ความซับซ้อนในกรณีที่เลวร้ายที่สุดที่สามารถเรียกใช้ข้อมูลอินพุตที่เลือกได้ เมื่อไม่ต้องการการจัดเรียงที่มีเสถียรภาพการจัดเรียงอย่างรวดเร็วคือการจัดเรียงวัตถุประสงค์ทั่วไปที่ยอดเยี่ยม

แม้สำหรับรุ่นที่ง่ายที่สุดการจัดเรียงอย่างรวดเร็วก็ค่อนข้างซับซ้อนกว่าเล็กน้อยในการใช้งานโดยใช้ไลบรารีมาตรฐานกว่าอัลกอริทึมการเรียงลำดับแบบคลาสสิกอื่น ๆ วิธีการด้านล่างใช้ยูทิลิตี iterator สองสามตัวเพื่อค้นหาองค์ประกอบกลางของช่วงอินพุต[first, last)เป็น pivot จากนั้นใช้การเรียกสองครั้งไปยังstd::partition(ซึ่งคือO(N)) เพื่อแบ่งพาร์ติชันสามทางเป็นช่วงของอินพุตในเซ็กเมนต์ขององค์ประกอบที่เล็กกว่าเท่ากับ และใหญ่กว่าเดือยที่เลือกตามลำดับ ในที่สุดทั้งสองส่วนด้านนอกที่มีองค์ประกอบที่เล็กกว่าและใหญ่กว่าเดือยจะถูกเรียงลำดับซ้ำ:

template<class FwdIt, class Compare = std::less<>>
void quick_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    auto const N = std::distance(first, last);
    if (N <= 1) return;
    auto const pivot = *std::next(first, N / 2);
    auto const middle1 = std::partition(first, last, [=](auto const& elem){ 
        return cmp(elem, pivot); 
    });
    auto const middle2 = std::partition(middle1, last, [=](auto const& elem){ 
        return !cmp(pivot, elem);
    });
    quick_sort(first, middle1, cmp); // assert(std::is_sorted(first, middle1, cmp));
    quick_sort(middle2, last, cmp);  // assert(std::is_sorted(middle2, last, cmp));
}

อย่างไรก็ตามการจัดเรียงอย่างรวดเร็วค่อนข้างยุ่งยากในการได้รับความถูกต้องและมีประสิทธิภาพเนื่องจากขั้นตอนข้างต้นแต่ละขั้นจะต้องมีการตรวจสอบอย่างรอบคอบและปรับให้เหมาะสมสำหรับรหัสระดับการผลิต โดยเฉพาะอย่างยิ่งสำหรับO(N log N)ความซับซ้อนเดือยจะต้องส่งผลให้เกิดการแบ่งพาร์ติชันที่สมดุลของข้อมูลอินพุตซึ่งไม่สามารถรับประกันได้โดยทั่วไปสำหรับO(1)เดือย แต่สามารถรับประกันได้ถ้ามีการตั้งค่าเดือยเป็นO(N)ค่ามัธยฐานของช่วงอินพุต

ละเว้นรายละเอียด :

  • การนำไปใช้ข้างต้นนั้นมีความเสี่ยงเป็นพิเศษต่ออินพุตพิเศษเช่นมีO(N^2)ความซับซ้อนสำหรับอินพุต " ท่ออวัยวะ " 1, 2, 3, ..., N/2, ... 3, 2, 1(เนื่องจากตรงกลางใหญ่กว่าองค์ประกอบอื่น ๆ เสมอ)
  • ค่ามัธยฐานของการเลือกแบบเดือย 3จากองค์ประกอบที่เลือกแบบสุ่มจากการ์ดช่วงอินพุตกับอินพุตที่เรียงลำดับเกือบซึ่งความซับซ้อนจะลดลงเป็นO(N^2)อย่างอื่น
  • การแบ่งพาร์ติชันแบบ 3 ทาง (การแยกองค์ประกอบที่เล็กกว่าเท่ากับและใหญ่กว่าเดือย) ดังที่แสดงโดยการเรียกสองครั้งไปstd::partitionไม่ใช่O(N)วิธีที่มีประสิทธิภาพที่สุดในการบรรลุผลลัพธ์นี้
  • สำหรับiterators เข้าถึงโดยสุ่ม , รับประกันO(N log N)ความซับซ้อนสามารถทำได้โดยการเลือกแบ่งเดือยใช้std::nth_element(first, middle, last)ตามด้วยโทร recursive ไปและquick_sort(first, middle, cmp)quick_sort(middle, last, cmp)
  • การรับประกันนี้มีค่าใช้จ่ายอย่างไรก็ตามเนื่องจากปัจจัยคงที่ของO(N)ความซับซ้อนของstd::nth_elementอาจมีราคาแพงกว่าO(1)ความซับซ้อนของค่ามัธยฐานของ 3 เดือยตามด้วยการO(N)เรียกไปstd::partitionที่ ข้อมูล).

เรียงลำดับการผสาน

หากการใช้O(N)พื้นที่พิเศษไม่ต้องกังวลการผสานการจัดเรียงเป็นตัวเลือกที่ยอดเยี่ยม: มันเป็นอัลกอริทึมการเรียงลำดับที่เสถียร เท่านั้นO(N log N)

มันง่ายที่จะใช้งานโดยใช้อัลกอริธึมมาตรฐาน: ใช้ยูทิลิตีตัววนซ้ำเพื่อค้นหาช่วงกลางของช่วงอินพุต[first, last)และรวมสองเซ็กเมนต์ที่เรียงลำดับซ้ำด้วยstd::inplace_merge:

template<class BiDirIt, class Compare = std::less<>>
void merge_sort(BiDirIt first, BiDirIt last, Compare cmp = Compare{})
{
    auto const N = std::distance(first, last);
    if (N <= 1) return;                   
    auto const middle = std::next(first, N / 2);
    merge_sort(first, middle, cmp); // assert(std::is_sorted(first, middle, cmp));
    merge_sort(middle, last, cmp);  // assert(std::is_sorted(middle, last, cmp));
    std::inplace_merge(first, middle, last, cmp); // assert(std::is_sorted(first, last, cmp));
}

merge sort ต้อง iterators std::inplace_mergeสองทิศทางคอขวดเป็น โปรดทราบว่าเมื่อเรียงลำดับรายการที่เชื่อมโยงการเรียงลำดับผสานต้องO(log N)ใช้พื้นที่เพิ่มเติมเท่านั้น(สำหรับการเรียกซ้ำ) อัลกอริทึมหลังถูกนำมาใช้โดยstd::list<T>::sortในไลบรารีมาตรฐาน

เรียงลำดับกอง

การเรียงลำดับฮีปนั้นง่ายต่อการใช้งานทำการO(N log N)เรียงลำดับในสถานที่ แต่ไม่เสถียร

ลูปแรกO(N)เฟส "heapify" วางอาร์เรย์ลงในลำดับฮีพ ลูปที่สองคือO(N log Nเฟส) "sortdown" จะแยกจำนวนสูงสุดซ้ำ ๆ และเรียกคืนลำดับฮีพ ไลบรารีมาตรฐานทำให้สิ่งนี้ตรงไปตรงมามาก:

template<class RandomIt, class Compare = std::less<>>
void heap_sort(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    lib::make_heap(first, last, cmp); // assert(std::is_heap(first, last, cmp));
    lib::sort_heap(first, last, cmp); // assert(std::is_sorted(first, last, cmp));
}

ในกรณีที่คุณพิจารณาว่าเป็น "การโกง" ที่จะใช้std::make_heapและstd::sort_heapคุณสามารถไปได้ลึกกว่าระดับหนึ่งและเขียนฟังก์ชั่นเหล่านั้นด้วยตัวเองในแง่ของstd::push_heapและstd::pop_heapตามลำดับ:

namespace lib {

// NOTE: is O(N log N), not O(N) as std::make_heap
template<class RandomIt, class Compare = std::less<>>
void make_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last;) {
        std::push_heap(first, ++it, cmp); 
        assert(std::is_heap(first, it, cmp));           
    }
}

template<class RandomIt, class Compare = std::less<>>
void sort_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    for (auto it = last; it != first;) {
        std::pop_heap(first, it--, cmp);
        assert(std::is_heap(first, it, cmp));           
    } 
}

}   // namespace lib

ห้องสมุดมาตรฐานระบุทั้งpush_heapและความซับซ้อนpop_heap O(log N)โปรดทราบว่าการวนรอบนอกของช่วง[first, last)ทำให้เกิดO(N log N)ความซับซ้อนmake_heapในขณะที่std::make_heapมีO(N)ความซับซ้อนเท่านั้น สำหรับO(N log N)ความซับซ้อนโดยรวมของheap_sortมันไม่สำคัญ

ละเว้นรายละเอียด : O(N)การดำเนินการของmake_heap

การทดสอบ

นี่คือตัวอย่างสดสี่รายการ ( C ++ 14 , C ++ 11 , C ++ 98 และ Boost , C ++ 98 ) ทดสอบอัลกอริธึมทั้งห้าทั้งหมดในอินพุตที่หลากหลาย เพียงสังเกตความแตกต่างอย่างมากใน LOC: C ++ 11 / C ++ 14 ต้องการประมาณ 130 LOC, C ++ 98 และ Boost 190 (+ 50%) และ C ++ 98 มากกว่า 270 (+ 100%)


13
ในขณะที่ฉันไม่เห็นด้วยกับการใช้งานของคุณauto (และหลายคนไม่เห็นด้วยกับฉัน) ฉันสนุกกับการดูอัลกอริทึมไลบรารีมาตรฐานที่ใช้งานได้ดี ฉันต้องการเห็นตัวอย่างของรหัสประเภทนี้หลังจากได้เห็นการพูดคุยของ Sean Parent นอกจากนี้ผมมีความคิดที่มีอยู่แม้ว่ามันจะดูเหมือนว่าแปลกกับผมว่ามันอยู่ในstd::iter_swap <algorithm>
Joseph Mansfield

32
@sbabbi ไลบรารีมาตรฐานทั้งหมดตั้งอยู่บนหลักการที่ว่าตัวทำซ้ำมีราคาถูกในการคัดลอก มันผ่านพวกเขาตามตัวอักษรตัวอย่างเช่น หากการคัดลอกตัววนซ้ำไม่ถูกคุณก็จะประสบปัญหาด้านประสิทธิภาพในทุกที่
James Kanze

2
โพสต์ยอดเยี่ยม เกี่ยวกับส่วนที่โกงของ [std ::] make_heap ถ้า std :: make_heap ถูกพิจารณาว่าโกงดังนั้น std :: push_heap Ie cheating = ไม่ใช้พฤติกรรมตามจริงที่กำหนดไว้สำหรับโครงสร้างฮีป ฉันจะพบว่ามันให้คำแนะนำรวม push_heap ด้วย
Captain Giraffe

3
@gnzlbg การยืนยันที่คุณสามารถแสดงความคิดเห็นได้แน่นอน การทดสอบในช่วงต้นสามารถแท็กส่งต่อหมวดหมู่ iterator if (first == last || std::next(first) == last)กับรุ่นปัจจุบันสำหรับการเข้าถึงแบบสุ่มและ ฉันอาจอัปเดตในภายหลัง การนำสิ่งต่าง ๆ ไปใช้ในส่วน "รายละเอียดที่ละเว้น" นั้นอยู่นอกเหนือขอบเขตของคำถาม IMO เนื่องจากมีลิงก์ไปยัง Q & As ทั้งหมด การใช้งานรูทีนการเรียงลำดับแบบเรียลไทม์เป็นเรื่องยาก!
TemplateRex

3
โพสต์ยอดเยี่ยม ถึงแม้ว่าคุณจะได้โกงกับ quicksort ของคุณโดยใช้nth_elementในความคิดของฉัน nth_elementทำครึ่งทางสั้น ๆ แล้ว (รวมถึงขั้นตอนการแบ่งและการเรียกซ้ำในครึ่งที่รวมองค์ประกอบที่ n ที่คุณสนใจ)
sellibitze

14

อีกคนหนึ่งที่มีขนาดเล็กและค่อนข้างหรูหราเดิมที่พบในการตรวจสอบรหัส ฉันคิดว่ามันคุ้มค่าที่จะแบ่งปัน

การเรียงลำดับการนับ

ในขณะที่มันค่อนข้างเชี่ยวชาญการเรียงลำดับการนับเป็นอัลกอริธึมการเรียงลำดับเลขจำนวนเต็มอย่างง่ายและมักจะรวดเร็วมากหากค่าของจำนวนเต็มเรียงไม่ไกลเกินไป มันอาจจะเหมาะถ้าใครต้องการเรียงลำดับคอลเลกชันของจำนวนเต็มหนึ่งล้านที่รู้กันว่าอยู่ระหว่าง 0 ถึง 100

ในการดำเนินการเรียงลำดับการนับที่ง่ายมากที่ทำงานกับทั้งจำนวนเต็มที่ลงนามและไม่ได้ลงนามหนึ่งต้องค้นหาองค์ประกอบที่เล็กที่สุดและยิ่งใหญ่ที่สุดในคอลเลกชันที่จะเรียงลำดับ; ความแตกต่างของพวกเขาจะบอกขนาดของจำนวนแถวที่จะจัดสรร จากนั้นจะมีการส่งผ่านชุดที่สองเพื่อนับจำนวนการเกิดขึ้นของทุกองค์ประกอบ สุดท้ายเราก็เขียนจำนวนที่ต้องการของจำนวนเต็มทุกตัวกลับไปที่คอลเล็กชันดั้งเดิม

template<typename ForwardIterator>
void counting_sort(ForwardIterator first, ForwardIterator last)
{
    if (first == last || std::next(first) == last) return;

    auto minmax = std::minmax_element(first, last);  // avoid if possible.
    auto min = *minmax.first;
    auto max = *minmax.second;
    if (min == max) return;

    using difference_type = typename std::iterator_traits<ForwardIterator>::difference_type;
    std::vector<difference_type> counts(max - min + 1, 0);

    for (auto it = first ; it != last ; ++it) {
        ++counts[*it - min];
    }

    for (auto count: counts) {
        first = std::fill_n(first, count, min++);
    }
}

ในขณะที่มันจะมีประโยชน์เฉพาะเมื่อช่วงของจำนวนเต็มเรียงเป็นที่รู้กันว่าเล็ก (โดยทั่วไปไม่ใหญ่กว่าขนาดของการรวบรวมการเรียงลำดับ) การนับเรียงทั่วไปมากกว่าจะทำให้มันช้าลงสำหรับกรณีที่ดีที่สุด ถ้าช่วงที่ไม่ได้เป็นที่รู้จักกันให้มีขนาดเล็กขั้นตอนวิธีการอื่นเช่นสมุฎฐานการจัดเรียง , ska_sortหรือspreadsortสามารถใช้แทน

ละเว้นรายละเอียด :

  • เราสามารถผ่านขอบเขตของช่วงของค่าที่ยอมรับโดยอัลกอริทึมเป็นพารามิเตอร์เพื่อกำจัดการstd::minmax_elementส่งผ่านครั้งแรกโดยสิ้นเชิง สิ่งนี้จะทำให้อัลกอริทึมเร็วยิ่งขึ้นเมื่อมีการใช้ขอบเขตอื่น ๆ (มันไม่ได้เป็นที่แน่นอน; ผ่านคงที่ 0-100 ยังคงมากดีกว่าผ่านพิเศษกว่าล้านองค์ประกอบที่พบว่าขอบเขตที่แท้จริง 1 ถึง 95 แม้ 0-1000 จะคุ้มค่านั้น องค์ประกอบพิเศษจะถูกเขียนครั้งเดียวด้วยศูนย์และอ่านครั้งเดียว)

  • การเติบโตอย่างcountsรวดเร็วเป็นอีกวิธีหนึ่งในการหลีกเลี่ยงการผ่านครั้งแรกแยกต่างหาก การเพิ่มcountsขนาดเป็นสองเท่าในแต่ละครั้งที่มีการโตขึ้นจะให้เวลา O (1) ที่ถูกตัดจำหน่ายต่อองค์ประกอบที่เรียง (ดูการวิเคราะห์ค่าใช้จ่ายการแทรกตารางแฮชเพื่อพิสูจน์ว่าการเติบโตแบบทวีคูณเป็นกุญแจสำคัญ) การเติบโตในตอนท้ายสำหรับสิ่งใหม่maxนั้นง่ายด้วยstd::vector::resizeการเพิ่มองค์ประกอบที่เป็นศูนย์ใหม่ การเปลี่ยนแปลงได้minทันทีและการใส่องค์ประกอบที่เป็นศูนย์ใหม่ที่ด้านหน้าสามารถทำได้ด้วยstd::copy_backwardหลังจากเติบโตเวกเตอร์ จากนั้นstd::fillไปที่ศูนย์องค์ประกอบใหม่

  • การcountsเพิ่มลูปคือฮิสโตแกรม หากข้อมูลมีแนวโน้มที่จะซ้ำซ้อนสูงและจำนวนของถังขยะมีขนาดเล็กก็อาจคุ้มค่าในการเปิดใช้งานหลายอาร์เรย์เพื่อลดปัญหาคอขวดของการพึ่งพาข้อมูลต่อเนื่องของร้านค้า / โหลดไปยังถังขยะเดียวกัน นี่หมายถึงการนับจำนวนมากขึ้นเป็นศูนย์ในตอนเริ่มต้นและอีกมากที่จะวนซ้ำในตอนท้าย แต่ควรจะคุ้มกับซีพียูส่วนใหญ่สำหรับตัวอย่างของเราจาก 0 ถึง 100 ตัวเลขโดยเฉพาะอย่างยิ่งถ้าอินพุทอาจจะเรียงลำดับแล้วบางส่วน มีจำนวนที่รันนานเหมือนกัน

  • ในอัลกอริทึมด้านบนเราใช้การmin == maxตรวจสอบเพื่อส่งคืน แต่เนิ่นๆเมื่อทุกองค์ประกอบมีค่าเท่ากัน เป็นไปได้จริงที่จะทำการตรวจสอบอย่างเต็มรูปแบบว่ามีการจัดเรียงคอลเลกชันแล้วในขณะที่ค้นหาค่าสูงสุดของคอลเลกชันโดยไม่เสียเวลาเพิ่มเติม (ถ้าการส่งครั้งแรกยังคงเป็นปัญหาหน่วยความจำ อย่างไรก็ตามอัลกอริทึมดังกล่าวไม่มีอยู่ในไลบรารีมาตรฐานและการเขียนจะน่าเบื่อกว่าการเขียนเรียงลำดับการนับที่เหลือเอง มันเหลือเป็นแบบฝึกหัดสำหรับผู้อ่าน

  • เนื่องจากอัลกอริทึมใช้งานได้เฉพาะกับค่าจำนวนเต็มจึงสามารถใช้การยืนยันแบบคงที่เพื่อป้องกันผู้ใช้จากการทำผิดประเภทที่เห็นได้ชัด ในบางบริบทอาจใช้การแทนที่ด้วยความล้มเหลวstd::enable_if_tได้

  • ในขณะที่ C ++ ที่ทันสมัยนั้นเจ๋งในอนาคต C ++ นั้นอาจจะเย็นกว่านี้: การผูกโครงสร้างและบางส่วนของRanges TSจะทำให้อัลกอริธึมสะอาดยิ่งขึ้น


@TemplateRex ถ้ามันสามารถใช้วัตถุเปรียบเทียบโดยพลการมันจะทำให้การเรียงลำดับการเปรียบเทียบการเรียงลำดับและการเรียงลำดับการเปรียบเทียบไม่สามารถมีกรณีที่เลวร้ายที่สุดดีกว่า O (n log n) การเรียงลำดับการนับมีกรณีที่เลวร้ายที่สุดของ O (n + r) ซึ่งหมายความว่ามันไม่สามารถเปรียบเทียบแบบเรียงลำดับได้ จำนวนเต็มสามารถเปรียบเทียบได้ แต่คุณสมบัตินี้ไม่ได้ใช้ในการเรียงลำดับ (ใช้ในการstd::minmax_elementรวบรวมข้อมูลเท่านั้น) คุณสมบัติที่ใช้คือความจริงที่ว่าจำนวนเต็มสามารถใช้เป็นดัชนีหรือออฟเซ็ตและสามารถเพิ่มได้ในขณะที่รักษาคุณสมบัติหลัง
Morwenn

ช่วง TS ย่อมเป็นสิ่งที่ดีมากเช่นวงสุดท้ายสามารถมากกว่าcounts | ranges::view::filter([](auto c) { return c != 0; })เพื่อที่คุณจะได้ไม่ต้องซ้ำ ๆ fill_nสำหรับการทดสอบนับเป็นศูนย์ภายใน
TemplateRex

(ผมพบความผิดพลาดในและ- ผมอาจจะให้พวกเขา til แก้ไขเกี่ยวกับการ reggae_sort?)small ratherappart
greybeard

@greybeard คุณสามารถทำสิ่งที่คุณต้องการ: p
Morwenn

ฉันสงสัยว่าการเติบโตอย่างcounts[]รวดเร็วจะเป็นชัยชนะเมื่อเทียบกับอินพุทminmax_elementก่อนการฮิสโตแกรม โดยเฉพาะอย่างยิ่งสำหรับกรณีการใช้งานที่เหมาะอย่างยิ่งสำหรับอินพุตที่มีขนาดใหญ่มากและมีการทำซ้ำหลายครั้งในช่วงขนาดเล็กเนื่องจากคุณจะเติบโตcountsเป็นขนาดเต็มได้อย่างรวดเร็ว (แน่นอนการรู้ขอบเขตที่เล็กพอที่จะทำให้คุณสามารถหลีกเลี่ยงการminmax_elementสแกนและหลีกเลี่ยงการตรวจสอบขอบเขตภายในฮิสโตแกรมได้)
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.