Mantenere un ordinamento efficiente in cui è possibile inserire elementi "in mezzo" tra due altri elementi nell'ordinamento?


8

Immagina di avere un ordine su un mucchio di elementi come questo:

inserisci qui la descrizione dell'immagine

Dove una freccia XY si intende X<Y. È anche transitivo:(X<Y)(Y<Z)(X<Z).

Per rispondere in modo efficiente a domande come A<?D, è richiesta una sorta di etichettatura o struttura dei dati. Ad esempio, potresti numerare i nodi da sinistra a destra, e quindi puoi semplicemente fare un confronto intero per rispondere alla query:A<?D1<4T. Sarebbe simile a questo:

inserisci qui la descrizione dell'immagine

Dove il numero è l'ordinamento e la lettera è solo un nome.

E se fosse necessario inserire elementi "tra" altri due elementi nell'ordine, in questo modo:

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine

Come puoi mantenere un tale ordine? Con la numerazione semplice, si verifica il problema che non ci sono numeri interi "in mezzo"2,3 usare.

Risposte:


7

Questo è noto come problema di "manutenzione dell'ordine" . C'è una soluzione relativamente semplice usandoO(1)tempo ammortizzato sia per le query che per gli inserti. Ora, per "relativamente semplice", intendo che devi capire alcuni elementi costitutivi, ma che una volta ottenuti quelli, il resto non è difficile da vedere.

http://courses.csail.mit.edu/6.851/spring12/lectures/L08.html

L'idea di base è una struttura di dati a due livelli. Il livello più alto è come la soluzione ad albero AVL di Realz Slaw, ma

  • I nodi sono etichettati direttamente con stringhe di bit di lunghezza O(lgn)con un ordine che corrisponde al loro ordine nell'albero. Il confronto richiede quindi tempo costante

  • Viene utilizzato un albero con meno rotazioni rispetto a un albero AVL, come un albero capro espiatorio o un albero equilibrato in base al peso, quindi le rietichettature avvengono meno frequentemente.

Il livello inferiore sono le foglie dell'albero. Quel livello utilizza la stessa lunghezza di etichette,O(lgn), ma vale solo O(lgn)elementi in ogni foglia in un semplice elenco collegato. Questo ti dà abbastanza bit extra per rietichettare in modo aggressivo.

Le foglie diventano troppo grandi o troppo piccole ogni O(lgn) inserisce, inducendo un cambiamento nel livello superiore, che richiede O(lgn) tempo ammortizzato (Ω(n)momento peggiore). Ammortizzato, questo è soloO(1).

Esistono strutture molto più complesse per l'esecuzione di aggiornamenti in O(1) momento peggiore.


7

Invece della semplice numerazione, è possibile distribuire i numeri su un intervallo ampio (di dimensioni costanti), come minimo intero e massimo di un intero CPU. Quindi puoi continuare a mettere i numeri "in mezzo" calcolando la media dei due numeri circostanti. Se i numeri diventano troppo affollati (ad esempio si finiscono con due numeri interi adiacenti e non c'è un numero in mezzo), è possibile effettuare una rinumerazione una tantum dell'intero ordine, ridistribuendo i numeri uniformemente nell'intervallo.

Ovviamente, puoi imbatterti nella limitazione che vengono utilizzati tutti i numeri all'interno dell'intervallo della costante grande. In primo luogo, questo non è di solito un problema, poiché la dimensione intera su una macchina è abbastanza grande in modo che se avessi più elementi probabilmente non rientrerebbe nella memoria. Ma se si tratta di un problema, puoi semplicemente rinumerarli con un intervallo intero più ampio.

Se l'ordine di input non è patologico, questo metodo potrebbe ammortizzare le rinumerazioni.

Rispondere alle domande

Un semplice confronto di numeri interi può rispondere alla query (X<?Y).

Il tempo di query sarebbe molto rapido ( O(1)) se si utilizzano numeri interi macchina, poiché si tratta di un semplice confronto di numeri interi. L'uso di un intervallo più ampio richiederebbe numeri più grandi e il confronto richiederebbeO(log|integer|).

Inserimento

Innanzitutto, manterrai l'elenco collegato dell'ordine, mostrato nella domanda. Inserimento qui, dati i nodi per posizionare il nuovo elemento in mezzo, sarebbeO(1).

L'etichettatura del nuovo elemento sarebbe in genere rapida O(1)perché calcoleresti facilmente il nuovo numero calcolando la media dei numeri circostanti. Occasionalmente potresti rimanere senza numeri "in mezzo", il che innescherebbe ilO(n) procedura di rinumerazione del tempo.

Evitare la rinumerazione

Puoi usare float invece di numeri interi, quindi quando ottieni due numeri interi "adiacenti", possono essere mediati. In questo modo puoi evitare di rinumerare di fronte a due float interi: basta dividerli a metà. Tuttavia, alla fine il tipo a virgola mobile avrà una precisione insufficiente e non sarà possibile calcolare la media di due galleggianti "adiacenti" (la media dei numeri circostanti sarà probabilmente uguale a uno dei numeri circostanti).

Allo stesso modo è possibile utilizzare un numero intero "decimale", in cui si mantengono due numeri interi per un elemento; uno per il numero e uno per il decimale. In questo modo, è possibile evitare la rinumerazione. Tuttavia, il numero intero decimale alla fine trabocca.

L'uso di un elenco di numeri interi o bit per ciascuna etichetta può evitare del tutto la rinumerazione; questo equivale sostanzialmente all'utilizzo di numeri decimali con lunghezza illimitata. Il confronto verrebbe effettuato in termini lessicografici e i tempi di confronto aumenteranno fino alla lunghezza degli elenchi interessati. Tuttavia, questo può sbilanciare l'etichettatura; alcune etichette potrebbero richiedere solo un numero intero (senza decimali), altre potrebbero avere un elenco di lunghezze (decimali lunghi). Questo è un problema, e la rinumerazione può aiutare anche qui, ridistribuendo la numerazione (qui elenchi di numeri) in modo uniforme su un intervallo scelto (intervallo qui che probabilmente significa lunghezza di elenchi) in modo che dopo tale rinumerazione, gli elenchi abbiano tutti la stessa lunghezza .


Questo metodo è effettivamente utilizzato in questo algoritmo ( implementazione , struttura dei dati rilevanti ); nel corso dell'algoritmo, è necessario mantenere un ordinamento arbitrario e l'autore utilizza numeri interi e rinumerazione per eseguire ciò.


Cercare di attenersi ai numeri rende lo spazio delle chiavi un po 'limitato. Si potrebbero invece usare stringhe a lunghezza variabile, usando la logica di confronto "a" <"ab" <"b". Rimangono ancora due problemi da risolvere A. Le chiavi potrebbero diventare arbitrariamente lunghe B. Il confronto delle chiavi lunghe potrebbe diventare costoso


3

È possibile mantenere un albero AVL senza chiave o simile.

Funzionerebbe come segue: L'albero mantiene un ordinamento sui nodi proprio come fa normalmente un albero AVL, ma invece della chiave che determina dove deve trovarsi il nodo, non ci sono chiavi ed è necessario inserire esplicitamente i nodi "dopo "un altro nodo (o in altre parole" tra "due nodi), dove" dopo "significa che viene dopo di esso nell'attraversamento in ordine dell'albero. L'albero manterrà così l'ordinamento per te in modo naturale e si bilancerebbe, grazie alle rotazioni integrate dell'AVL. Ciò manterrà tutto uniformemente distribuito automaticamente.

Inserimento

Oltre all'inserimento regolare nell'elenco, come dimostrato nella domanda, manterrai un albero AVL separato. L'inserimento nell'elenco stesso èO(1), poiché hai i nodi "prima" e "dopo".

Il tempo di inserimento nell'albero è O(logn), uguale all'inserimento in un albero AVL. L'inserimento implica avere un riferimento al nodo che si desidera inserire dopo, e si inserisce semplicemente il nuovo nodo nella parte sinistra del nodo più a sinistra del figlio destro; questa posizione è "successiva" nell'ordinamento dell'albero (è la successiva nell'attraversamento in ordine). Quindi eseguire le rotazioni AVL tipiche per riequilibrare l'albero. È possibile eseguire un'operazione simile per "inserire prima"; questo è utile quando è necessario inserire qualcosa all'inizio dell'elenco e non esiste un nodo "prima" del nodo.

