Come implementare algoritmi di ordinamento classici nel moderno C ++?


331

L' std::sortalgoritmo (e i suoi cugini std::partial_sorte std::nth_element) della libreria standard C ++ è nella maggior parte delle implementazioni una fusione complicata e ibrida di algoritmi di ordinamento più elementari , come ordinamento di selezione, ordinamento di inserimento, ordinamento rapido, ordinamento unito o ordinamento heap.

Ci sono molte domande qui e su siti gemelli come https://codereview.stackexchange.com/ relativi a bug, complessità e altri aspetti delle implementazioni di questi algoritmi di ordinamento classici. La maggior parte delle implementazioni offerte sono costituite da loop non elaborati, utilizzano manipolazione dell'indice e tipi concreti e sono generalmente poco banali da analizzare in termini di correttezza ed efficienza.

Domanda : come possono essere implementati i suddetti algoritmi di ordinamento classici usando il moderno C ++?

  • nessun loop non elaborato , ma combinando i blocchi costitutivi algoritmici della Standard Library da<algorithm>
  • interfaccia iteratore e utilizzo di modelli anziché manipolazione di indici e tipi concreti
  • Stile C ++ 14 , inclusa la libreria standard completa, nonché riduttori di rumore sintattici come autoalias di template, comparatori trasparenti e lambda polimorfici.

Note :

  • per ulteriori riferimenti sulle implementazioni degli algoritmi di ordinamento consultare Wikipedia , il codice Rosetta o http://www.sorting-algorithms.com/
  • secondo le convenzioni di Sean Parent (slide 39), un ciclo grezzo è un forciclo più lungo della composizione di due funzioni con un operatore. Quindi f(g(x));o f(x); g(x);o f(x) + g(x);non sono cicli prime, e non sono i loop in selection_sorte insertion_sortdi sotto.
  • Seguo la terminologia di Scott Meyers per indicare l'attuale C ++ 1y già come C ++ 14, e per indicare C ++ 98 e C ++ 03 entrambi come C ++ 98, quindi non darmi fuoco.
  • Come suggerito nei commenti di @Mehrdad, fornisco quattro implementazioni come esempio live alla fine della risposta: C ++ 14, C ++ 11, C ++ 98 e Boost e C ++ 98.
  • La risposta stessa è presentata solo in termini di C ++ 14. Se pertinente, denoto le differenze sintattiche e di libreria in cui differiscono le varie versioni linguistiche.

8
Sarebbe bello aggiungere il tag C ++ Faq alla domanda, anche se richiederebbe di perdere almeno uno degli altri. Suggerirei di rimuovere le versioni (in quanto si tratta di una domanda C ++ generica, con implementazioni disponibili nella maggior parte delle versioni con qualche adattamento).
Matthieu M.

@TemplateRex Bene, tecnicamente, se non si tratta di FAQ, allora questa domanda è troppo ampia (indovinando - non ho votato a fondo). Btw. buon lavoro, molte informazioni utili, grazie :)
BartoszKP,

Risposte:


388

Blocchi algoritmici

Iniziamo assemblando i blocchi algoritmici dalla libreria standard:

#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
  • gli strumenti iteratori come non membri std::begin()/ std::end()nonché con std::next()sono disponibili solo a partire da C ++ 11 e oltre. Per C ++ 98, è necessario scrivere questi stessi. Ci sono sostituti da Boost.Range in boost::begin()/ boost::end(), e da Boost.Utility in boost::next().
  • l' std::is_sortedalgoritmo è disponibile solo per C ++ 11 e versioni successive. Per C ++ 98, questo può essere implementato in termini di std::adjacent_finde un oggetto funzione scritto a mano. Boost.Algorithm fornisce anche boost::algorithm::is_sortedun sostituto.
  • l' std::is_heapalgoritmo è disponibile solo per C ++ 11 e versioni successive.

Chicche sintattiche

