Vorrei aggiungere alle grandi risposte esistenti alcune considerazioni matematiche su come QuickSort si comporta quando si discosta dal caso migliore e quanto è probabile, che spero aiuterà le persone a capire un po 'meglio perché il caso O (n ^ 2) non è reale preoccupazione per le implementazioni più sofisticate di QuickSort.
Al di fuori dei problemi di accesso casuale, esistono due fattori principali che possono influire sulle prestazioni di QuickSort e sono entrambi correlati al modo in cui il pivot confronta con i dati ordinati.
1) Un piccolo numero di chiavi nei dati. Un set di dati dello stesso valore verrà ordinato in n ^ 2 volte su un QuickSort a 2 partizioni vaniglia perché tutti i valori tranne la posizione pivot vengono posizionati su un lato ogni volta. Le moderne implementazioni affrontano questo con metodi come l'uso di un ordinamento a 3 partizioni. Questi metodi vengono eseguiti su un set di dati dello stesso valore in O (n) time. Pertanto, l'utilizzo di un'implementazione del genere significa che un input con un numero limitato di chiavi migliora effettivamente i tempi delle prestazioni e non è più un problema.
2) La selezione estremamente pivot può causare prestazioni nel caso peggiore. In un caso ideale, il perno sarà sempre tale che il 50% dei dati sia più piccolo e il 50% che i dati siano più grandi, in modo che l'input venga interrotto a metà durante ogni iterazione. Questo ci dà n confronti e tempi di scambio log-2 (n) ricorsioni per il tempo O (n * logn).
Quanto influisce la selezione del pivot non ideale sui tempi di esecuzione?
Consideriamo un caso in cui il pivot viene scelto coerentemente in modo tale che il 75% dei dati si trovi su un lato del pivot. È ancora O (n * logn) ma ora la base del registro è cambiata in 1 / 0,75 o 1,33. La relazione nelle prestazioni quando si cambia base è sempre una costante rappresentata da log (2) / log (newBase). In questo caso, quella costante è 2.4. Quindi questa qualità della scelta del perno richiede 2,4 volte più a lungo dell'ideale.
Quanto velocemente peggiora?
Non molto velocemente fino a quando la scelta del perno diventa (costantemente) molto cattiva:
- 50% su un lato: (custodia ideale)
- 75% su un lato: 2,4 volte più a lungo
- 90% su un lato: 6,6 volte più lungo
- 95% su un lato: 13,5 volte di più
- 99% su un lato: 69 volte più a lungo
Mentre ci avviciniamo al 100% da un lato, la parte log dell'esecuzione si avvicina a n e l'intera esecuzione si avvicina asintoticamente a O (n ^ 2).
In un'implementazione ingenua di QuickSort, casi come un array ordinato (per il pivot del primo elemento) o un array in ordine inverso (per il pivot dell'ultimo elemento) produrranno in modo affidabile un tempo di esecuzione O (n ^ 2) nel caso peggiore. Inoltre, le implementazioni con una prevedibile selezione pivot possono essere soggette all'attacco DoS da dati progettati per produrre l'esecuzione nel caso peggiore. Le implementazioni moderne lo evitano con una varietà di metodi, come randomizzare i dati prima dell'ordinamento, scegliere la mediana di 3 indici scelti casualmente, ecc. Con questa randomizzazione nel mix, abbiamo 2 casi:
- Piccolo set di dati. Il caso peggiore è ragionevolmente possibile ma O (n ^ 2) non è catastrofico perché n è abbastanza piccolo che n ^ 2 è anche piccolo.
- Set di dati di grandi dimensioni. Il caso peggiore è possibile in teoria ma non in pratica.
Quanto è probabile che assistiamo a prestazioni terribili?
Le possibilità sono minuscole . Consideriamo una sorta di 5.000 valori:
La nostra ipotetica implementazione sceglierà un perno usando una mediana di 3 indici scelti casualmente. Considereremo "buoni" i pivot compresi tra il 25% e il 75% e i pivot compresi tra 0% -25% o tra il 75% e il 100%. Se osservi la distribuzione di probabilità usando la mediana di 3 indici casuali, ogni ricorsione ha una probabilità 11/16 di finire con un buon perno. Facciamo 2 ipotesi conservative (e false) per semplificare la matematica:
I buoni pivot sono sempre esattamente con una divisione del 25% / 75% e funzionano nel caso ideale 2.4 *. Non otteniamo mai una divisione ideale o una divisione migliore di 25/75.
I perni difettosi sono sempre i casi peggiori e essenzialmente non contribuiscono alla soluzione.
La nostra implementazione di QuickSort si fermerà a n = 10 e passerà a un ordinamento di inserzione, quindi abbiamo bisogno di 22 partizioni pivot del 25% / 75% per abbattere il valore di 5.000 immessi fino a quel punto. (10 * 1.333333 ^ 22> 5000) Oppure abbiamo bisogno di 4990 perni nel caso peggiore. Tieni presente che se accumuliamo 22 buoni pivot in qualsiasi momento, l'ordinamento verrà completato, quindi il caso peggiore o qualcosa di simile richiede estremamente sfortuna. Se ci volessero 88 ricorsioni per raggiungere effettivamente i 22 buoni pivot richiesti per ordinare fino a n = 10, sarebbe il caso ideale 4 * 2,4 * o circa 10 volte il tempo di esecuzione del caso ideale. Quante probabilità ci sono che avremmo non ottenere i richiesti 22 buoni perni dopo 88 ricorsioni?
Le distribuzioni di probabilità binomiale possono rispondere a questa domanda e la risposta è circa 10 ^ -18. (n è 88, k è 21, p è 0,6875) Il tuo utente ha circa mille volte più probabilità di essere colpito da un fulmine nel giro di 1 secondo che serve per fare clic su [ORDINE] di quanto non lo siano per vedere che l'ordinamento di 5.000 articoli peggiora di 10 * custodia ideale. Questa possibilità si riduce con l'aumentare del set di dati. Ecco alcune dimensioni dell'array e le relative possibilità di esecuzione più lunghe di 10 * ideali:
- Matrice di 640 articoli: 10 ^ -13 (richiede 15 buoni punti pivot su 60 tentativi)
- Matrice di 5.000 articoli: 10 ^ -18 (richiede 22 buoni pivot su 88 tentativi)
- Matrice di 40.000 articoli: 10 ^ -23 (richiede 29 buoni pivot su 116)
Ricorda che questo è con 2 ipotesi conservative che sono peggiori della realtà. Quindi le prestazioni effettive sono ancora migliori e l'equilibrio delle probabilità rimanenti è più vicino all'ideale che no.
Infine, come altri hanno già detto, anche questi casi assurdamente improbabili possono essere eliminati passando a un tipo di heap se lo stack di ricorsione va troppo in profondità. Quindi il TLDR è che, per le buone implementazioni di QuickSort, il caso peggiore non esiste davvero perché è stato progettato e l'esecuzione è stata completata in tempo O (n * logn).
qsort
, Pythonlist.sort
eArray.prototype.sort
JavaScript di Firefox sono tutte unite in modo confuso. (GNU STLsort
usa invece Introsort, ma ciò potrebbe essere dovuto al fatto che in C ++ lo scambio potenzialmente vince molto rispetto alla copia.)