Rispondere alle domande

Per rispondere alle domande di (X<?Y), trovi semplicemente tutti gli antenati di X e Ynell'albero e analizzi la posizione in cui divergono gli antenati; quello che diverge a "sinistra" è il minore dei due.

Questa procedura richiede O(logn)tempo di scalare l'albero fino alla radice e ottenere le liste degli antenati. Mentre è vero che questo sembra più lento del confronto dei numeri interi, la verità è che è la stessa; solo quel confronto di numeri interi su una CPU è limitato da una grande costante per renderloO(1); se si trabocca questa costante, è necessario mantenere più numeri interi (O(logn) numeri interi effettivamente) e fanno lo stesso O(logn)confronti. In alternativa, puoi "legare" l'altezza dell'albero di una quantità costante e "imbrogliare" come fa la macchina con gli interi: ora le query sembrerannoO(1).

Dimostrazione dell'operazione di inserimento

Per dimostrare, è possibile inserire alcuni elementi con il loro ordinamento dall'elenco nella domanda:

Passo 1

Iniziare con D

Elenco:

elencare il passaggio 1

Albero:

albero passo 1

Passo 2

Inserire C, <C<D.

Elenco:

elencare il passaggio 2

Albero:

albero passo 2

Nota, hai esplicitamente messo C "prima" D, non perché la lettera C sia prima di D, ma perché C<D nella lista.

Passaggio 3

Inserire A, <A<C.

Elenco:

elencare il passaggio 3

Albero:

passaggio 3 dell'albero prima della rotazione

Rotazione AVL:

passaggio 3 dell'albero dopo la rotazione

Passaggio 4

Inserire B, A<B<C.

Elenco:

elencare il passaggio 4

Albero:

passaggio dell'albero 4

Non sono necessarie rotazioni.

Passaggio 5

Inserire E, D<E<

Elenco:

elencare il passaggio 5

Albero:

passaggio albero 5

Passaggio 6

Inserire F, B<F<C

Lo abbiamo appena messo "dopo" B nell'albero, in questo caso semplicemente collegandolo a Bè giusto; cosìF è subito dopo B nell'attraversamento in ordine dell'albero.

Elenco:

elencare il passaggio 6

Albero:

albero passo 6 prima della rotazione

Rotazione AVL:

albero 6 dopo la rotazione

Dimostrazione dell'operazione di confronto

A<?F

ancestors(A) = [C,B]
ancestors(F) = [C,B]
last_common_ancestor = B
B.left = A
B.right = F
... A < F #left is less than right

D<?F

ancestors(D) = [C]
ancestors(F) = [C,B]
last_common_ancestor = C
C.left = D
C.right = B #next ancestor for F is to the right
... D < F #left is less than right

B<?A

ancestors(B) = [C]
ancestors(A) = [B,C]
last_common_ancestor = B
B.left = A
... A < B #left is always less than parent

Fonti grafiche


@saadtaame corretto e aggiunte fonti di file dot in basso. Grazie per averlo segnalato.
Realz Slaw,
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.