Blok bangunan algoritma
Kita mulai dengan merakit blok bangunan algoritmik dari Perpustakaan Standar:
#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
- alat iterator seperti non-anggota
std::begin()/ std::end()serta dengan std::next()hanya tersedia pada C ++ 11 dan seterusnya. Untuk C ++ 98, orang perlu menulis ini sendiri. Ada pengganti dari Boost.Range di boost::begin()/ boost::end(), dan dari Boost.Utility di boost::next().
- yang
std::is_sortedalgoritma ini hanya tersedia untuk C ++ 11 dan seterusnya. Untuk C ++ 98, ini dapat diimplementasikan dalam hal std::adjacent_finddan objek fungsi tulisan tangan. Boost.Algorithm juga menyediakan boost::algorithm::is_sortedsebagai penggantinya.
- yang
std::is_heapalgoritma ini hanya tersedia untuk C ++ 11 dan seterusnya.
Barang sintaksis
C ++ 14 memberikan komparator transparan dari bentuk std::less<>yang bertindak secara polimorfik pada argumen mereka. Ini menghindari keharusan memberikan tipe iterator. Ini dapat digunakan dalam kombinasi dengan argumen templat fungsi default C ++ 11 untuk membuat kelebihan tunggal untuk menyortir algoritma yang dianggap <sebagai perbandingan dan yang memiliki objek fungsi perbandingan yang ditentukan pengguna.
template<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});
Di C ++ 11, seseorang dapat mendefinisikan alias templat yang dapat digunakan kembali untuk mengekstrak tipe nilai iterator yang menambahkan kekacauan kecil pada tanda tangan pengurutan algoritma:
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{});
Dalam C ++ 98, kita perlu menulis dua overload dan menggunakan typename xxx<yyy>::typesintaksis 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>());
}
- Keramahan sintaksis lainnya adalah bahwa C ++ 14 memfasilitasi pembungkus komparator yang ditentukan pengguna melalui lambda polimorfik (dengan
autoparameter yang dideduksi seperti argumen templat fungsi).
- C ++ 11 hanya memiliki lambda monomorfik, yang membutuhkan penggunaan templat alias di atas
value_type_t.
- Dalam C ++ 98, kita perlu menulis objek fungsi mandiri atau menggunakan sintaks verbose
std::bind1st/ std::bind2nd/ std::not1.
- Boost.Bind meningkatkan ini dengan
boost::binddan _1/ _2sintaks placeholder.
- C ++ 11 dan seterusnya juga memiliki
std::find_if_not, sedangkan C ++ 98 perlu std::find_ifdengan std::not1sekitar objek fungsi.
Gaya C ++
Belum ada gaya C ++ 14 yang dapat diterima secara umum. Untuk lebih baik atau lebih buruk, saya dengan cermat mengikuti rancangan Scott Modern Pengacara C ++ Modern Efektif dan GotW yang dirubah oleh Herb Sutter . Saya menggunakan rekomendasi gaya berikut:
- Rekomendasi Herb Sutter "Almost Always Auto" dan Scott Meyers "Prefer auto to specific type declaration" , yang singkatnya tidak tertandingi, walaupun kejelasannya terkadang diperdebatkan .
- Scott Meyers's "Membedakan
()dan {}ketika membuat objek" dan secara konsisten memilih inisialisasi bracing {}alih - alih inisialisasi yang diurung lama yang baik ()(untuk memihak semua masalah yang paling menjengkelkan-parse dalam kode generik).
- Scott Meyers "Memilih alias deklarasi untuk mengetik" . Untuk templat, ini adalah keharusan, dan menggunakannya di mana-mana alih-alih
typedefmenghemat waktu dan menambah konsistensi.
- Saya menggunakan
for (auto it = first; it != last; ++it)pola di beberapa tempat, untuk memungkinkan pemeriksaan invarian lingkaran untuk sub-rentang yang sudah diurutkan. Dalam kode produksi, penggunaan while (first != last)dan suatu ++firsttempat di dalam loop mungkin sedikit lebih baik.
Sortir seleksi
Sortir pemilihan tidak beradaptasi dengan data dengan cara apa pun, sehingga runtime selaluO(N²). Namun, pemilihan semacam memiliki sifat meminimalkan jumlah swap . Dalam aplikasi di mana biaya item bertukar tinggi, pemilihan semacam sangat baik mungkin merupakan algoritma pilihan.
Untuk mengimplementasikannya menggunakan Perpustakaan Standar, berulang kali gunakan std::min_elementuntuk menemukan elemen minimum yang tersisa, dan iter_swapuntuk menukar itu ke tempatnya:
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));
}
}
Perhatikan bahwa selection_sortrentang yang sudah diproses [first, it)diurutkan sebagai loop invarian. Persyaratan minimal adalah iterator maju , dibandingkan dengan std::sortiterator akses acak.
Detail dihilangkan :
- jenis seleksi dapat dioptimalkan dengan tes awal
if (std::distance(first, last) <= 1) return;(atau untuk iterators maju / dua arah:) if (first == last || std::next(first) == last) return;.
- untuk iterator dua arah , tes di atas dapat dikombinasikan dengan loop selama interval
[first, std::prev(last)), karena elemen terakhir dijamin menjadi elemen yang tersisa minimal dan tidak memerlukan swap.
Jenis penyisipan
Meskipun ini adalah salah satu algoritma pengurutan dasar dengan O(N²)waktu kasus terburuk, jenis penyisipan adalah algoritma pilihan baik ketika data hampir diurutkan (karena adaptif ) atau ketika ukuran masalahnya kecil (karena memiliki overhead rendah). Untuk alasan ini, dan karena ini juga stabil , jenis penyisipan sering digunakan sebagai kasus dasar rekursif (ketika ukuran masalahnya kecil) untuk algoritma pengurutan pembagian-dan-penaklukan overhead yang lebih tinggi, seperti pengurutan gabungan atau pengurutan cepat.
Untuk menerapkan insertion_sortdengan Perpustakaan Standar, berulang kali gunakan std::upper_bounduntuk menemukan lokasi di mana elemen saat ini perlu pergi, dan gunakan std::rotateuntuk menggeser elemen yang tersisa ke atas dalam rentang input:
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));
}
}
Perhatikan bahwa insertion_sortrentang yang sudah diproses [first, it)diurutkan sebagai loop invarian. Jenis penyisipan juga berfungsi dengan iterator maju.
Detail dihilangkan :
- jenis penyisipan dapat dioptimalkan dengan tes awal
if (std::distance(first, last) <= 1) return;(atau untuk iterator maju / dua arah:) if (first == last || std::next(first) == last) return;dan loop di atas interval [std::next(first), last), karena elemen pertama dijamin berada di tempatnya dan tidak memerlukan rotasi.
- untuk iterator dua arah , pencarian biner untuk menemukan titik penyisipan dapat diganti dengan pencarian linear terbalik dengan menggunakan
std::find_if_notalgoritma Perpustakaan Standar .
Empat Contoh Langsung ( C ++ 14 , C ++ 11 , C ++ 98 dan Boost , C ++ 98 ) untuk fragmen di bawah ini:
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();
- Untuk input acak ini memberikan
O(N²)perbandingan, tetapi ini meningkatkan O(N)perbandingan untuk input yang hampir diurutkan. Pencarian biner selalu menggunakan O(N log N)perbandingan.
- Untuk rentang input kecil, lokalitas memori yang lebih baik (cache, prefetching) dari pencarian linier mungkin juga mendominasi pencarian biner (tentu saja orang harus menguji ini).
Sortir cepat
Ketika diimplementasikan dengan hati-hati, pengurutan cepat adalah kuat dan memiliki O(N log N)kompleksitas yang diharapkan, tetapi dengan O(N²)kompleksitas terburuk yang dapat dipicu dengan data input yang dipilih secara berlawanan. Ketika jenis stabil tidak diperlukan, jenis cepat adalah jenis tujuan umum yang sangat baik.
Bahkan untuk versi yang paling sederhana, penyortiran cepat agak sedikit lebih rumit untuk diterapkan menggunakan Perpustakaan Standar daripada algoritma penyortiran klasik lainnya. Pendekatan di bawah ini menggunakan beberapa utilitas iterator untuk menemukan elemen tengah dari rentang input [first, last)sebagai pivot, kemudian menggunakan dua panggilan ke std::partition(yang O(N)) untuk mempartisi tiga arah rentang input ke dalam segmen elemen yang lebih kecil dari, sama dengan, dan lebih besar dari pivot yang dipilih, masing-masing. Akhirnya dua segmen luar dengan elemen lebih kecil dari dan lebih besar dari pivot diurutkan secara rekursif:
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));
}
Namun, penyortiran cepat agak sulit untuk mendapatkan yang benar dan efisien, karena masing-masing langkah di atas harus hati-hati diperiksa dan dioptimalkan untuk kode tingkat produksi. Khususnya, untuk O(N log N)kompleksitas, pivot harus menghasilkan partisi yang seimbang dari data input, yang tidak dapat dijamin secara umum untuk O(1)pivot, tetapi yang dapat dijamin jika seseorang menetapkan pivot sebagai O(N)median rentang input.
Detail dihilangkan :
- implementasi di atas sangat rentan terhadap input khusus, misalnya memiliki
O(N^2)kompleksitas untuk input " pipa organ " 1, 2, 3, ..., N/2, ... 3, 2, 1(karena tengah selalu lebih besar dari semua elemen lainnya).
- median-of-3 seleksi pivot dari elemen yang dipilih secara acak dari pelindung rentang input terhadap input yang hampir diurutkan yang kompleksitasnya akan menurun
O(N^2).
- 3-way partisi (memisahkan elemen yang lebih kecil dari, sama dengan dan lebih besar dari pivot) seperti yang ditunjukkan oleh dua panggilan
std::partitionbukan merupakanO(N)algoritma yangpaling efisienuntuk mencapai hasil ini.
- untuk iterator akses acak ,
O(N log N)kompleksitas yang dijamin dapat dicapai melalui pemilihan median pivot menggunakan std::nth_element(first, middle, last), diikuti dengan panggilan rekursif ke quick_sort(first, middle, cmp)dan quick_sort(middle, last, cmp).
- jaminan ini datang pada biaya, bagaimanapun, karena faktor konstan dari
O(N)kompleksitas std::nth_elementdapat lebih mahal daripada O(1)kompleksitas median-of-3 pivot diikuti oleh O(N)panggilan ke std::partition(yang merupakan satu-satunya forward-friendly single cache-friendly melewati data).
Gabungkan semacam
Jika menggunakan O(N)ruang ekstra tidak menjadi masalah, maka menggabungkan jenis adalah pilihan yang sangat baik: itu adalah satu-satunya algoritma penyortiran yang stabil O(N log N) .
Mudah diterapkan menggunakan algoritma Standar: gunakan beberapa utilitas iterator untuk mencari bagian tengah rentang input [first, last)dan menggabungkan dua segmen yang diurutkan secara rekursif dengan 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));
}
Penggabungan jenis memerlukan iterator dua arah, hambatannya adalah std::inplace_merge. Perhatikan bahwa saat menyortir daftar yang ditautkan, menggabungkan jenis hanya membutuhkan O(log N)ruang tambahan (untuk rekursi). Algoritma yang terakhir diimplementasikan oleh std::list<T>::sortdi Perpustakaan Standar.
Heap sort
Heap sort mudah diimplementasikan, melakukanO(N log N)sortir di tempat, tetapi tidak stabil.
Loop pertama, O(N)fase "heapify", menempatkan array ke dalam urutan heap. Loop kedua, O(N log Nfase) "sortdown", berulang kali mengekstrak maksimum dan mengembalikan urutan tumpukan. Perpustakaan Standar membuat ini sangat mudah:
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));
}
Jika Anda menganggapnya "curang" untuk digunakan std::make_heapdan std::sort_heap, Anda dapat naik satu tingkat lebih dalam dan menulis sendiri fungsi-fungsi tersebut dalam hal std::push_heapdan std::pop_heap, masing-masing:
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
Perpustakaan Standar menentukan keduanya push_heapdan pop_heapsebagai kompleksitas O(log N). Namun perlu dicatat bahwa loop luar pada rentang [first, last)menghasilkan O(N log N)kompleksitas make_heap, sedangkan std::make_heaphanya memiliki O(N)kompleksitas. Untuk O(N log N)kerumitan keseluruhan heap_sortitu tidak masalah.
Rincian dihilangkan : O(N)implementasimake_heap
Pengujian
Berikut adalah empat Contoh Langsung ( C ++ 14 , C ++ 11 , C ++ 98 dan Boost , C ++ 98 ) yang menguji kelima algoritma pada berbagai input (tidak dimaksudkan untuk lengkap atau ketat). Perhatikan perbedaan besar pada LOC: C ++ 11 / C ++ 14 membutuhkan sekitar 130 LOC, C ++ 98 dan Boost 190 (+ 50%) dan C ++ 98 lebih dari 270 (+ 100%).