The std::sort
algoritma (dan sepupu std::partial_sort
dan std::nth_element
) dari C ++ Standar Perpustakaan di sebagian besar implementasi penggabungan rumit dan hybrid dari algoritma pengurutan yang lebih elementer , seperti pemilihan semacam, insertion sort, cepat semacam, semacam penggabungan, atau semacam tumpukan.
Ada banyak pertanyaan di sini dan di situs saudara seperti https://codereview.stackexchange.com/ yang terkait dengan bug, kompleksitas dan aspek lain dari implementasi dari algoritma pengurutan klasik ini. Sebagian besar implementasi yang ditawarkan terdiri dari loop mentah, menggunakan manipulasi indeks dan jenis beton, dan umumnya non-sepele untuk menganalisis dalam hal kebenaran dan efisiensi.
Pertanyaan : bagaimana bisa algoritma pengurutan klasik yang disebutkan di atas diimplementasikan menggunakan C ++ modern?
- tidak ada loop mentah , tetapi menggabungkan blok bangunan algoritmik Perpustakaan Standar dari
<algorithm>
- antarmuka iterator dan penggunaan templat alih-alih manipulasi indeks dan tipe konkret
- C ++ 14 gaya , termasuk Perpustakaan Standar lengkap, serta reduksi kebisingan sintaksis seperti
auto
, alias template, pembanding transparan dan lambdas polimorfik.
Catatan :
- untuk referensi lebih lanjut tentang implementasi algoritma pengurutan lihat Wikipedia , Rosetta Code atau http://www.sorting-algorithms.com/
- menurut konvensi Sean Parent (slide 39), loop mentah
for
lebih panjang dari komposisi dua fungsi dengan operator. Jadif(g(x));
atauf(x); g(x);
atauf(x) + g(x);
tidak loop mentah, dan tidak adalah loop dalamselection_sort
daninsertion_sort
bawah. - Saya mengikuti terminologi Scott Meyers untuk menunjukkan C ++ 1y saat ini sudah sebagai C ++ 14, dan untuk menunjukkan C ++ 98 dan C ++ 03 keduanya sebagai C ++ 98, jadi jangan nyalakan saya untuk itu.
- Seperti yang disarankan dalam komentar oleh @Mehrdad, saya memberikan empat implementasi sebagai Contoh Langsung di akhir jawaban: C ++ 14, C ++ 11, C ++ 98 dan Boost dan C ++ 98.
- Jawabannya disajikan hanya dalam bentuk C ++ 14 saja. Jika relevan, saya menunjukkan perbedaan sintaksis dan pustaka di mana berbagai versi bahasa berbeda.
Jawaban:
Blok bangunan algoritma
Kita mulai dengan merakit blok bangunan algoritmik dari Perpustakaan Standar:
std::begin()
/std::end()
serta denganstd::next()
hanya tersedia pada C ++ 11 dan seterusnya. Untuk C ++ 98, orang perlu menulis ini sendiri. Ada pengganti dari Boost.Range diboost::begin()
/boost::end()
, dan dari Boost.Utility diboost::next()
.std::is_sorted
algoritma ini hanya tersedia untuk C ++ 11 dan seterusnya. Untuk C ++ 98, ini dapat diimplementasikan dalam halstd::adjacent_find
dan objek fungsi tulisan tangan. Boost.Algorithm juga menyediakanboost::algorithm::is_sorted
sebagai penggantinya.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.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:
Dalam C ++ 98, kita perlu menulis dua overload dan menggunakan
typename xxx<yyy>::type
sintaksis verboseauto
parameter yang dideduksi seperti argumen templat fungsi).value_type_t
.std::bind1st
/std::bind2nd
/std::not1
.boost::bind
dan_1
/_2
sintaks placeholder.std::find_if_not
, sedangkan C ++ 98 perlustd::find_if
denganstd::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:
()
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).typedef
menghemat waktu dan menambah konsistensi.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, penggunaanwhile (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 selalu
O(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, daniter_swap
untuk menukar itu ke tempatnya:Perhatikan bahwa
selection_sort
rentang yang sudah diproses[first, it)
diurutkan sebagai loop invarian. Persyaratan minimal adalah iterator maju , dibandingkan denganstd::sort
iterator akses acak.Detail dihilangkan :
if (std::distance(first, last) <= 1) return;
(atau untuk iterators maju / dua arah:)if (first == last || std::next(first) == last) return;
.[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 gunakanstd::upper_bound
untuk menemukan lokasi di mana elemen saat ini perlu pergi, dan gunakanstd::rotate
untuk menggeser elemen yang tersisa ke atas dalam rentang input:Perhatikan bahwa
insertion_sort
rentang yang sudah diproses[first, it)
diurutkan sebagai loop invarian. Jenis penyisipan juga berfungsi dengan iterator maju.Detail dihilangkan :
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.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:
O(N²)
perbandingan, tetapi ini meningkatkanO(N)
perbandingan untuk input yang hampir diurutkan. Pencarian biner selalu menggunakanO(N log N)
perbandingan.Sortir cepat
Ketika diimplementasikan dengan hati-hati, pengurutan cepat adalah kuat dan memiliki
O(N log N)
kompleksitas yang diharapkan, tetapi denganO(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 kestd::partition
(yangO(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: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 untukO(1)
pivot, tetapi yang dapat dijamin jika seseorang menetapkan pivot sebagaiO(N)
median rentang input.Detail dihilangkan :
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).O(N^2)
.std::partition
bukan merupakanO(N)
algoritma yangpaling efisienuntuk mencapai hasil ini.O(N log N)
kompleksitas yang dijamin dapat dicapai melalui pemilihan median pivot menggunakanstd::nth_element(first, middle, last)
, diikuti dengan panggilan rekursif kequick_sort(first, middle, cmp)
danquick_sort(middle, last, cmp)
.O(N)
kompleksitasstd::nth_element
dapat lebih mahal daripadaO(1)
kompleksitas median-of-3 pivot diikuti olehO(N)
panggilan kestd::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 stabilO(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 denganstd::inplace_merge
:Penggabungan jenis memerlukan iterator dua arah, hambatannya adalah
std::inplace_merge
. Perhatikan bahwa saat menyortir daftar yang ditautkan, menggabungkan jenis hanya membutuhkanO(log N)
ruang tambahan (untuk rekursi). Algoritma yang terakhir diimplementasikan olehstd::list<T>::sort
di Perpustakaan Standar.Heap sort
Heap sort mudah diimplementasikan, melakukan
O(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:Jika Anda menganggapnya "curang" untuk digunakan
std::make_heap
danstd::sort_heap
, Anda dapat naik satu tingkat lebih dalam dan menulis sendiri fungsi-fungsi tersebut dalam halstd::push_heap
danstd::pop_heap
, masing-masing:Perpustakaan Standar menentukan keduanya
push_heap
danpop_heap
sebagai kompleksitasO(log N)
. Namun perlu dicatat bahwa loop luar pada rentang[first, last)
menghasilkanO(N log N)
kompleksitasmake_heap
, sedangkanstd::make_heap
hanya memilikiO(N)
kompleksitas. UntukO(N log N)
kerumitan keseluruhanheap_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%).
sumber
auto
(dan banyak orang tidak setuju dengan saya), saya senang melihat algoritma perpustakaan standar yang digunakan dengan baik. Saya ingin melihat beberapa contoh kode semacam ini setelah melihat pembicaraan Sean Parent. Juga, saya tidak tahustd::iter_swap
ada, meskipun tampaknya aneh bagi saya bahwa itu di<algorithm>
.if (first == last || std::next(first) == last)
. Saya mungkin memperbaruinya nanti. Menerapkan hal-hal di bagian "rincian yang dihilangkan" berada di luar cakupan pertanyaan, IMO, karena berisi tautan ke seluruh Tanya Jawab sendiri. Menerapkan rutinitas penyortiran kata-kata sebenarnya sulit!nth_element
menurut saya.nth_element
melakukan setengah quicksort sudah (termasuk langkah partisi dan rekursi pada setengah yang mencakup elemen ke-n yang Anda minati).Satu lagi kecil dan agak elegan awalnya ditemukan pada ulasan kode . Saya pikir itu layak untuk dibagikan.
Jenis penghitungan
Meskipun agak khusus, penghitungan sort adalah algoritma pengurutan integer sederhana dan sering kali bisa sangat cepat asalkan nilai integer untuk diurutkan tidak terlalu berjauhan. Mungkin ideal jika seseorang perlu mengurutkan koleksi satu juta bilangan bulat yang diketahui antara 0 dan 100 misalnya.
Untuk menerapkan jenis penghitungan yang sangat sederhana yang berfungsi baik dengan bilangan bulat bertanda tangan maupun tidak, kita perlu menemukan elemen terkecil dan terhebat dalam koleksi untuk disortir; perbedaan mereka akan memberi tahu ukuran array jumlah untuk dialokasikan. Kemudian, pass kedua melalui koleksi dilakukan untuk menghitung jumlah kemunculan setiap elemen. Akhirnya, kami menulis kembali jumlah yang diperlukan dari setiap bilangan bulat kembali ke koleksi asli.
Meskipun hanya berguna ketika kisaran bilangan bulat untuk disortir diketahui kecil (umumnya tidak lebih besar dari ukuran koleksi untuk disortir), membuat penghitungan lebih umum akan membuatnya lebih lambat untuk kasus terbaiknya. Jika rentang tidak diketahui kecil, algoritma lain seperti semacam radix , ska_sort , atau spreadsort dapat digunakan sebagai gantinya.
Detail dihilangkan :
Kita bisa melewati batas kisaran nilai yang diterima oleh algoritma sebagai parameter untuk benar-benar menyingkirkan
std::minmax_element
melewati pertama melalui koleksi. Ini akan membuat algoritma lebih cepat ketika batas rentang berguna-kecil diketahui dengan cara lain. (Tidak harus tepat; melewati konstan 0 hingga 100 masih jauh lebih baik daripada melewati ekstra lebih dari satu juta elemen untuk mengetahui bahwa batas sebenarnya adalah 1 hingga 95. Bahkan 0 hingga 1000 akan sepadan; elemen tambahan ditulis sekali dengan nol dan dibaca sekali).Bertumbuh
counts
dengan cepat adalah cara lain untuk menghindari umpan pertama yang terpisah. Menggandakancounts
ukuran setiap kali itu harus tumbuh memberikan O (1) waktu diamortisasi per elemen diurutkan (lihat analisis biaya penyisipan tabel hash untuk bukti bahwa tumbuh eksponensial adalah kuncinya). Menumbuhkan pada akhirnya untuk yang barumax
itu mudah denganstd::vector::resize
menambahkan elemen yang baru di-zeroed. Mengubahmin
dengan cepat dan memasukkan elemen yang baru saja di-zeroed di bagian depan dapat dilakukanstd::copy_backward
setelah menumbuhkan vektor. Kemudianstd::fill
untuk nol elemen baru.The
counts
kenaikan loop histogram. Jika data cenderung sangat berulang, dan jumlah nampan kecil, bisa bermanfaat membuka gulungan beberapa array untuk mengurangi hambatan ketergantungan serialisasi data store / reload ke nampan yang sama. Ini berarti lebih banyak hitungan ke nol di awal, dan lebih banyak untuk loop di akhir, tetapi harus layak pada kebanyakan CPU untuk contoh kita jutaan angka 0 hingga 100, terutama jika input mungkin sudah (sebagian) diurutkan dan memiliki jangka panjang dari nomor yang sama.Dalam algoritma di atas, kami menggunakan tanda
min == max
centang untuk kembali lebih awal ketika setiap elemen memiliki nilai yang sama (dalam hal ini koleksi diurutkan). Sebenarnya dimungkinkan untuk sepenuhnya memeriksa apakah koleksi sudah diurutkan sementara menemukan nilai-nilai ekstrem dari koleksi tanpa waktu tambahan yang terbuang (jika pass pertama masih memori macet dengan kerja ekstra memperbarui min dan max). Namun algoritma semacam itu tidak ada di perpustakaan standar dan menulis satu akan lebih membosankan daripada menulis sisa penghitungan semacam itu sendiri. Itu dibiarkan sebagai latihan untuk pembaca.Karena algoritma hanya bekerja dengan nilai integer, pernyataan statis dapat digunakan untuk mencegah pengguna membuat kesalahan tipe yang jelas. Dalam beberapa konteks, kegagalan substitusi dengan
std::enable_if_t
mungkin lebih disukai.Sementara C ++ modern itu keren, C ++ di masa depan bisa lebih dingin: binding terstruktur dan beberapa bagian dari Ranges TS akan membuat algoritma lebih bersih.
sumber
std::minmax_element
mana hanya mengumpulkan informasi). Properti yang digunakan adalah fakta bahwa bilangan bulat dapat digunakan sebagai indeks atau offset, dan bahwa mereka dapat ditingkatkan sambil menjaga properti yang terakhir.counts | ranges::view::filter([](auto c) { return c != 0; })
sehingga Anda tidak perlu berulang kali menguji jumlah yang tidak nol di dalamfill_n
.small
sebuahrather
danappart
- mungkin aku menjaga mereka til mengedit mengenai reggae_sort?)counts[]
cepat akan menjadi kemenangan vs melintasi inputminmax_element
sebelum histogram. Khusus untuk kasus penggunaan di mana ini ideal, input sangat besar dengan banyak pengulangan dalam kisaran kecil, karena Anda akan dengan cepat tumbuhcounts
ke ukuran penuh, dengan beberapa mispredict cabang atau penggandaan ukuran. (Tentu saja, mengetahui batas yang cukup kecil pada rentang akan memungkinkan Anda menghindariminmax_element
pemindaian dan menghindari pemeriksaan batas di dalam lingkaran histogram.)