Algoritmi di divisione e conquista: perché non dividere in più parti di due?


33

Negli algoritmi di divisione e conquista come quicksort e mergesort, l'input è di solito (almeno nei testi introduttivi) diviso in due e i due set di dati più piccoli vengono quindi trattati in modo ricorsivo. Per me ha senso che ciò renda più veloce la risoluzione di un problema se le due metà impiegano meno della metà del lavoro di gestione dell'intero set di dati. Ma perché non dividere il set di dati in tre parti? Quattro? n ?

Immagino che il lavoro di dividere i dati in molti, molti sottoinsiemi non valga la pena, ma mi manca l'intuizione di vedere che uno dovrebbe fermarsi a due sottoinsiemi.

Ho anche visto molti riferimenti a quicksort a 3 vie. Quando è più veloce? Cosa viene utilizzato in pratica?


Prova a creare un algoritmo simile a quicksort che divide un array in tre parti.
gnasher729,

Risposte:


49

Per me ha senso che ciò renda più veloce la risoluzione di un problema se le due metà impiegano meno della metà del lavoro di gestione dell'intero set di dati.

Questa non è l'essenza degli algoritmi di divisione e conquista. Di solito il punto è che gli algoritmi non possono "gestire l'intero set di dati". Invece, è diviso in pezzi che sono banali da risolvere (come l'ordinamento di due numeri), quindi quelli vengono risolti in modo banale e i risultati vengono ricombinati in un modo che fornisce una soluzione per l'intero set di dati.

Ma perché non dividere il set di dati in tre parti? Quattro? n?

Principalmente perché suddividendolo in più di due parti e ricombinando più di due risultati si ottiene un'implementazione più complessa ma non cambia la caratteristica fondamentale (Big O) dell'algoritmo: la differenza è un fattore costante e può causare un rallentamento se la divisione e la ricombinazione di più di 2 sottoinsiemi creano un sovraccarico aggiuntivo.

Ad esempio, se esegui un ordinamento di unione a 3 vie, nella fase di ricombinazione ora devi trovare il più grande dei 3 elementi per ogni elemento, che richiede 2 confronti anziché 1, quindi eseguirai il doppio di tutti i confronti . In cambio, riduci la profondità di ricorsione di un fattore di ln (2) / ln (3) == 0,63, quindi hai il 37% in meno di scambi, ma 2 * 0,63 == 26% in più di confronti (e accessi alla memoria). Che sia buono o cattivo dipende da quale è più costoso nel tuo hardware.

Ho anche visto molti riferimenti a quicksort a 3 vie. Quando è più veloce?

Apparentemente è possibile dimostrare che una variante a doppio perno di quicksort richiede lo stesso numero di confronti ma in media il 20% in meno di swap, quindi è un guadagno netto.

Cosa viene utilizzato in pratica?

Oggi quasi nessuno programma più i propri algoritmi di ordinamento; ne usano uno fornito da una biblioteca. Ad esempio, l' API Java 7 utilizza effettivamente il quicksort dual-pivot.

Le persone che programmano effettivamente il proprio algoritmo di ordinamento per qualche motivo tenderanno ad attenersi alla semplice variante a 2 vie perché un minor potenziale di errori supera il 20% delle prestazioni migliori per la maggior parte del tempo. Ricorda: il miglioramento delle prestazioni di gran lunga più importante è quando il codice passa da "non funzionante" a "funzionante".


1
Piccola nota: Java 7 utilizza Quicksort Dual-Pivot solo durante l'ordinamento delle primitive. Per ordinare gli oggetti utilizza Timsort.
Bakuriu,

1
+1 per "Oggigiorno quasi nessuno programma più i propri algoritmi di ordinamento" e (cosa più importante) "Ricorda: il miglioramento delle prestazioni di gran lunga più importante è quando il codice passa da" non funziona "a" funzionante "." Tuttavia, vorrei sapere se quell'overhead è ancora banale se, ad esempio, si divide il set di dati in molte, molte parti. Come succede così, in modo da avere altre persone: bealto.com/gpu-sorting_intro.html stackoverflow.com/questions/1415679/... devgurus.amd.com/thread/157159
AndrewJacksonZA

Sono un po 'lento. Qualcuno potrebbe spiegare perché ci vogliono 2 * 0,69 più confronti? Non sono sicuro di dove sia arrivato lo 0.69.
jeebface,

@jeebface oops, era un refuso (ora risolto). È 0,63 (la riduzione della profondità di ricorsione), quindi risulta anche il risultato del 26% in più.
Michael Borgwardt,

30

