Ordinamento Radix sul posto


200

Questo è un lungo testo. Per favore abbi pazienza. In poche parole, la domanda è: esiste un algoritmo di ordinamento radix sul posto praticabile ?


Preliminare

Ho un numero enorme di piccole stringhe a lunghezza fissa che usano solo le lettere “A”, “C”, “G” e “T” (sì, l'hai indovinato: DNA ) che voglio ordinare.

Al momento, io uso std::sortche utilizza introsort in tutte le implementazioni comuni del STL . Funziona abbastanza bene. Tuttavia, sono convinto che l' ordinamento radix si adatta perfettamente al mio set di problemi e dovrebbe funzionare molto meglio nella pratica.

Dettagli

Ho testato questo presupposto con un'implementazione molto ingenua e per input relativamente piccoli (dell'ordine di 10.000) questo era vero (beh, almeno più del doppio della velocità). Tuttavia, il tempo di esecuzione si riduce in modo abissale quando la dimensione del problema aumenta ( N > 5.000.000).

Il motivo è ovvio: radix sort richiede la copia di tutti i dati (più di una volta nella mia ingenua implementazione, in realtà). Ciò significa che ho inserito ~ 4 GiB nella mia memoria principale, il che ovviamente uccide le prestazioni. Anche se non fosse così, non posso permettermi di usare così tanta memoria poiché le dimensioni del problema in realtà diventano ancora più grandi.

Casi d'uso

Idealmente, questo algoritmo dovrebbe funzionare con qualsiasi lunghezza di stringa compresa tra 2 e 100, sia per il DNA che per il DNA5 (che consente un carattere jolly aggiuntivo "N"), o persino il DNA con codici di ambiguità IUPAC (risultanti in 16 valori distinti). Tuttavia, mi rendo conto che tutti questi casi non possono essere coperti, quindi sono contento di qualsiasi miglioramento della velocità che ottengo. Il codice può decidere dinamicamente a quale algoritmo inviare.

Ricerca

Sfortunatamente, l' articolo di Wikipedia sull'ordinamento di Radix è inutile. La sezione relativa a una variante sul posto è spazzatura completa. La sezione NIST-DADS sull'ordinamento radix è accanto a inesistente. C'è un articolo dal suono promettente chiamato Efficient Adaptive In-Place Radix Sorting che descrive l'algoritmo "MSL". Sfortunatamente, anche questo documento è deludente.

In particolare, ci sono le seguenti cose.

Innanzitutto, l'algoritmo contiene diversi errori e lascia molto inspiegabile. In particolare, non dettaglia la chiamata di ricorsione (presumo semplicemente che aumenti o riduca alcuni puntatori per calcolare i valori di spostamento e maschera correnti). Inoltre, utilizza le funzioni dest_groupe dest_addresssenza fornire definizioni. Non riesco a vedere come implementarli in modo efficiente (cioè in O (1); almeno dest_addressnon è banale).

Ultimo ma non meno importante, l'algoritmo raggiunge la posizione sul posto scambiando gli indici di array con elementi all'interno dell'array di input. Questo ovviamente funziona solo su array numerici. Ho bisogno di usarlo sulle stringhe. Certo, potrei semplicemente rovinare la digitazione forte e andare avanti supponendo che la memoria tollererà la mia memorizzazione di un indice a cui non appartiene. Ma questo funziona solo finché riesco a comprimere le mie stringhe in 32 bit di memoria (assumendo numeri interi a 32 bit). Sono solo 16 caratteri (ignoriamo per il momento quel registro 16> (5.000.000)).

Un altro articolo di uno degli autori non fornisce alcuna descrizione accurata, ma fornisce l'autonomia di MSL come sub-lineare, il che è assolutamente sbagliato.

Ricapitolando : c'è qualche speranza di trovare un'implementazione di riferimento funzionante o almeno un buon pseudocodice / descrizione di un ordinamento radix sul posto funzionante che funzioni su stringhe di DNA?


65
Questa è una domanda scritta in modo eccellente.
Giusto il

1
quanto sono piccole le stringhe di lunghezza fissa piccola?
EvilTeach

1
@EvilTeach: ho aggiunto i casi d'uso.
Konrad Rudolph,

