Perché Python usa la tabella hash per implementare dict, ma non Red-Black Tree? [chiuso]


11

Perché Python usa la tabella hash per implementare dict, ma non Red-Black Tree?

Qual è la chiave? Prestazione?


2
Condividere la tua ricerca aiuta tutti . Raccontaci cosa hai provato e perché non ha soddisfatto le tue esigenze. Ciò dimostra che hai impiegato del tempo per cercare di aiutarti, ci salva dal ribadire risposte ovvie e soprattutto ti aiuta a ottenere una risposta più specifica e pertinente. Vedi anche Come chiedere
moscerino

Risposte:


16

Questa è una risposta generale, non specifica per Python.

Confronto algoritmico della complessità

       | Hash Table  |   Red-Black Tree    |
-------+-------------+---------------------+
Space  | O(n) : O(n) | O(n)     : O(n)     |
Insert | O(1) : O(n) | O(log n) : O(log n) |
Fetch  | O(1) : O(n) | O(log n) : O(log n) |
Delete | O(1) : O(n) | O(log n) : O(log n) |
       | avg  :worst | average  : worst    |

Il problema con le tabelle hash è che gli hash possono scontrarsi. Esistono vari meccanismi per risolvere le collisioni, ad esempio indirizzamento aperto o concatenamento separato. Il caso peggiore è che tutte le chiavi hanno lo stesso codice hash, nel qual caso una tabella hash si degrada in un elenco collegato.

In tutti gli altri casi, una tabella hash è un'ottima struttura di dati che è facile da implementare e offre buone prestazioni. Un aspetto negativo è che le implementazioni che possono far crescere rapidamente la tabella e ridistribuire le loro voci probabilmente sprecheranno quasi tutta la memoria effettivamente utilizzata.

Gli alberi RB sono auto-bilanciati e non cambiano la loro complessità algoritmica nel peggiore dei casi. Tuttavia, sono più difficili da implementare. Le loro complessità medie sono anche peggiori di quelle di una tabella hash.

Restrizioni per le chiavi

Tutte le chiavi in ​​una tabella hash devono essere hash e comparabili per l'uguaglianza tra loro. Questo è particolarmente facile per stringhe o numeri interi, ma è anche abbastanza semplice estenderlo a tipi definiti dall'utente. In alcune lingue come Java queste proprietà sono garantite per definizione.

Le chiavi in ​​un RB-Tree devono avere un ordine totale: ogni chiave deve essere comparabile con qualsiasi altra chiave e le due chiavi devono confrontare più piccolo, maggiore o uguale. Questa uguaglianza di ordinamento deve essere equivalente all'uguaglianza semantica. Questo è semplice per numeri interi e altri numeri, anche abbastanza facile per le stringhe (l'ordine deve essere solo coerente e non osservabile esternamente, quindi l'ordine non deve considerare le localizzazioni [1] ), ma difficile per altri tipi che non hanno un ordine intrinseco . È assolutamente impossibile avere chiavi di tipi diversi a meno che non sia possibile un confronto tra loro.

[1]: In realtà, mi sbaglio qui. Due stringhe potrebbero non essere uguali a byte ma essere comunque equivalenti secondo le regole di alcune lingue. Vedere ad esempio le normalizzazioni Unicode per un esempio in cui due stringhe uguali sono codificate in modo diverso. Se la composizione dei caratteri Unicode è importante per la tua chiave hash è qualcosa che un'implementazione della tabella hash non può sapere.

Si potrebbe pensare che una soluzione economica per le chiavi RB-Tree sarebbe prima testare l'uguaglianza, quindi confrontare l'identità (cioè confrontare i puntatori). Tuttavia, questo ordinamento non sarebbe transitivo: se a == be id(a) > id(c), allora dovrebbe seguire anche quello id(b) > id(c), che non è garantito qui. Quindi, invece, potremmo usare il codice hash delle chiavi come chiavi di ricerca. Qui, l'ordinamento funziona correttamente, ma potremmo finire con più chiavi distinte con lo stesso codice hash, che verrà assegnato allo stesso nodo nella struttura RB. Per risolvere queste collisioni di hash possiamo usare il concatenamento separato proprio come con le tabelle di hash, ma questo eredita anche il comportamento peggiore per le tabelle di hash - il peggio di entrambi i mondi.

