Esiste già una struttura dati "Raccoglitore"?

2

Stavo pensando a una struttura dati che non riesco a descrivere meglio della parola "libro", o più esattamente "raccoglitore". Mi chiedevo se questo fosse già stato implementato in librerie come Boost o altri.

Principio

È una struttura dati che consente l'accesso casuale a tempo costante e l'inserimento / eliminazione di un tempo costante ammortizzato (a condizione che la sequenza di tali eventi sia eseguita su indici non troppo distanti l'uno dall'altro).

Consiste di due stack "left" e "right" che rappresentano lo stato di un raccoglitore che sarebbe aperto su una determinata pagina. L'inserimento e l'eliminazione di elementi vicino a questa pagina è piuttosto veloce poiché consiste nello spingere / scoppiare le pagine nella / dalla pila di sinistra e sfogliare alcune pagine se necessario. Naturalmente nel peggiore dei casi si dovrebbe passare dalla prima pagina all'ultima pagina (riempire una pila con l'altra), ad esempio se l'utente inserisce qualcosa all'inizio e poi qualcosa alla fine.

Ecco un'implementazione molto rudimentale in C ++:

#include <iostream>
#include <vector>

using namespace std;

template<class T>
class Book {

    vector<T> left, right;

public:

    inline size_t size()
    {
        return left.size() + right.size();
    }

    void push(T t)
    {
        insert(size(),t);
    }

    T& get(size_t i)
    {
        if (i < left.size())
             return left[i];
        else return right[right.size()-i+left.size()-1];
    }

    void insert(size_t i, T t)
    {
        reposition(i-1);
        left.push_back(t);
    }

    void remove(size_t i)
    {
        reposition(i);
        left.pop_back();
    }

protected:

    void reposition(size_t i)
        // puts the element at position i on the top of the left stack
    {
        while (i < left.size()-1)
        {
            right.push_back(left.back());
            left.pop_back();
        }
        while (right.size() >= size()-i)
        {
            left.push_back(right.back());
            right.pop_back();
        }
    }

};

int main()
{
    Book<int> b;
    for (int i = 0; i < 15; i++)
        b.push(i);

    b.insert(5,42);
    b.remove(10);

    int i = 0;
    while (i < b.size())
    {
        if (b.get(i)%2 != 0)
             b.remove(i);
        else i++;
    }

    for (int i = 0; i < b.size(); i++)
    {
        cout << b.get(i) << " ";
    }
    cout << endl;
    return 0;
}

Nota: una possibile buona ottimizzazione sarebbe quella di avere i due stack in ordine inverso, per consentire a una memcpy dell'intera porzione di essere spostata dallo stack all'altro piuttosto che spostare gli elementi uno per uno (la memcpy è di solito molto più veloce ).

Usa

Quindi un buon caso d'uso di questo DS sarebbe un algoritmo che attraversa una sequenza di elementi e può, per ogni indice, aggiungere nuovi elementi o rimuovere vecchi elementi vicino a questo indice.

Un caso particolare di questo è il filtraggio di una matrice di elementi. Tutto quello che devi fare è iterare su tutti gli indici e semplicemente chiamare binder.remove (i) se l'elemento di index i deve essere rimosso.

Un modo efficiente (e ottimale) per farlo, data una serie di elementi, di solito è di allocare un altro array della stessa dimensione e copiare solo gli elementi che vogliamo salvare. È facile vedere che questo fa la stessa quantità di operazioni rispetto all'implementazione di Binder (se inizia sulla prima pagina), la differenza principale è che Binder consente un algoritmo più semplice che nasconde i dettagli sporchi dell'implementazione. (E non ci ritroviamo con un oggetto diverso.)

Le cose diventano ancora più complicate da implementare quando dobbiamo gestire la cancellazione di elementi vicini o un mix di cancellazione e aggiunta, mentre con il Raccoglitore DS è tutto incapsulato e l'algoritmo rimane molto semplice.

Quindi Binder DS può essere visto come una matrice (o "vettore" nel senso di C ++) che consente di lavorare in modo efficiente sul suo contenuto includendo un buffer di lavoro, quindi non si deve mai allocare spazio extra a mano (quindi aiuta a ridurre le allocazioni dinamiche della memoria). Rende trasparenti tutti i processi di mutazione.

Conclusione

Non l'ho testato a lungo contro algoritmi più convenzionali, ma mi aspetto che sarebbe un po 'più lento nell'accesso agli elementi di un vettore semplice a causa della maggiore complessità di avere due buffer invece di uno.

Pensi che questa sia una buona idea di una struttura dati e sarebbe efficiente? È già stato fatto e in quale libreria? Hai un'alternativa migliore da proporre?

    
posta LP_ 17.01.2014 - 14:04
fonte

1 risposta

3

È fondamentalmente una lista di chiusura lampo , una ben nota struttura di dati funzionali. Nella forma descritta su Wikipedia, si tratta di una struttura dati persistente, il che significa che dopo aver eseguito un aggiornamento (cancella o inserisci), la vecchia versione della struttura dati è ancora accessibile. Usando le matrici anziché le liste concatenate per implementare gli stack, si rinuncia alla persistenza e si ottiene invece la possibilità di accesso casuale, che in realtà è una bella idea.

Poiché insert e delete sono efficienti solo se si verificano vicino alla posizione corrente, suggerirei la seguente modifica, ispirata al tipico schema di utilizzo di una cerniera nella programmazione funzionale: invece di dare la posizione come argomento a insert/delete , introdurrei il concetto di focus . L'elemento centrale della struttura dei dati è l'indice attualmente al centro dei due stack. È il posto in cui puoi inserire ed eliminare in modo efficiente. Quindi fornirei le interfacce per leggere lo stato attivo ei metodi next() e previous() per spostarlo. Puoi inserire ed eliminare solo il focus corrente, quindi non puoi commettere l'errore di introdurre un'operazione costosa nel tuo algoritmo per sbaglio.

    
risposta data 25.01.2014 - 04:03
fonte

Leggi altre domande sui tag