Parlando asintoticamente, non importa. Ad esempio, la ricerca binaria effettua approssimativamente i  confronti di log 2 n e la ricerca ternaria fa approssimativamente i  confronti di log 3 n. Se conosci i tuoi logaritmi, sai che log a  x = log b  x / log b  a, quindi la ricerca binaria fa solo circa 1 / log 3 2 ≈ 1,5 volte più confronti rispetto alla ricerca ternaria. Questo è anche il motivo per cui nessuno specifica mai la base del logaritmo in grande notazione Oh: è sempre un fattore costante lontano dal logaritmo in una data base, qualunque sia la base effettiva. Quindi suddividere il problema in più sottoinsiemi non migliora la complessità del tempo e praticamente non è sufficiente per superare la logica più complessa. In effetti, tale complessità può influire negativamente sulle prestazioni pratiche, aumentando la pressione della cache o rendendo meno fattibili le microottimizzazioni.

D'altra parte, alcune strutture di dati ad albero usano un alto fattore di ramificazione (molto più grande di 3, spesso 32 o più), sebbene di solito per altri motivi. Migliora l'utilizzo della gerarchia di memoria: le strutture di dati archiviate nella RAM sfruttano meglio la cache, le strutture di dati archiviate sul disco richiedono meno letture HDD-> RAM.


Sì, cercare l'otto per un'applicazione specifica di una struttura ad albero più che binaria.
daaxix,

@daaxix btree è probabilmente più comune.
Jules,

4

Esistono algoritmi di ricerca / ordinamento che suddividono non per due, ma per N.

Un semplice esempio è la ricerca per codice hash, che richiede tempo O (1).

Se la funzione hash mantiene l'ordine, può essere utilizzata per creare un algoritmo di ordinamento O (N). (Puoi pensare a qualsiasi algoritmo di ordinamento come semplicemente facendo N ricerche per dove un numero dovrebbe andare nel risultato.)

Il problema fondamentale è che, quando un programma esamina alcuni dati e poi inserisce alcuni stati seguenti, quanti stati seguenti ci sono e quanto vicine sono le loro probabilità?

Quando un computer fa un confronto di due numeri, diciamo, e poi salta o no, se entrambi i percorsi sono ugualmente probabili, il contatore del programma "conosce" un altro bit di informazione su ciascun percorso, quindi in media ne ha "imparato" uno po. Se un problema richiede l'apprendimento di bit M, l'utilizzo di decisioni binarie non può ottenere la risposta in meno di decisioni M. Quindi, per esempio, cercare un numero in una tabella ordinata di dimensioni 1024 non può essere fatto in meno di 10 decisioni binarie, anche solo perché un numero minore non avrebbe risultati sufficienti, ma può certamente essere fatto in più.

Quando un computer prende un numero e lo trasforma in un indice in un array, "apprende" fino a registrare la base 2 del numero di elementi nell'array e lo fa in tempo costante. Ad esempio, se esiste una tabella di salto di 1024 voci, tutte più o meno ugualmente probabili, quindi saltare attraverso quella tabella "impara" 10 bit. Questo è il trucco fondamentale dietro la codifica hash. Un esempio di ordinamento di questo è come è possibile ordinare un mazzo di carte. Hai 52 scomparti, uno per ogni carta. Lancia ogni carta nel suo cestino, quindi raccoglile tutte. Nessuna suddivisione richiesta.


1

Poiché questa è una domanda sulla divisione e la conquista generali, non solo sull'ordinamento, sono sorpreso che nessuno abbia sollevato il Teorema del Maestro

In breve, il tempo di esecuzione degli algoritmi di divisione e conquista è determinato da due forze controrivolanti: il vantaggio che si ottiene dalla trasformazione di problemi più grandi in piccoli problemi e il prezzo che si paga nel dover risolvere più problemi. A seconda dei dettagli dell'algoritmo, può o meno pagare dividere un problema in più di due pezzi. Se ti dividi nello stesso numero di sottoproblemi ad ogni passo e conosci la complessità temporale della combinazione dei risultati ad ogni passo, il Teorema Master ti dirà la complessità temporale dell'algoritmo complessivo.

L' algoritmo Karatsuba per la moltiplicazione utilizza una divisione e conquista a 3 vie per ottenere un tempo di esecuzione di O (3 n ^ log_2 3) che batte O (n ^ 2) per l'algoritmo di moltiplicazione ordinario (n è il numero di cifre nella numeri).


Nel teorema del Maestro, il numero di sotto-problemi che crei non è l'unico fattore. In Karatsuba e suo cugino Strassen, il miglioramento deriva effettivamente dalla fusione intelligente di soluzioni di alcuni dei sotto-problemi, in modo da ridurre il numero di chiamate ricorsive sui sotto-problemi. In breve, il bteorema del master in salita richiede ache tu salga più lentamente per avere un miglioramento nell'ulteriore divisione.
Informato il

-4

A causa della sua natura binaria, un computer è molto efficiente nel dividere le cose in 2 e non tanto in 3. Si ottiene una divisione in 3 dividendo prima in 2 e quindi dividere nuovamente una delle parti in 2. Quindi, se è necessario dividere per 2 per ottenere la tua divisione 3, potresti anche dividere in 2.

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.