C ++ 14 fornisce comparatori trasparenti della forma std::less<>che agiscono polimorficamente sui loro argomenti. Questo evita di dover fornire un tipo di iteratore. Questo può essere usato in combinazione con gli argomenti del modello di funzione predefinito di C ++ 11 per creare un singolo sovraccarico per gli algoritmi di ordinamento che prendono <come confronto e quelli che hanno un oggetto funzione di confronto definito dall'utente.

template<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});

In C ++ 11, è possibile definire un alias modello riutilizzabile per estrarre il tipo di valore di un iteratore che aggiunge un disordine minore alle firme degli algoritmi di ordinamento:

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{});

In C ++ 98, è necessario scrivere due overload e utilizzare la typename xxx<yyy>::typesintassi dettagliata

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>());
}
  • Un'altra particolarità sintattica è che C ++ 14 facilita il wrapping di comparatori definiti dall'utente tramite lambda polimorfici (con autoparametri dedotti come argomenti del modello di funzione).
  • C ++ 11 ha solo lambda monomorfi, che richiedono l'uso dell'alias di modello sopra value_type_t.
  • In C ++ 98, è necessario scrivere un oggetto funzione autonomo o ricorrere al tipo di sintassi dettagliato std::bind1st/ std::bind2nd/ std::not1.
  • Boost.Bind migliora questo con la sintassi boost::binde _1/ _2segnaposto.
  • C ++ 11 e oltre hanno anche std::find_if_not, mentre C ++ 98 ha bisogno std::find_ifdi un std::not1oggetto funzione around.

Stile C ++

Non esiste ancora uno stile C ++ 14 generalmente accettabile. Nel bene o nel male, seguo da vicino la bozza di Effective Modern C ++ di Scott Meyers e il rinnovato GotW di Herb Sutter . Uso i seguenti consigli di stile:

  • La raccomandazione "Almost Always Auto" di Herb Sutter e le dichiarazioni "Preferire l'auto al tipo specifico " di Scott Meyers , per le quali la brevità è insuperabile, sebbene a volte la sua chiarezza sia contestata .
  • "Distinguere ()e {}quando si creano oggetti" di Scott Meyers e scegliere coerentemente {}l'inizializzazione con parentesi invece della buona vecchia inizializzazione tra parentesi ()(al fine di superare tutte le problematiche di analisi più fastidiose nel codice generico).
  • "Preferire le dichiarazioni alias a typedef" di Scott Meyers . Per i modelli questo è d'obbligo, e usarlo ovunque invece di typedefrisparmiare tempo e aggiungere coerenza.
  • Uso un for (auto it = first; it != last; ++it)pattern in alcuni punti, al fine di consentire il controllo invariante di loop per sottointervalli già ordinati. Nel codice di produzione, l'uso di while (first != last)e da ++firstqualche parte all'interno del ciclo potrebbe essere leggermente migliore.

Ordinamento di selezione

L'ordinamento di selezione non si adatta ai dati in alcun modo, quindi il suo tempo di esecuzione è sempreO(N²). Tuttavia, l'ordinamento per selezione ha la proprietà di ridurre al minimo il numero di swap . Nelle applicazioni in cui il costo dello scambio di articoli è elevato, l'ordinamento della selezione può essere l'algoritmo scelto.

Per implementarlo utilizzando la Libreria standard, utilizzare ripetutamente std::min_elementper trovare l'elemento minimo rimanente e iter_swapper scambiarlo in posizione:

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));
    }
}

Si noti che selection_sortl'intervallo già elaborato è [first, it)ordinato come invariante del ciclo. I requisiti minimi sono iteratori diretti , rispetto agli std::sortiteratori ad accesso casuale.