Altri aspetti

  • Mi aspetto che una tabella hash abbia una migliore posizione di memoria rispetto a un albero, perché una tabella hash è essenzialmente solo un array.

  • Le voci in entrambe le strutture dati hanno un sovraccarico abbastanza elevato:

    • tabella hash: chiave, valore e puntatore della voce successiva in caso di concatenamento separato. Anche la memorizzazione del codice hash può velocizzare il ridimensionamento.
    • Albero RB: chiave, valore, colore, puntatore figlio sinistro, puntatore figlio destro. Si noti che mentre il colore è un singolo bit, i problemi di allineamento potrebbero significare che si sprecherebbe spazio sufficiente per quasi un intero puntatore o anche quasi quattro puntatori quando è possibile allocare solo blocchi di memoria di dimensioni pari a due. In ogni caso, una voce dell'albero RB consuma più memoria di una voce della tabella hash.
  • Inserzioni ed eliminazioni in un albero RB comportano rotazioni dell'albero. Questi non sono molto costosi, ma comportano un sovraccarico. In un hash, l'inserimento e la cancellazione non sono più costosi di un semplice accesso (sebbene ridimensionare una tabella hash dopo l'inserimento sia uno O(n)sforzo).

  • Le tabelle hash sono intrinsecamente mutabili, mentre un albero RB potrebbe anche essere implementato in modo immutabile. Tuttavia, questo è raramente utile.


Possiamo avere una tabella hash con piccoli alberi RB per far collidere hash?
Aragaer,

@aragaer non in generale, ma sarebbe possibile in alcuni casi specifici. Tuttavia, le collisioni sono generalmente gestite da elenchi collegati: molto più facili da implementare, molto meno generali e di solito molto più performanti perché in genere abbiamo solo pochissime collisioni. Se prevediamo molte collisioni, potremmo cambiare la funzione hash o utilizzare un albero B più semplice. Gli alberi auto-bilanciati come gli alberi RB sono fantastici, ma ci sono molti casi in cui semplicemente non aggiungono valore.
amon

Gli alberi hanno bisogno di oggetti che supportano "<". Le tabelle hash richiedono oggetti che supportano hash + "=". Quindi gli alberi RB potrebbero non essere possibili. Ma davvero se la tua tabella hash ha una quantità significativa di collisioni, allora hai bisogno di una nuova funzione hash, non di un algoritmo alternativo per le chiavi di collisione.
gnasher729,

1

Ci sono tutta una serie di ragioni che potrebbero essere vere, ma è probabile che quelle chiave siano:

  • Le tabelle hash sono più facili da implementare rispetto agli alberi. Nessuno dei due è del tutto banale, ma le tabelle hash sono un po 'più facili e l'impatto sul dominio delle chiavi legali è meno rigoroso in quanto è sufficiente una funzione di hashing e una funzione di uguaglianza; gli alberi richiedono una funzione di ordine totale, ed è molto più difficile da scrivere.
  • Le tabelle hash (possono) avere prestazioni migliori a dimensioni ridotte. Ciò conta molto perché una parte significativa del lavoro si occupa solo teoricamente di grandi set di dati; in pratica, molto funziona con solo decine o centinaia di chiavi, non milioni. Le prestazioni su piccola scala contano molto e non è possibile utilizzare l'analisi asintotica per capire cosa è meglio lì; devi effettivamente implementare e misurare.

Più facile da scrivere / mantenere e un vincitore delle prestazioni in casi d'uso tipici? Registrami, per favore!

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.