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