2
@Stephan: va tutto bene. Ma in caso di mancata copia / cache ho solo un ritardo. Nel caso della memoria ho raggiunto un limite fisico. Questo è semplicemente non negoziabile. Tutte quelle fantasiose tecniche per archiviare parti dei dati su disco sono decisamente più lente dell'attuale soluzione quicksort.
Konrad Rudolph,

2
(cont ') La soluzione di dsimcha, d'altra parte, è decisamente più veloce di quicksort per alcuni input. Il numero di mosse può essere elevato e la cache di piccole dimensioni, ma nel mondo reale è comunque buona. Ho anche modificato leggermente la soluzione per ridurre il numero di swap che devo eseguire.
Konrad Rudolph,

Risposte:


61

Bene, ecco una semplice implementazione di una sorta di radix MSD per il DNA. È scritto in D perché è la lingua che uso di più e quindi è meno probabile che commetta errori sciocchi, ma potrebbe facilmente essere tradotto in un'altra lingua. È installato ma richiede 2 * seq.lengthpassaggi attraverso l'array.

void radixSort(string[] seqs, size_t base = 0) {
    if(seqs.length == 0)
        return;

    size_t TPos = seqs.length, APos = 0;
    size_t i = 0;
    while(i < TPos) {
        if(seqs[i][base] == 'A') {
             swap(seqs[i], seqs[APos++]);
             i++;
        }
        else if(seqs[i][base] == 'T') {
            swap(seqs[i], seqs[--TPos]);
        } else i++;
    }

    i = APos;
    size_t CPos = APos;
    while(i < TPos) {
        if(seqs[i][base] == 'C') {
            swap(seqs[i], seqs[CPos++]);
        }
        i++;
    }
    if(base < seqs[0].length - 1) {
        radixSort(seqs[0..APos], base + 1);
        radixSort(seqs[APos..CPos], base + 1);
        radixSort(seqs[CPos..TPos], base + 1);
        radixSort(seqs[TPos..seqs.length], base + 1);
   }
}

Ovviamente, questo è un po 'specifico per il DNA, invece di essere generale, ma dovrebbe essere veloce.

Modificare:

Mi sono incuriosito se questo codice funziona davvero, quindi l'ho testato / debug mentre aspettavo che il mio codice bioinformatico fosse eseguito. La versione sopra ora è attualmente testata e funziona. Per 10 milioni di sequenze di 5 basi ciascuna, è circa 3 volte più veloce di un introsort ottimizzato.


9
Se riesci a convivere con un approccio a 2 passaggi, questo si estende a radix-N: passa 1 = basta attraversare e contare quante sono le cifre di ciascuna delle N cifre. Quindi se stai partizionando l'array, questo ti dice da dove inizia ogni cifra. Il passaggio 2 esegue lo swap nella posizione appropriata dell'array.
Jason S,

(ad esempio per N = 4, se ci sono 90000 A, 80000 G, 100 C, 100000 T, quindi creare un array inizializzato con le somme cumulative = [0, 90000, 170000, 170100] che viene utilizzato al posto dei tuoi APOS, CPos, ecc. Come cursore per il punto in cui scambiare l'elemento successivo per ogni cifra.)
Jason S,

Non sono sicuro di quale sarà la relazione tra la rappresentazione binaria e questa rappresentazione di stringhe, a parte l'utilizzo di almeno 4 volte la memoria necessaria
Stephan Eggermont,

Come è la velocità con sequenze più lunghe? Non ne hai abbastanza di diversi con una lunghezza di 5
Stephan Eggermont,

4
Questo ordinamento radix sembra essere un caso speciale dell'ordinamento American Flag, una variante ben nota di ordinamento radix sul posto.
Edward KMETT,

21

Non ho mai visto un ordinamento radix sul posto, e dalla natura dell'ordinamento radix dubito che sia molto più veloce di un ordinamento fuori posto fintanto che l'array temporaneo si adatta alla memoria.

Motivo:

L'ordinamento esegue una lettura lineare sull'array di input, ma tutte le scritture saranno quasi casuali. Da un certo N in poi questo si riduce a una cache mancata per scrittura. Questa mancata cache è ciò che rallenta il tuo algoritmo. Se è a posto o no non cambierà questo effetto.

So che questo non risponderà direttamente alla tua domanda, ma se l'ordinamento è un collo di bottiglia potresti voler dare un'occhiata agli algoritmi di ordinamento vicini come fase di preelaborazione (la pagina wiki sull'heap soft potrebbe iniziare).

Ciò potrebbe dare una bella spinta alla localizzazione della cache. Un ordinamento radix fuori posto da manuale funzionerà quindi meglio. Le scritture saranno ancora quasi casuali ma almeno si raggrupperanno attorno agli stessi blocchi di memoria e di conseguenza aumenteranno il rapporto di hit della cache.

Non ho idea se funzionerà nella pratica però.

Btw: se hai a che fare solo con stringhe di DNA: puoi comprimere un carattere in due bit e comprimere parecchio i tuoi dati. Ciò ridurrà il fabbisogno di memoria del fattore quattro rispetto a una rappresentazione naiiva. L'indirizzamento diventa più complesso, ma l'ALU della tua CPU ha comunque molto tempo da spendere durante tutti i mancati cache.


2
Due buoni punti; lo smistamento vicino è un nuovo concetto per me, dovrò leggerlo. Misses cache è un'altra considerazione che tormenta i miei sogni. ;-) Dovrò vedere questo.
Konrad Rudolph,

È nuovo anche per me (un paio di mesi), ma una volta capito il concetto inizi a vedere opportunità di miglioramento delle prestazioni.
Nils Pipenbrinck,

Le scritture sono tutt'altro che quasi casuali a meno che la tua radice non sia molto grande. Ad esempio, supponendo che ordiniate un carattere alla volta (un ordinamento radix-4) tutte le scritture saranno su uno dei 4 bucket a crescita lineare. Questo è sia cache che prefetch amichevole. Ovviamente, potresti voler usare un radix più grande, e ad un certo puntatore trovi un compromesso tra cache e prefetch cordialità e dimensioni del radix. Puoi spingere il punto di pareggio verso radici più grandi usando il prefetching del software o un'area di scratch per i tuoi secchi con un lavaggio periodico ai secchi "reali".
BeeOnRope,

8

Puoi sicuramente eliminare i requisiti di memoria codificando la sequenza in bit. Stai osservando le permutazioni quindi, per la lunghezza 2, con "ACGT" che è 16 stati o 4 bit. Per la lunghezza 3, sono 64 stati, che possono essere codificati in 6 bit. Quindi sembrano 2 bit per ogni lettera nella sequenza, o circa 32 bit per 16 caratteri come hai detto.

Se esiste un modo per ridurre il numero di "parole" valide, potrebbe essere possibile un'ulteriore compressione.

Quindi, per sequenze di lunghezza 3, si potrebbero creare 64 bucket, magari di dimensione uint32 o uint64. Inizializzali a zero. Scorri il tuo elenco molto ampio di 3 sequenze di caratteri e codificali come sopra. Usa questo come un pedice e incrementa quel bucket.
Ripeti fino a quando tutte le tue sequenze non sono state elaborate.

Quindi, rigenerare l'elenco.

Scorrere i 64 bucket in ordine, per il conteggio trovato in quel bucket, generare così tante istanze della sequenza rappresentata da quel bucket.
quando tutti i bucket sono stati ripetuti, hai il tuo array ordinato.

Una sequenza di 4, aggiunge 2 bit, quindi ci sarebbero 256 bucket. Una sequenza di 5, aggiunge 2 bit, quindi ci sarebbero 1024 bucket.

Ad un certo punto il numero di secchi si avvicinerà ai tuoi limiti. Se leggi le sequenze da un file, invece di tenerle in memoria, sarebbe disponibile più memoria per i bucket.

Penso che questo sarebbe più veloce che fare l'ordinamento in situ poiché è probabile che i secchi si adattino al tuo set di lavoro.

Ecco un trucco che mostra la tecnica

#include <iostream>
#include <iomanip>

#include <math.h>

using namespace std;

const int width = 3;
const int bucketCount = exp(width * log(4)) + 1;
      int *bucket = NULL;

const char charMap[4] = {'A', 'C', 'G', 'T'};

void setup
(
    void
)
{
    bucket = new int[bucketCount];
    memset(bucket, '\0', bucketCount * sizeof(bucket[0]));
}

void teardown
(
    void
)
{
    delete[] bucket;
}

void show
(
    int encoded
)
{
    int z;
    int y;
    int j;
    for (z = width - 1; z >= 0; z--)
    {
        int n = 1;
        for (y = 0; y < z; y++)
            n *= 4;

        j = encoded % n;
        encoded -= j;
        encoded /= n;
        cout << charMap[encoded];
        encoded = j;
    }

    cout << endl;
}

int main(void)
{
    // Sort this sequence
    const char *testSequence = "CAGCCCAAAGGGTTTAGACTTGGTGCGCAGCAGTTAAGATTGTTT";

    size_t testSequenceLength = strlen(testSequence);

    setup();


    // load the sequences into the buckets
    size_t z;
    for (z = 0; z < testSequenceLength; z += width)
    {
        int encoding = 0;

        size_t y;
        for (y = 0; y < width; y++)
        {
            encoding *= 4;

            switch (*(testSequence + z + y))
            {
                case 'A' : encoding += 0; break;
                case 'C' : encoding += 1; break;
                case 'G' : encoding += 2; break;
                case 'T' : encoding += 3; break;
                default  : abort();
            };
        }

        bucket[encoding]++;
    }

    /* show the sorted sequences */ 
    for (z = 0; z < bucketCount; z++)
    {
        while (bucket[z] > 0)
        {
            show(z);
            bucket[z]--;
        }
    }

    teardown();

    return 0;
}

Perché confrontare quando puoi hash eh?
wowest

1
Dannatamente dritto. Le prestazioni sono generalmente un problema con qualsiasi elaborazione del DNA.
EvilTeach

6

Se il tuo set di dati è così grande, penso che un approccio buffer basato su disco sarebbe il migliore:

sort(List<string> elements, int prefix)
    if (elements.Count < THRESHOLD)
         return InMemoryRadixSort(elements, prefix)
    else
         return DiskBackedRadixSort(elements, prefix)

DiskBackedRadixSort(elements, prefix)
    DiskBackedBuffer<string>[] buckets
    foreach (element in elements)
        buckets[element.MSB(prefix)].Add(element);

    List<string> ret
    foreach (bucket in buckets)
        ret.Add(sort(bucket, prefix + 1))

    return ret

Sperimenterei anche il raggruppamento in un numero maggiore di bucket, ad esempio, se la stringa fosse:

GATTACA

la prima chiamata MSB restituisce il bucket per GATT (256 bucket totali), in questo modo si creano meno rami del buffer basato su disco. Questo può o meno migliorare le prestazioni, quindi sperimentale.


Usiamo file mappati in memoria per alcune applicazioni. Tuttavia, in generale lavoriamo supponendo che la macchina fornisca appena la RAM sufficiente per non richiedere il backup esplicito del disco (ovviamente, lo scambio ha ancora luogo). Ma stiamo già sviluppando un meccanismo per array automatici supportati da disco
Konrad Rudolph,

6

Ho intenzione di uscire su un arto e suggerire di passare a un'implementazione heap / heapsort . Questo suggerimento viene fornito con alcuni presupposti:

  1. Tu controlli la lettura dei dati
  2. Puoi fare qualcosa di significativo con i dati ordinati non appena 'inizi' a ordinarli.

Il bello dell'heap / heap-sort è che puoi creare l'heap mentre leggi i dati e puoi iniziare a ottenere risultati nel momento in cui hai creato l'heap.

Facciamo un passo indietro. Se sei così fortunato da poter leggere i dati in modo asincrono (vale a dire, puoi pubblicare qualche tipo di richiesta di lettura ed essere avvisato quando alcuni dati sono pronti), quindi puoi creare un pezzo di heap mentre aspetti il prossimo pezzo di dati in arrivo - anche dal disco. Spesso, questo approccio può seppellire la maggior parte dei costi di metà dell'ordinamento dietro il tempo impiegato per ottenere i dati.

Dopo aver letto i dati, il primo elemento è già disponibile. A seconda di dove stai inviando i dati, questo può essere fantastico. Se lo si sta inviando a un altro lettore asincrono, o un modello parallelo di 'evento' o UI, è possibile inviare blocchi e blocchi mentre si procede.

Detto questo - se non hai alcun controllo sul modo in cui i dati vengono letti e vengono letti in modo sincrono e non hai alcun uso per i dati ordinati fino a quando non vengono completamente scritti - ignora tutto questo. :(

Vedi gli articoli di Wikipedia:


1
Buon consiglio Tuttavia, l'ho già provato e nel mio caso particolare l'overhead di mantenere un heap è più grande del semplice accumulare i dati in un vettore e ordinarli una volta arrivati ​​tutti i dati.
Konrad Rudolph,


4

Per quanto riguarda le prestazioni, potresti voler esaminare algoritmi di ordinamento di confronto delle stringhe più generali.

Attualmente finisci per toccare ogni elemento di ogni stringa, ma puoi fare di meglio!

In particolare, una sorta di raffica è molto adatta per questo caso. Come bonus, dal momento che burstsort si basa sui tentativi, funziona ridicolmente bene per le piccole dimensioni dell'alfabeto utilizzate nel DNA / RNA, poiché non è necessario creare alcun tipo di nodo di ricerca ternario, hash o altro schema di compressione del nodo trie nel trie implementazione. I tentativi possono essere utili anche per il tuo obiettivo finale simile a un array di suffissi.

Un'implementazione decente di burstsort per scopi generici è disponibile su forge di origine su http://sourceforge.net/projects/burstsort/ - ma non è disponibile.

A fini di confronto, l'implementazione di C-burstsort è stata trattata su http://www.cs.mu.oz.au/~rsinha/papers/SinhaRingZobel-2006.pdf benchmark 4-5 volte più veloce di quicksort e ordinamenti radix per alcuni carichi di lavoro tipici.


Dovrò sicuramente esaminare il tipo di burst, anche se al momento non vedo come il trie possa essere costruito sul posto. In generale, le matrici di suffissi hanno quasi completamente sostituito gli alberi di suffisso (e quindi i tentativi) in bioinformatica a causa delle caratteristiche di prestazioni superiori in applicazioni pratiche.
Konrad Rudolph,

4

Ti consigliamo di dare un'occhiata all'elaborazione della sequenza del genoma su larga scala da parte dei dottori. Kasahara e Morishita.

Le stringhe composte dalle quattro lettere nucleotidiche A, C, G e T possono essere appositamente codificate in numeri interi per un'elaborazione molto più rapida. L'ordinamento Radix è tra i molti algoritmi discussi nel libro; dovresti essere in grado di adattare la risposta accettata a questa domanda e vedere un grande miglioramento delle prestazioni.


Il tipo di radix presentato in questo libro non è a posto, quindi non è utilizzabile per questo scopo. Per quanto riguarda la compattazione delle stringhe, lo sto già facendo (ovviamente). La mia (più o meno) soluzione finale (pubblicata di seguito) non mostra questo perché la libreria mi consente di trattarli come normali stringhe, ma il RADIXvalore utilizzato può (ed è) ovviamente adattarsi a valori più grandi.
Konrad Rudolph,

3

Potresti provare a usare un trie . L'ordinamento dei dati è semplicemente iterando attraverso il set di dati e inserendolo; la struttura è naturalmente ordinata e puoi considerarla simile a un B-Tree (tranne che invece di fare confronti, usi sempre le indicazioni indirette del puntatore).

Il comportamento della cache favorirà tutti i nodi interni, quindi probabilmente non migliorerai su quello; ma puoi anche giocherellare con il fattore di ramificazione del tuo trie (assicurati che ogni nodo si adatti a una singola riga della cache, alloca nodi trie simili a un heap, come un array contiguo che rappresenta un attraversamento di ordine di livello). Poiché i tentativi sono anche strutture digitali (O (k) inserisci / trova / elimina per elementi di lunghezza k), dovresti avere prestazioni competitive per un ordinamento radix.


Il trie ha lo stesso problema della mia ingenua implementazione: richiede O (n) memoria aggiuntiva che è semplicemente troppo.
Konrad Rudolph,

3

Vorrei scoppiare una rappresentazione a bit delle stringhe. Si dice che Burstsort abbia una localizzazione molto migliore rispetto a quella dei radix, mantenendo basso lo spazio aggiuntivo con i tentativi di scoppio al posto dei tentativi classici. La carta originale ha misure.


2

Radix-Sort non è consapevole della cache e non è l'algoritmo di ordinamento più veloce per grandi set. Puoi guardare:

Puoi anche usare la compressione e codificare ogni lettera del tuo DNA in 2 bit prima di archiviarli nella matrice di ordinamento.


fattura: potresti spiegare quali vantaggi ha questa qsortfunzione rispetto alla std::sortfunzione fornita da C ++? In particolare, quest'ultimo implementa un introsort altamente sofisticato nelle biblioteche moderne e sottolinea l'operazione di confronto. Non compro l'affermazione che si esibisce in O (n) per la maggior parte dei casi, poiché ciò richiederebbe un grado di introspezione non disponibile nel caso generale (almeno non senza un sacco di spese generali).
Konrad Rudolph,

Non sto usando c ++, ma nei miei test il QSORT in linea può essere 3 volte più veloce del qsort in stdlib. Ti7qsort è l'ordinamento più veloce per numeri interi (più veloce di QSORT in linea). Puoi anche usarlo per ordinare piccoli dati di dimensioni fisse. Devi fare i test con i tuoi dati.
fattura il

1

L'ordinamento radix MSB di dsimcha sembra carino, ma Nils si avvicina al nocciolo del problema con l'osservazione che la localizzazione della cache è ciò che ti sta uccidendo a problemi di grandi dimensioni.

Suggerisco un approccio molto semplice:

  1. Stimare empiricamente la dimensione più grande m per la quale un ordinamento radix è efficiente.
  2. Leggere blocchi di melementi alla volta, radixarli e scriverli (su un buffer di memoria se si dispone di memoria sufficiente, ma altrimenti su file), fino a esaurire l'input.
  3. Unisce i blocchi ordinati risultanti.

Mergesort è l'algoritmo di ordinamento più intuitivo per la cache di cui sono a conoscenza: "Leggi l'elemento successivo dall'array A o B, quindi scrivi un elemento nel buffer di output". Funziona in modo efficiente su unità nastro . Richiede 2nspazio per ordinare gli noggetti, ma la mia scommessa è che la posizione della cache molto migliorata che vedrai renderà poco importante - e se stavi usando un ordinamento radix non sul posto, avevi comunque bisogno di quello spazio extra.

Si noti infine che il mergesort può essere implementato senza ricorsione, e infatti farlo in questo modo chiarisce il vero modello di accesso alla memoria lineare.


1

Sembra che tu abbia risolto il problema, ma per la cronaca, sembra che una versione di un ordinamento radix sul posto funzionante sia "American Flag Sort". È descritto qui: Engineering Radix Sort . L'idea generale è di fare 2 passaggi su ciascun personaggio - prima conta quanti di ciascuno di essi hai, quindi puoi suddividere l'array di input in bin. Quindi riprova, scambiando ogni elemento nel cestino corretto. Ora ordina in modo ricorsivo ogni cestino nella posizione successiva del personaggio.


In realtà, la soluzione che utilizzo è strettamente correlata all'algoritmo di ordinamento delle bandiere. Non so se ci sia qualche distinzione rilevante.
Konrad Rudolph,

2
Non ho mai sentito parlare di American Flag Sort, ma apparentemente è quello che ho codificato: coliru.stacked-crooked.com/a/94eb75fbecc39066 Attualmente sta superando le prestazioni std::sort, e sono certo che un digitalizzatore multidigit potrebbe andare ancora più veloce, ma la mia suite di test ha memoria problemi (non l'algoritmo, la suite di test stessa)
Mooing Duck

@KonradRudolph: la grande distinzione tra l'ordinamento Flag e altri tipi di radix è il passaggio di conteggio. Hai ragione sul fatto che tutti i tipi di radix sono strettamente correlati, ma non considero il tuo un tipo Flag.
Mooing Duck

@MooingDuck: ho appena preso ispirazione dal tuo campione lì - mi sono bloccato nella mia implementazione indipendente e il tuo mi ha aiutato a tornare in pista. Grazie! Una possibile ottimizzazione - Non sono arrivato abbastanza lontano qui per vedere se vale ancora la pena: se l'elemento nella posizione in cui stai scambiando sembra che sia già dove deve essere, potresti voler saltare quello e passare a quello che non lo è. Rilevare ciò richiederà ovviamente una logica aggiuntiva e anche un possibile spazio di archiviazione aggiuntivo, ma poiché gli swap sono costosi rispetto ai confronti, può valere la pena farlo.
500 - Errore interno del server

1

Innanzitutto, pensa alla codifica del tuo problema. Sbarazzarsi delle stringhe, sostituirle con una rappresentazione binaria. Utilizzare il primo byte per indicare lunghezza + codifica. In alternativa, utilizzare una rappresentazione a lunghezza fissa con un limite di quattro byte. Quindi l'ordinamento radix diventa molto più semplice. Per un ordinamento radix, la cosa più importante è non avere la gestione delle eccezioni nel punto caldo del circuito interno.

OK, ho pensato un po 'di più al problema 4-nary. Per questo vuoi una soluzione come un albero di Judy . La soluzione successiva può gestire stringhe di lunghezza variabile; per lunghezza fissa basta rimuovere i bit di lunghezza, che in realtà lo rendono più facile.

Allocare blocchi di 16 puntatori. Il bit meno significativo dei puntatori può essere riutilizzato, poiché i blocchi saranno sempre allineati. Potrebbe essere necessario un allocatore di memoria speciale per esso (suddividere la memoria di grandi dimensioni in blocchi più piccoli). Esistono diversi tipi di blocchi:

  • Codifica con 7 bit di lunghezza di stringhe di lunghezza variabile. Mentre si riempiono, li sostituisci con:
  • Posizione codifica i prossimi due caratteri, hai 16 puntatori ai blocchi successivi, che terminano con:
  • Codifica bitmap degli ultimi tre caratteri di una stringa.

Per ogni tipo di blocco, è necessario memorizzare informazioni diverse negli LSB. Dato che hai stringhe di lunghezza variabile, devi archiviare anche end-of-string e l'ultimo tipo di blocco può essere utilizzato solo per le stringhe più lunghe. I 7 bit di lunghezza dovrebbero essere sostituiti da meno man mano che si approfondisce la struttura.

Ciò fornisce una memorizzazione ragionevolmente veloce e molto efficiente della memoria delle stringhe ordinate. Si comporterà in qualche modo come un trie . Per farlo funzionare, assicurati di costruire abbastanza unit test. Vuoi copertura di tutte le transizioni di blocco. Vuoi iniziare solo con il secondo tipo di blocco.

Per prestazioni ancora maggiori, potresti voler aggiungere diversi tipi di blocco e una dimensione di blocco più grande. Se i blocchi hanno sempre le stesse dimensioni e sono abbastanza grandi, puoi usare ancora meno bit per i puntatori. Con una dimensione di blocco di 16 puntatori, hai già un byte libero in uno spazio di indirizzi a 32 bit. Dai un'occhiata alla documentazione dell'albero di Judy per i tipi di blocchi interessanti. Fondamentalmente, aggiungi il codice e il tempo di progettazione per un compromesso spaziale (e di runtime)

Probabilmente vuoi iniziare con una radix diretta larga 256 per i primi quattro caratteri. Ciò fornisce un discreto compromesso spazio / tempo. In questa implementazione, si ottiene molto meno sovraccarico di memoria rispetto a un semplice trie; è circa tre volte più piccolo (non ho misurato). O (n) non è un problema se la costante è abbastanza bassa, come notato durante il confronto con lo quicksort O (n log n).

Sei interessato a gestire i doppi? Con brevi sequenze, ci saranno. Adattare i blocchi per gestire i conteggi è complicato, ma può essere molto efficiente in termini di spazio.


Non vedo come l'ordinamento radix diventa più facile nel mio caso se uso una rappresentazione ricca di bit. A proposito, il framework che utilizzo in realtà offre la possibilità di utilizzare una rappresentazione ricca di bit, ma questo è completamente trasparente per me come utente dell'interfaccia.
Konrad Rudolph,

Non quando guardi il tuo cronometro :)
Stephan Eggermont,

Daremo sicuramente un'occhiata agli alberi di Judy. I tentativi di vaniglia non portano davvero molto al tavolo, perché si comportano sostanzialmente come un normale ordinamento radix MSD con meno passaggi sugli elementi ma richiedono spazio di archiviazione aggiuntivo.
Konrad Rudolph,
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.