Bagaimana menerapkan algoritma penyortiran klasik di C ++ modern?

331

The std::sortalgoritma (dan sepupu std::partial_sortdan 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 forlebih panjang dari komposisi dua fungsi dengan operator. Jadi f(g(x));atau f(x); g(x);atau f(x) + g(x);tidak loop mentah, dan tidak adalah loop dalam selection_sortdan insertion_sortbawah.
  • 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.
TemplateRex
sumber
8
Akan bagus untuk menambahkan tag C ++ Faq ke pertanyaan, meskipun itu harus kehilangan setidaknya satu dari yang lain. Saya akan menyarankan menghapus versi (karena ini adalah pertanyaan C ++ generik, dengan implementasi yang tersedia di sebagian besar versi dengan beberapa adaptasi).
Matthieu M.
@TemplateRex Nah, secara teknis, jika bukan FAQ maka pertanyaan ini terlalu luas (menebak - saya tidak downvote). Btw. kerja bagus, banyak informasi berguna, terima kasih :)
BartoszKP

Jawaban:

388

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 menurunO(N^2).
  • 3-way partisi (memisahkan elemen yang lebih kecil dari, sama dengan dan lebih besar dari pivot) seperti yang ditunjukkan oleh dua panggilanstd::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%).

TemplateRex
sumber
13
Sementara saya tidak setuju dengan penggunaan Andaauto (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 tahu std::iter_swapada, meskipun tampaknya aneh bagi saya bahwa itu di <algorithm>.
Joseph Mansfield
32
@ sbabbi Seluruh perpustakaan standar didasarkan pada prinsip bahwa iterator murah untuk disalin; itu melewati mereka dengan nilai, misalnya. Jika menyalin iterator tidak murah, maka Anda akan mengalami masalah kinerja di mana-mana.
James Kanze
2
Pos yang bagus. Mengenai bagian kecurangan dari [std ::] make_heap. Jika std :: make_heap dianggap curang, maka std :: push_heap. Yaitu curang: tidak menerapkan perilaku aktual yang ditentukan untuk struktur tumpukan. Saya akan menemukan itu telah memasukkan push_heap juga.
Kapten Giraffe
3
@gnzlbg Memastikan Anda bisa berkomentar, tentu saja. Tes awal dapat dikirim-tag per kategori iterator, dengan versi saat ini untuk akses acak, dan 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!
TemplateRex
3
Pos yang bagus. Padahal, Anda telah menipu dengan quicksort Anda dengan menggunakan nth_elementmenurut saya. nth_elementmelakukan setengah quicksort sudah (termasuk langkah partisi dan rekursi pada setengah yang mencakup elemen ke-n yang Anda minati).
sellibitze
14

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.

template<typename ForwardIterator>
void counting_sort(ForwardIterator first, ForwardIterator last)
{
    if (first == last || std::next(first) == last) return;

    auto minmax = std::minmax_element(first, last);  // avoid if possible.
    auto min = *minmax.first;
    auto max = *minmax.second;
    if (min == max) return;

    using difference_type = typename std::iterator_traits<ForwardIterator>::difference_type;
    std::vector<difference_type> counts(max - min + 1, 0);

    for (auto it = first ; it != last ; ++it) {
        ++counts[*it - min];
    }

    for (auto count: counts) {
        first = std::fill_n(first, count, min++);
    }
}

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_elementmelewati 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 countsdengan cepat adalah cara lain untuk menghindari umpan pertama yang terpisah. Menggandakan countsukuran 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 baru maxitu mudah dengan std::vector::resizemenambahkan elemen yang baru di-zeroed. Mengubah mindengan cepat dan memasukkan elemen yang baru saja di-zeroed di bagian depan dapat dilakukan std::copy_backwardsetelah menumbuhkan vektor. Kemudian std::filluntuk nol elemen baru.

  • The countskenaikan 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 == maxcentang 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_tmungkin 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.

Morwenn
sumber
@TemplateRex Jika ia dapat mengambil objek perbandingan sewenang-wenang, itu akan membuat penghitungan mengurutkan jenis perbandingan, dan jenis perbandingan tidak dapat memiliki kasus terburuk yang lebih baik daripada O (n log n). Jenis penghitungan memiliki kasus terburuk O (n + r), yang berarti bahwa itu bukan jenis perbandingan. Bilangan bulat dapat dibandingkan tetapi properti ini tidak digunakan untuk melakukan pengurutan (hanya digunakan di std::minmax_elementmana 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.
Morwenn
Rentang TS memang sangat bagus, misal putaran terakhir dapat berakhir counts | ranges::view::filter([](auto c) { return c != 0; })sehingga Anda tidak perlu berulang kali menguji jumlah yang tidak nol di dalam fill_n.
TemplateRex
(Saya menemukan kesalahan ketik dalam small sebuah rather dan appart- mungkin aku menjaga mereka til mengedit mengenai reggae_sort?)
orangtua
@greybeard Anda dapat melakukan apa pun yang Anda inginkan: p
Morwenn
Saya menduga bahwa menumbuhkan dengan counts[]cepat akan menjadi kemenangan vs melintasi input minmax_elementsebelum histogram. Khusus untuk kasus penggunaan di mana ini ideal, input sangat besar dengan banyak pengulangan dalam kisaran kecil, karena Anda akan dengan cepat tumbuh countske ukuran penuh, dengan beberapa mispredict cabang atau penggandaan ukuran. (Tentu saja, mengetahui batas yang cukup kecil pada rentang akan memungkinkan Anda menghindari minmax_elementpemindaian dan menghindari pemeriksaan batas di dalam lingkaran histogram.)
Peter Cordes