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_sorted
algoritma ini hanya tersedia untuk C ++ 11 dan seterusnya. Untuk C ++ 98, ini dapat diimplementasikan dalam hal std::adjacent_find
dan objek fungsi tulisan tangan. Boost.Algorithm juga menyediakan boost::algorithm::is_sorted
sebagai penggantinya.
- yang
std::is_heap
algoritma 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>::type
sintaksis 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
auto
parameter 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::bind
dan _1
/ _2
sintaks placeholder.
- C ++ 11 dan seterusnya juga memiliki
std::find_if_not
, sedangkan C ++ 98 perlu std::find_if
dengan std::not1
sekitar 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
typedef
menghemat 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 ++first
tempat 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_element
untuk menemukan elemen minimum yang tersisa, dan iter_swap
untuk 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_sort
rentang yang sudah diproses [first, it)
diurutkan sebagai loop invarian. Persyaratan minimal adalah iterator maju , dibandingkan dengan std::sort
iterator 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_sort
dengan Perpustakaan Standar, berulang kali gunakan std::upper_bound
untuk menemukan lokasi di mana elemen saat ini perlu pergi, dan gunakan std::rotate
untuk 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_sort
rentang 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_not
algoritma 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::partition
bukan 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_element
dapat 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>::sort
di 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 N
fase) "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_heap
dan std::sort_heap
, Anda dapat naik satu tingkat lebih dalam dan menulis sendiri fungsi-fungsi tersebut dalam hal std::push_heap
dan 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_heap
dan pop_heap
sebagai 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_heap
hanya memiliki O(N)
kompleksitas. Untuk O(N log N)
kerumitan keseluruhan heap_sort
itu 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%).