การสร้างอัลกอริทึม
เราเริ่มต้นด้วยการประกอบหน่วยการสร้างอัลกอริทึมจากไลบรารีมาตรฐาน:
#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%)