Dettagli omessi :

  • l'ordinamento della selezione può essere ottimizzato con un test iniziale if (std::distance(first, last) <= 1) return;(o per iteratori forward / bidirezionali:) if (first == last || std::next(first) == last) return;.
  • per gli iteratori bidirezionali , il test sopra può essere combinato con un ciclo sull'intervallo [first, std::prev(last)), poiché l'ultimo elemento è garantito per essere l'elemento minimo rimanente e non richiede uno scambio.

Ordinamento per inserzione

Sebbene sia uno degli algoritmi di ordinamento elementare con il tempo O(N²)peggiore, l'ordinamento per inserzione è l'algoritmo di scelta quando i dati sono quasi ordinati (perché è adattivo ) o quando la dimensione del problema è piccola (perché ha un sovraccarico basso). Per questi motivi, e poiché è anche stabile , l'ordinamento per inserzione viene spesso utilizzato come caso base ricorsivo (quando la dimensione del problema è ridotta) per gli algoritmi di ordinamento di divisione e conquista superiori, come ad esempio unisci ordine o ordinamento rapido.

Per implementare insertion_sortcon la Libreria standard, utilizzare ripetutamente std::upper_boundper trovare la posizione in cui deve andare l'elemento corrente e utilizzare std::rotateper spostare gli elementi rimanenti verso l'alto nell'intervallo di 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));
    }
}

Si noti che insertion_sortl'intervallo già elaborato è [first, it)ordinato come invariante del ciclo. L'ordinamento per inserzione funziona anche con iteratori diretti.

Dettagli omessi :

  • l'ordinamento di inserzione può essere ottimizzato con un test iniziale if (std::distance(first, last) <= 1) return;(o per iteratori forward / bidirezionali:) if (first == last || std::next(first) == last) return;e un ciclo sull'intervallo [std::next(first), last), perché il primo elemento è garantito per essere in atto e non richiede una rotazione.
  • per gli iteratori bidirezionali , la ricerca binaria per trovare il punto di inserimento può essere sostituita con una ricerca lineare inversa usando l' std::find_if_notalgoritmo della libreria standard .

Quattro esempi live ( C ++ 14 , C ++ 11 , C ++ 98 e Boost , C ++ 98 ) per il frammento seguente:

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();
  • Per gli input casuali questo fornisce O(N²)confronti, ma migliora i O(N)confronti per input quasi ordinati. La ricerca binaria utilizza sempre O(N log N)confronti.
  • Per piccoli intervalli di input, la migliore località di memoria (cache, prefetching) di una ricerca lineare potrebbe anche dominare una ricerca binaria (si dovrebbe testare questo, ovviamente).

Ordinamento rapido

Se implementato con cura, l' ordinamento rapido è robusto e ha O(N log N)previsto la complessità, ma con la O(N²)complessità del caso peggiore che può essere attivata con i dati di input scelti in modo contraddittorio. Quando non è necessario un ordinamento stabile, l'ordinamento rapido è un eccellente ordinamento generale.

Anche per le versioni più semplici, l'ordinamento rapido è un po 'più complicato da implementare utilizzando la libreria standard rispetto agli altri algoritmi di ordinamento classici. L'approccio seguente utilizza alcune utilità iteratore per individuare l' elemento intermedio dell'intervallo di input [first, last)come pivot, quindi utilizzare due chiamate a std::partition(che sono O(N)) per partizionare a tre vie l'intervallo di input in segmenti di elementi più piccoli di, uguale a, e più grande del perno selezionato, rispettivamente. Infine, i due segmenti esterni con elementi più piccoli e più grandi del perno vengono ordinati in modo ricorsivo:

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));
}

Tuttavia, l'ordinamento rapido è piuttosto difficile da ottenere corretto ed efficiente, poiché ciascuno dei passaggi precedenti deve essere attentamente controllato e ottimizzato per il codice del livello di produzione. In particolare, per O(N log N)complessità, il pivot deve risultare in una partizione bilanciata dei dati di input, che non può essere garantita in generale per un O(1)pivot, ma che può essere garantita se si imposta il pivot come O(N)mediana dell'intervallo di input.

