Sono curioso di sapere se O (n log n) è il meglio che una lista collegata può fare.
Sono curioso di sapere se O (n log n) è il meglio che una lista collegata può fare.
Risposte:
È ragionevole aspettarsi che non si possa fare di meglio di O (N log N) nel tempo di esecuzione .
Tuttavia, la parte interessante è verificare se è possibile ordinarlo sul posto , in modo stabile , il suo comportamento nel peggiore dei casi e così via.
Simon Tatham, famoso per Putty, spiega come ordinare un elenco collegato con merge sort . Conclude con i seguenti commenti:
Come ogni algoritmo di ordinamento che si rispetti, questo ha un tempo di esecuzione O (N log N). Poiché questo è Mergesort, il tempo di esecuzione nel caso peggiore è ancora O (N log N); non ci sono casi patologici.
Il fabbisogno di stoccaggio ausiliario è piccolo e costante (cioè poche variabili all'interno della routine di smistamento). Grazie al comportamento intrinsecamente diverso degli elenchi collegati dagli array, questa implementazione di Mergesort evita il costo di archiviazione ausiliario O (N) normalmente associato all'algoritmo.
C'è anche un'implementazione di esempio in C che funziona sia per elenchi collegati singolarmente che doppiamente.
Come @ Jørgen Fogh menziona di seguito, la notazione O grande può nascondere alcuni fattori costanti che possono far funzionare meglio un algoritmo a causa della località della memoria, a causa di un numero basso di elementi, ecc.
listsort
, vedrai che puoi cambiare usando il parametro int is_double
.
listsort
codice C che supporta solo elenchi collegati singolarmente
A seconda di una serie di fattori, potrebbe essere effettivamente più veloce copiare l'elenco in un array e quindi utilizzare un Quicksort .
Il motivo per cui questo potrebbe essere più veloce è che un array ha prestazioni della cache molto migliori rispetto a un elenco collegato. Se i nodi nell'elenco sono dispersi nella memoria, potresti generare errori di cache dappertutto. Poi di nuovo, se l'array è grande, si otterranno comunque errori di cache.
Mergesort parallelizza meglio, quindi potrebbe essere una scelta migliore se è quello che vuoi. È anche molto più veloce se lo esegui direttamente nell'elenco collegato.
Poiché entrambi gli algoritmi vengono eseguiti in O (n * log n), prendere una decisione informata comporterebbe la profilazione di entrambi sulla macchina su cui si desidera eseguirli.
--- MODIFICARE
Ho deciso di testare la mia ipotesi e ho scritto un programma C che misurava il tempo (usando clock()
) impiegato per ordinare un elenco collegato di int. Ho provato con un elenco collegato in cui è stato allocato ogni nodomalloc()
e un elenco collegato in cui i nodi erano disposti linearmente in un array, quindi le prestazioni della cache sarebbero state migliori. Li ho confrontati con il qsort integrato, che includeva la copia di tutto da un elenco frammentato a un array e la copia di nuovo del risultato. Ogni algoritmo è stato eseguito sugli stessi 10 set di dati e i risultati sono stati mediati.
Questi sono i risultati:
N = 1000:
Elenco frammentato con merge sort: 0,000000 secondi
Array con qsort: 0,000000 secondi
Packed list con merge sort: 0,000000 secondi
N = 100000:
Elenco frammentato con merge sort: 0,039000 secondi
Array con qsort: 0,025000 secondi
Packed list con merge sort: 0,009000 secondi
N = 1000000:
Elenco frammentato con merge sort: 1.162000 secondi
Array con qsort: 0.420000 secondi
Packed list con merge sort: 0,112000 secondi
N = 100000000:
Elenco frammentato con merge sort: 364.797000 secondi
Array con qsort: 61.166000 secondi
Packed list con merge sort: 16.525000 secondi
Conclusione:
Almeno sulla mia macchina, vale la pena copiare in un array per migliorare le prestazioni della cache, dal momento che nella vita reale raramente si dispone di un elenco collegato completo. Va notato che la mia macchina ha un Phenom II da 2.8GHz, ma solo 0.6GHz di RAM, quindi la cache è molto importante.
Gli ordinamenti di confronto (ovvero quelli basati sul confronto di elementi) non possono essere più veloci di n log n
. Non importa quale sia la struttura dati sottostante. Vedi Wikipedia .
Altri tipi di ordinamento che sfruttano la presenza di molti elementi identici nell'elenco (come l'ordinamento conteggio), o alcune distribuzioni previste di elementi nell'elenco, sono più veloci, anche se non riesco a pensare a nessuno che funzioni particolarmente bene in un elenco collegato.
Questo è un bel paper su questo argomento. La sua conclusione empirica è che Treesort è il migliore, seguito da Quicksort e Mergesort. L'ordinamento dei sedimenti, l'ordinamento delle bolle, l'ordinamento della selezione hanno prestazioni molto scadenti.
UNO STUDIO COMPARATO DEGLI ALGORITMI DI ORDINAMENTO DELLE LISTE COLLEGATE di Ching-Kuang Shene
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.9981
Come affermato molte volte, il limite inferiore dell'ordinamento basato sul confronto per i dati generali sarà O (n log n). Per riassumere brevemente questi argomenti, ci sono n! diversi modi in cui un elenco può essere ordinato. Qualsiasi tipo di albero di confronto che abbia n! (che è in O (n ^ n)) eventuali ordinamenti finali richiederanno almeno log (n!) come altezza: questo ti dà un limite inferiore O (log (n ^ n)), che è O (n log n).
Quindi, per i dati generali su un elenco collegato, il miglior ordinamento possibile che funzionerà su tutti i dati che possono confrontare due oggetti sarà O (n log n). Tuttavia, se hai un dominio più limitato di cose su cui lavorare, puoi migliorare il tempo necessario (almeno proporzionale a n). Ad esempio, se stai lavorando con numeri interi non più grandi di un valore, puoi usare Counting Sort o Radix Sort , poiché questi usano gli oggetti specifici che stai ordinando per ridurre la complessità con proporzione a n. Fai attenzione, però, questi aggiungono alcune altre cose alla complessità che potresti non considerare (ad esempio, Counting Sort e Radix sort aggiungono entrambi fattori basati sulla dimensione dei numeri che stai ordinando, O (n + k ) dove k è la dimensione del numero più grande per l'ordinamento conteggio, ad esempio).
Inoltre, se ti capita di avere oggetti che hanno un hash perfetto (o almeno un hash che mappa tutti i valori in modo diverso), potresti provare a utilizzare un conteggio o un ordinamento digitale sulle loro funzioni hash.
Un ordinamento Radix è particolarmente adatto a una lista concatenata, poiché è facile creare una tabella di puntatori a testa corrispondente a ogni possibile valore di una cifra.
L'ordinamento di tipo merge non richiede l'accesso O (1) ed è O (n ln n). Nessun algoritmo noto per l'ordinamento dei dati generali è migliore di O (n ln n).
Gli algoritmi di dati speciali come l'ordinamento digitale (limita la dimensione dei dati) o l'ordinamento dell'istogramma (conta i dati discreti) potrebbero ordinare un elenco collegato con una funzione di crescita inferiore, purché si utilizzi una struttura diversa con accesso O (1) come memoria temporanea .
Un'altra classe di dati speciali è una sorta di confronto di una lista quasi ordinata con k elementi fuori ordine. Questo può essere ordinato in operazioni O (kn).
Copiare l'elenco in un array e viceversa sarebbe O (N), quindi qualsiasi algoritmo di ordinamento può essere utilizzato se lo spazio non è un problema.
Ad esempio, dato un elenco collegato contenente uint_8
, questo codice lo ordinerà in O (N) tempo utilizzando un ordinamento istogramma:
#include <stdio.h>
#include <stdint.h>
#include <malloc.h>
typedef struct _list list_t;
struct _list {
uint8_t value;
list_t *next;
};
list_t* sort_list ( list_t* list )
{
list_t* heads[257] = {0};
list_t* tails[257] = {0};
// O(N) loop
for ( list_t* it = list; it != 0; it = it -> next ) {
list_t* next = it -> next;
if ( heads[ it -> value ] == 0 ) {
heads[ it -> value ] = it;
} else {
tails[ it -> value ] -> next = it;
}
tails[ it -> value ] = it;
}
list_t* result = 0;
// constant time loop
for ( size_t i = 255; i-- > 0; ) {
if ( tails[i] ) {
tails[i] -> next = result;
result = heads[i];
}
}
return result;
}
list_t* make_list ( char* string )
{
list_t head;
for ( list_t* it = &head; *string; it = it -> next, ++string ) {
it -> next = malloc ( sizeof ( list_t ) );
it -> next -> value = ( uint8_t ) * string;
it -> next -> next = 0;
}
return head.next;
}
void free_list ( list_t* list )
{
for ( list_t* it = list; it != 0; ) {
list_t* next = it -> next;
free ( it );
it = next;
}
}
void print_list ( list_t* list )
{
printf ( "[ " );
if ( list ) {
printf ( "%c", list -> value );
for ( list_t* it = list -> next; it != 0; it = it -> next )
printf ( ", %c", it -> value );
}
printf ( " ]\n" );
}
int main ( int nargs, char** args )
{
list_t* list = make_list ( nargs > 1 ? args[1] : "wibble" );
print_list ( list );
list_t* sorted = sort_list ( list );
print_list ( sorted );
free_list ( list );
}
O(n lg n)
non sarebbe basato sul confronto (ad esempio, radix sort). Per definizione, l'ordinamento per confronto si applica a qualsiasi dominio che ha un ordine totale (cioè può essere confrontato).
Non è una risposta diretta alla tua domanda, ma se utilizzi un elenco da saltare , è già ordinato e ha un tempo di ricerca O (log N).
O(lg N)
tempo di ricerca previsto , ma non garantito, poiché gli elenchi da saltare si basano sulla casualità. Se ricevi input non attendibili, assicurati che il fornitore dell'input non sia in grado di prevedere il tuo RNG, altrimenti potrebbe inviarti dati che attivano le sue prestazioni nel caso peggiore
Come so, il miglior algoritmo di ordinamento è O (n * log n), qualunque sia il contenitore: è stato dimostrato che l'ordinamento nel senso ampio della parola (stile mergesort / quicksort ecc.) Non può essere inferiore. L'uso di un elenco collegato non ti darà un tempo di esecuzione migliore.
L'unico algoritmo che viene eseguito in O (n) è un algoritmo di "hacking" che si basa sul conteggio dei valori piuttosto che sull'ordinamento effettivo.
O(n lg c)
. Se tutti i tuoi elementi sono unici, allora c >= n
, e quindi ci vuole più tempo di O(n lg n)
.
Ecco un'implementazione che attraversa l'elenco solo una volta, raccogliendo le esecuzioni, quindi pianifica le unioni nello stesso modo in cui fa il mergesort.
La complessità è O (n log m) dove n è il numero di elementi em è il numero di esecuzioni. Il caso migliore è O (n) (se i dati sono già ordinati) e il caso peggiore è O (n log n) come previsto.
Richiede una memoria temporanea O (log m); l'ordinamento viene eseguito sul posto negli elenchi.
(aggiornato di seguito. Il commentatore uno fa un buon punto sul fatto che dovrei descriverlo qui)
L'essenza dell'algoritmo è:
while list not empty
accumulate a run from the start of the list
merge the run with a stack of merges that simulate mergesort's recursion
merge all remaining items on the stack
L'accumulo di discese non richiede molte spiegazioni, ma è bene cogliere l'occasione per accumulare sia discese che ascendenti (invertite). Qui antepone elementi più piccoli dell'inizio della sequenza e aggiunge elementi maggiori o uguali alla fine della sequenza. (Si noti che la preposizione dovrebbe utilizzare rigorosamente meno di per preservare la stabilità dell'ordinamento.)
È più semplice incollare qui il codice di unione:
int i = 0;
for ( ; i < stack.size(); ++i) {
if (!stack[i])
break;
run = merge(run, stack[i], comp);
stack[i] = nullptr;
}
if (i < stack.size()) {
stack[i] = run;
} else {
stack.push_back(run);
}
Considera l'idea di ordinare l'elenco (dagibecfjh) (ignorando le esecuzioni). Gli stati dello stack procedono come segue:
[ ]
[ (d) ]
[ () (a d) ]
[ (g), (a d) ]
[ () () (a d g i) ]
[ (b) () (a d g i) ]
[ () (b e) (a d g i) ]
[ (c) (b e) (a d g i ) ]
[ () () () (a b c d e f g i) ]
[ (j) () () (a b c d e f g i) ]
[ () (h j) () (a b c d e f g i) ]
Quindi, finalmente, unisci tutti questi elenchi.
Notare che il numero di elementi (esecuzioni) nello stack [i] è zero o 2 ^ i e la dimensione dello stack è limitata da 1 + log2 (monache). Ogni elemento viene unito una volta per livello di stack, quindi confronti O (n log m). C'è una somiglianza passeggera con Timsort qui, anche se Timsort mantiene il suo stack usando qualcosa come una sequenza di Fibonacci in cui usa poteri di due.
L'accumulo di corse sfrutta tutti i dati già ordinati in modo che la complessità del caso migliore sia O (n) per un elenco già ordinato (una corsa). Dal momento che stiamo accumulando sia le sequenze ascendenti che quelle discendenti, le sequenze saranno sempre almeno di lunghezza 2. (Ciò riduce la profondità massima dello stack di almeno uno, pagando il costo di trovare le piste in primo luogo). O (n log n), come previsto, per dati altamente randomizzati.
(Um ... Secondo aggiornamento.)
O guarda semplicemente wikipedia sul mergesort dal basso verso l'alto .
O(log m)
non dovrebbe essere necessaria memoria aggiuntiva: è sufficiente aggiungere le corse a due elenchi alternativamente finché uno non è vuoto.
Puoi copiarlo in un array e quindi ordinarlo.
Copia nell'array O (n),
ordinamento O (nlgn) (se usi un algoritmo veloce come merge sort),
copia di nuovo alla lista collegata O (n) se necessario,
quindi sarà O (nlgn).
nota che se non conosci il numero di elementi nell'elenco collegato non conoscerai la dimensione dell'array. Se stai codificando in java puoi usare un Arraylist per esempio.
Mergesort è il meglio che puoi fare qui.
La domanda è LeetCode # 148 e ci sono molte soluzioni offerte in tutte le principali lingue. Il mio è il seguente, ma mi chiedo quale sia la complessità temporale. Per trovare l'elemento centrale, esaminiamo ogni volta l'elenco completo. La prima volta che gli n
elementi vengono ripetuti, la seconda volta gli 2 * n/2
elementi vengono ripetuti, così via e così via. Sembra che sia O(n^2)
arrivato il momento.
def sort(linked_list: LinkedList[int]) -> LinkedList[int]:
# Return n // 2 element
def middle(head: LinkedList[int]) -> LinkedList[int]:
if not head or not head.next:
return head
slow = head
fast = head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
def merge(head1: LinkedList[int], head2: LinkedList[int]) -> LinkedList[int]:
p1 = head1
p2 = head2
prev = head = None
while p1 and p2:
smaller = p1 if p1.val < p2.val else p2
if not head:
head = smaller
if prev:
prev.next = smaller
prev = smaller
if smaller == p1:
p1 = p1.next
else:
p2 = p2.next
if prev:
prev.next = p1 or p2
else:
head = p1 or p2
return head
def merge_sort(head: LinkedList[int]) -> LinkedList[int]:
if head and head.next:
mid = middle(head)
mid_next = mid.next
# Makes it easier to stop
mid.next = None
return merge(merge_sort(head), merge_sort(mid_next))
else:
return head
return merge_sort(linked_list)