Partizionamento Quicksort: Hoare vs. Lomuto


83

Esistono due metodi di partizione quicksort menzionati in Cormen:

Hoare-Partition(A, p, r)
x = A[p]
i = p - 1
j = r + 1
while true
    repeat
        j = j - 1
    until A[j] <= x
    repeat
        i = i + 1
    until A[i] >= x
    if i < j
        swap( A[i], A[j] )
    else
        return j

e:

Lomuto-Partition(A, p, r)
x = A[r]
i = p - 1
for j = p to r - 1
    if A[j] <= x
        i = i + 1
        swap( A[i], A[j] )
swap( A[i +1], A[r] )
return i + 1

Ignorando il metodo di scelta del perno, in quali situazioni è preferibile l'una all'altra? So ad esempio che Lomuto si preforma in modo relativamente scarso quando c'è un'alta percentuale di valori duplicati (cioè dove diciamo più di 2/3 l'array ha lo stesso valore), dove Hoare si comporta bene in quella situazione.

Quali altri casi speciali rendono significativamente migliore un metodo di partizione rispetto all'altro?


2
Non riesco a pensare a nessuna situazione in cui Lomuto è migliore di Hoare. Sembra che Lomuto esegua scambi extra ogni volta A[i+1] <= x. In una matrice ordinata (e dati i perni scelti ragionevolmente) Hoare non fa praticamente swap e Lomuto fa una tonnellata (una volta che j diventa abbastanza piccolo di tutto il resto A[j] <= x). Cosa mi manca?
Wandering Logic,

2
@WanderingLogic Non ne sono sicuro, ma sembra che la decisione di Cormen di usare la partizione di Lomuto nel suo libro possa essere pedagogica - sembra avere un invariante loop piuttosto diretto.
Robert S. Barnes,

2
Nota che quei due algoritmi non fanno la stessa cosa. Alla fine dell'algoritmo di Hoare, il perno non è al suo posto finale. Puoi aggiungere swap(A[p], A[j])a alla fine di Hoare's per ottenere lo stesso comportamento per entrambi.
Aurélien Ooms,

Dovresti anche controllare i < jnei 2 loop ripetitivi del partizionamento di Hoare.
Aurélien Ooms,

@ AurélienOoms Il codice viene copiato direttamente dal libro.
Robert S. Barnes,

Risposte:


92

Dimensione pedagogica

Per la sua semplicità, il metodo di partizionamento di Lomuto potrebbe essere più semplice da implementare. C'è un bel aneddoto nella programmazione Pearl di Jon Bentley sull'ordinamento:

“La maggior parte delle discussioni su Quicksort utilizzano uno schema di partizionamento basato su due indici in avvicinamento [...] [ie Hoare's]. Sebbene l'idea di base di questo schema sia semplice, ho sempre trovato i dettagli difficili - una volta ho trascorso la maggior parte dei due giorni inseguendo un bug nascosto in un breve ciclo di partizionamento. Un lettore di una bozza preliminare si è lamentato del fatto che il metodo standard a due indici è in realtà più semplice di quello di Lomuto e ha abbozzato un po 'di codice per chiarire il punto; Ho smesso di occuparmi di aver trovato due bug. "

Dimensione delle prestazioni

Per un uso pratico, la facilità di implementazione potrebbe essere sacrificata per motivi di efficienza. Su base teorica, possiamo determinare il numero di confronti tra elementi e swap per confrontare le prestazioni. Inoltre, il tempo di esecuzione effettivo sarà influenzato da altri fattori, come le prestazioni di memorizzazione nella cache e le previsioni errate del ramo.

Come mostrato di seguito, gli algoritmi si comportano in modo molto simile su permutazioni casuali ad eccezione del numero di swap . Lomuto ha bisogno di tre volte più di Hoare!

Numero di confronti

n1n

Numero di swap

Il numero di swap è casuale per entrambi gli algoritmi, a seconda degli elementi dell'array. Se assumiamo permutazioni casuali , ovvero tutti gli elementi sono distinti e ogni permutazione degli elementi è ugualmente probabile, possiamo analizzare il numero atteso di swap.

1,,n

Metodo di Lomuto

jA[j]x1,,nx1xx1x

{1,,n}1n

1nx=1n(x1)=n212.

n

Metodo Hoare

x

ijxij

x

Hyp(n1,nx,x1)nxx1(nx)(x1)/(n1)x

Infine, eseguiamo nuovamente la media su tutti i valori pivot per ottenere il numero complessivo previsto di swap per il partizionamento di Hoare:

1nx=1n(nx)(x1)n1=n613.

(Una descrizione più dettagliata può essere trovata nella tesi del mio maestro , pagina 29.)

Pattern di accesso alla memoria

Entrambi gli algoritmi utilizzano due puntatori nell'array che lo scansionano in sequenza . Pertanto entrambi si comportano in modo quasi ottimale nella memorizzazione nella cache wrt.

Elementi uguali ed elenchi già ordinati

Come già accennato da Wandering Logic, le prestazioni degli algoritmi differiscono più drasticamente per gli elenchi che non sono permutazioni casuali.

n/2

0ijO(nlogn)

0A[j] <= xi=nΘ(n2)

Conclusione

Il metodo di Lomuto è semplice e facile da implementare, ma non dovrebbe essere utilizzato per implementare un metodo di ordinamento delle librerie.


16
Wow, questa è una risposta dettagliata. Ben fatto!
Raffaello

Sono d'accordo con Raffaello, davvero bella risposta!
Robert S. Barnes,

1
Vorrei fare un piccolo chiarimento, poiché man mano che il rapporto tra elementi unici e elementi totali si riduce, il numero di confronti che Lomuto fa aumenta significativamente più rapidamente di quelli di Hoare. Ciò è probabilmente dovuto al cattivo partizionamento da parte di Lomuto e al buon partizionamento medio da parte di Hoare.
Robert S. Barnes,

Grande spiegazione dei due metodi! Grazie!
v kouk,

Puoi facilmente creare una variante del metodo Lomuto in grado di estrarre tutti gli elementi uguali al perno e lasciarli fuori dalla ricorsione, anche se non sono sicuro che possa aiutare o ostacolare il caso medio.
Jakub Narębski,

5

Alcuni commenti aggiunti all'eccellente risposta di Sebastian.

Parlerò dell'algoritmo di riorganizzazione delle partizioni in generale e non del suo uso particolare per Quicksort .

Stabilità

L'algoritmo di Lomuto è semistabile : l'ordine relativo degli elementi che non soddisfano il predicato viene preservato. L'algoritmo di Hoare è instabile.

Pattern di accesso agli elementi

L'algoritmo di Lomuto può essere utilizzato con un elenco collegato singolarmente o strutture di dati forward-only simili. L'algoritmo di Hoare richiede bidirezionalità .

Numero di confronti

n1n

Ma per fare questo dobbiamo sacrificare 2 proprietà:

  1. La sequenza da partizionare non deve essere vuota.
  2. L'algoritmo non è in grado di restituire il punto di partizione.

n

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.