Dettagli omessi :

  • l'implementazione di cui sopra è particolarmente vulnerabile a input speciali, ad esempio ha O(N^2)complessità per l' input " pipe organ " 1, 2, 3, ..., N/2, ... 3, 2, 1(perché la parte centrale è sempre più grande di tutti gli altri elementi).
  • selezione del pivot mediano di 3 da elementi scelti casualmente dalle protezioni del campo di input contro input quasi ordinati per i quali altrimenti la complessità si deteriorerebbeO(N^2).
  • Il partizionamento a 3 vie (elementi di separazione più piccoli di, uguali e più grandi del perno) come mostrato dalle due chiamate astd::partitionnon è l'O(N)algoritmopiù efficienteper raggiungere questo risultato.
  • per gli iteratori ad accesso casuale , è O(N log N)possibile ottenere una complessità garantita mediante la selezione del pivot mediano mediante std::nth_element(first, middle, last), seguita da chiamate ricorsive a quick_sort(first, middle, cmp)e quick_sort(middle, last, cmp).
  • questa garanzia ha un costo, tuttavia, poiché il fattore costante della O(N)complessità di std::nth_elementpuò essere più costoso di quello della O(1)complessità di un pivot mediano di 3 seguito da una O(N)chiamata a std::partition(che è un passaggio di inoltro singolo compatibile con la cache i dati).

Unisci ordinamento

Se l'utilizzo di O(N)spazio extra non è un problema, allora unire l'ordinamento è una scelta eccellente: è l'unico algoritmo di ordinamento stabile O(N log N) .

È semplice da implementare usando algoritmi standard: usa alcune utility iteratore per localizzare il centro dell'intervallo di input [first, last)e combinare due segmenti ordinati in modo ricorsivo con un 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));
}

Unisci ordinamento richiede iteratori bidirezionali, il collo di bottiglia è il std::inplace_merge. Si noti che quando si ordinano gli elenchi collegati, unisci ordinamento richiede solo O(log N)spazio aggiuntivo (per la ricorsione). Quest'ultimo algoritmo è implementato std::list<T>::sortnella Libreria standard.

Ordinamento dell'heap

L'ordinamento heap è semplice da implementare, esegue unO(N log N)ordinamento sul posto, ma non è stabile.

Il primo ciclo, la O(N)fase "heapify", mette l'array in ordine heap. Il secondo ciclo, la O(N log Nfase "sortdown", estrae ripetutamente il massimo e ripristina l'ordine degli heap. La libreria standard lo rende estremamente semplice:

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));
}

Nel caso in cui lo consideri "barare" da usare std::make_heape std::sort_heap, puoi andare di un livello più in profondità e scrivere quelle funzioni da solo in termini di std::push_heape std::pop_heap, rispettivamente:

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

La libreria standard specifica sia push_heape pop_heapcome complessità O(log N). Si noti tuttavia che il ciclo esterno sull'intervallo [first, last)provoca O(N log N)complessità make_heap, mentre std::make_heapha solo O(N)complessità. Per la complessità O(N log N)complessiva heap_sortnon importa.

Dettagli omessi : O(N)implementazione dimake_heap

analisi

Ecco quattro esempi live ( C ++ 14 , C ++ 11 , C ++ 98 e Boost , C ++ 98 ) che testano tutti e cinque gli algoritmi su una varietà di input (non intesi come esaustivi o rigorosi). Basta notare le enormi differenze nel LOC: C ++ 11 / C ++ 14 richiedono circa 130 LOC, C ++ 98 e Boost 190 (+ 50%) e C ++ 98 più di 270 (+ 100%).


13
Mentre non sono d'accordo con il tuo uso diauto (e molte persone non sono d'accordo con me), mi è piaciuto vedere che gli algoritmi di libreria standard vengono usati bene. Avrei voluto vedere alcuni esempi di questo tipo di codice dopo aver visto il discorso di Sean Parent. Inoltre, non avevo idea che std::iter_swapesistesse, anche se mi sembra strano che ci sia <algorithm>.
Joseph Mansfield,

