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_sorted
algoritmo è disponibile solo per C ++ 11 e versioni successive. Per C ++ 98, questo può essere implementato in termini di std::adjacent_find
e un oggetto funzione scritto a mano. Boost.Algorithm fornisce anche boost::algorithm::is_sorted
un sostituto.
- l'
std::is_heap
algoritmo è 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>::type
sintassi 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
auto
parametri 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::bind
e _1
/ _2
segnaposto.
- C ++ 11 e oltre hanno anche
std::find_if_not
, mentre C ++ 98 ha bisogno std::find_if
di un std::not1
oggetto 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
typedef
risparmiare 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 ++first
qualche 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_element
per trovare l'elemento minimo rimanente e iter_swap
per 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_sort
l'intervallo già elaborato è [first, it)
ordinato come invariante del ciclo. I requisiti minimi sono iteratori diretti , rispetto agli std::sort
iteratori 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_sort
con la Libreria standard, utilizzare ripetutamente std::upper_bound
per trovare la posizione in cui deve andare l'elemento corrente e utilizzare std::rotate
per 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_sort
l'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_not
algoritmo 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 deteriorerebbe
O(N^2)
.
- Il partizionamento a 3 vie (elementi di separazione più piccoli di, uguali e più grandi del perno) come mostrato dalle due chiamate a
std::partition
non è 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_element
può 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>::sort
nella 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 N
fase "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_heap
e std::sort_heap
, puoi andare di un livello più in profondità e scrivere quelle funzioni da solo in termini di std::push_heap
e 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_heap
e pop_heap
come 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_heap
ha solo O(N)
complessità. Per la complessità O(N log N)
complessiva heap_sort
non 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%).