32
@sbabbi L'intera libreria standard si basa sul principio secondo cui gli iteratori sono economici da copiare; li passa per valore, per esempio. Se copiare un iteratore non è economico, allora soffrirai problemi di prestazioni ovunque.
James Kanze,

2
Ottimo post. Per quanto riguarda la parte barare di [std ::] make_heap. Se std :: make_heap è considerato imbroglione, anche std :: push_heap. Cioè barare = non implementare il comportamento effettivo definito per una struttura di heap. Troverei istruttivo includere anche push_heap.
Capitan Giraffe

3
@gnzlbg Le affermazioni che puoi commentare, ovviamente. Il test iniziale può essere inviato per tag per categoria iteratore, con la versione corrente per l'accesso casuale e if (first == last || std::next(first) == last). Potrei aggiornarlo più tardi. L'implementazione dei contenuti nelle sezioni "dettagli omessi" va oltre lo scopo della domanda, IMO, perché contengono collegamenti a domande e risposte complete. L'implementazione di routine di ordinamento di parole reali è difficile!
TemplateRex,

3
Ottimo post. Tuttavia, hai imbrogliato con il tuo quicksort usando nth_elementsecondo me. nth_elementfa già mezzo quicksort (incluso il passaggio di partizionamento e una ricorsione sulla metà che include l'n-esimo elemento che ti interessa).
sellibitze,

14

Un altro piccolo e piuttosto elegante originariamente trovato sulla revisione del codice . Ho pensato che valesse la pena condividerlo.

Ordinamento di conteggio

Sebbene sia piuttosto specializzato, contare l'ordinamento è un semplice algoritmo di ordinamento di numeri interi e spesso può essere molto veloce a condizione che i valori degli interi da ordinare non siano troppo distanti. È probabilmente l'ideale se si ha mai bisogno di ordinare una raccolta di un milione di numeri interi conosciuti tra 0 e 100 per esempio.

Per implementare un ordinamento di conteggio molto semplice che funziona con numeri interi sia con segno sia senza segno, è necessario trovare gli elementi più piccoli e più grandi nella raccolta da ordinare; la loro differenza indica la dimensione dell'array di conteggi da allocare. Quindi, viene eseguito un secondo passaggio nella raccolta per contare il numero di occorrenze di ogni elemento. Infine, riscriviamo il numero richiesto di ogni numero intero nella raccolta originale.

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++);
    }
}

Sebbene sia utile solo quando l'intervallo degli interi da ordinare è piccolo (generalmente non più grande della dimensione della raccolta da ordinare), rendere il conteggio più generico lo renderebbe più lento per i suoi casi migliori. Se l'intervallo non è noto per essere piccolo, è possibile utilizzare un altro algoritmo come un ordinamento radix , ska_sort o spreadsort .

Dettagli omessi :

  • Avremmo potuto superare i limiti dell'intervallo di valori accettati dall'algoritmo come parametri per eliminare completamente il primo std::minmax_elementpassaggio della raccolta. Ciò renderà l'algoritmo ancora più veloce quando un limite di intervallo utilmente piccolo è noto con altri mezzi. (Non deve essere esatto; passare una costante da 0 a 100 è ancora molto meglio di un passaggio extra su un milione di elementi per scoprire che i limiti reali sono compresi tra 1 e 95. Ne varrebbe la pena anche da 0 a 1000; il gli elementi extra vengono scritti una volta con zero e letti una volta).

  • Crescere countsal volo è un altro modo per evitare un primo passaggio separato. Raddoppiando la countsdimensione ogni volta che deve crescere, si ottiene O (1) tempo ammortizzato per elemento ordinato (vedere l'analisi dei costi di inserimento della tabella hash per la prova che l'espansione potenziale è la chiave). Crescere alla fine per un nuovo maxè facile con std::vector::resizel'aggiunta di nuovi elementi azzerati. È possibile cambiare minal volo e inserire nuovi elementi azzerati nella parte anteriore std::copy_backwarddopo aver aumentato il vettore. Quindi std::fillazzerare i nuovi elementi.

  • Il countsloop di incremento è un istogramma. Se è probabile che i dati siano altamente ripetitivi e il numero di bin sia ridotto, può valere la pena srotolarlo su più array per ridurre il collo di bottiglia di dipendenza dei dati di serializzazione di store / ricaricare nello stesso bin. Ciò significa che un numero maggiore di conteggi a zero all'inizio e un numero maggiore di cicli alla fine, ma dovrebbe valerne la pena sulla maggior parte delle CPU per il nostro esempio di milioni da 0 a 100 numeri, soprattutto se l'input potrebbe essere già (parzialmente) ordinato e hanno lunghe tirate dello stesso numero.

  • Nell'algoritmo sopra, usiamo un min == maxsegno di spunta per tornare presto quando ogni elemento ha lo stesso valore (nel qual caso la raccolta è ordinata). In realtà è possibile invece controllare completamente se la raccolta è già ordinata mentre si trovano i valori estremi di una raccolta senza sprecare tempo aggiuntivo (se il primo passaggio è ancora il collo di bottiglia della memoria con il lavoro extra di aggiornamento min e max). Tuttavia un tale algoritmo non esiste nella libreria standard e scrivere uno sarebbe più noioso che scrivere il resto del conteggio stesso. È lasciato come esercizio per il lettore.

  • Poiché l'algoritmo funziona solo con valori interi, è possibile utilizzare asserzioni statiche per impedire agli utenti di commettere errori di tipo ovvio. In alcuni contesti, std::enable_if_tpotrebbe essere preferito un errore di sostituzione con .

  • Mentre il moderno C ++ è interessante, il futuro C ++ potrebbe essere ancora più interessante: i collegamenti strutturati e alcune parti di Ranges TS renderebbero l'algoritmo ancora più pulito.


@TemplateRex Se fosse in grado di prendere un oggetto di confronto arbitrario, renderebbe il conteggio dell'ordinamento un ordinamento di confronto e gli ordinamenti di confronto non potrebbero avere un caso peggiore di O (n log n). L'ordinamento di conteggio ha il caso peggiore di O (n + r), il che significa che non può essere comunque un ordinamento di confronto. I numeri interi possono essere confrontati ma questa proprietà non viene utilizzata per eseguire l'ordinamento (viene utilizzata solo nella std::minmax_elementquale vengono raccolte solo informazioni). La proprietà utilizzata è il fatto che gli interi possono essere utilizzati come indici o offset e che sono incrementabili preservando quest'ultima proprietà.
Morwenn,

Ranges TS è davvero molto bello, ad esempio il loop finale può essere finito in counts | ranges::view::filter([](auto c) { return c != 0; })modo da non dover più testare conteggi diversi da zero all'interno di fill_n.
TemplateRex

(Ho trovato errori di battitura in small un rather e appart- posso tenerli fino alla modifica riguardante reggae_sort?)
greybeard

@greybeard Puoi fare quello che vuoi: p
Morwenn,

Ho il sospetto che la crescita al counts[]volo sarebbe una vittoria contro l'attraversamento dell'input minmax_elementprima dell'istogramma. Soprattutto per il caso d'uso in cui questo è l'ideale, con input molto grandi con molte ripetizioni in un piccolo intervallo, perché crescerai rapidamente countsalla sua dimensione completa, con pochi fraintendimenti di rami o raddoppiamenti di dimensioni. (Ovviamente, conoscere un limite abbastanza piccolo sull'intervallo ti consentirà di evitare una minmax_elementscansione ed evitare il controllo dei limiti all'interno del loop dell'istogramma.)
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.