Interfaccia della struttura dati standardizzata

4

Voglio lavorare con una varietà di strutture dati (matrici, elenchi singoli / doppiamente collegati, strutture ordinate, ecc.) su base plug-and-play. Ad esempio, voglio essere in grado di scambiare facilmente la lista ordinata e la matrice (che viene riordinata dopo ogni inserimento / eliminazione), per testare quale ha prestazioni migliori.

In ogni lingua che conosco, l'interfaccia pubblica a tali strutture dati è molto incoerente. Quindi cambiare l'implementazione richiede molto lavoro.

Esiste un linguaggio o un modello di progettazione indipendente dalla lingua che rende facile definire un'interfaccia pubblica generica che funzioni con qualsiasi struttura dati?

Ovviamente, capisco come sarebbero diverse le prestazioni (ad esempio, la ricerca binaria in un array ordinato rispetto alla ricerca lineare in un array normale). Tutto quello che chiedo è che le prestazioni della struttura dati non si degradino asintoticamente o per un fattore costante di grandi dimensioni a causa dell'uso dell'interfaccia standard.

Esempio di problema: scorrere il contenitore di numeri, trovare i due numeri più vicini tra loro e rimuoverli. (Se più coppie hanno la stessa distanza, scegli una coppia arbitraria).

Mi piacerebbe che il codice risolva il problema rimanendo invariato mentre inserisco e esco diversi contenitori.

    
posta max 01.05.2012 - 05:42
fonte

10 risposte

6

Il framework di raccolta più elaborato con cui ho lavorato personalmente è Scala , in particolare combinato con le estensioni di Scalaz libreria.

La community Haskell al momento sembra essere l'apripista quando si tratta di individuare interfacce di raccolta molto generali, ad es. Data.Traversable o Data.Foldable classi di tipi, cerniere, iterate, enumerati, ecc. (Molto di questo è anche implementato in Scalaz .)

Ecco un esempio su come risolvere il tuo problema in Scala. Nota, non ho fatto nulla per rendere questo efficiente o bello, né ho pensato a se questo algoritmo è ragionevole o meno.

La caratteristica importante è che questo codice è solo dipendente dalle operazioni fornite dal tratto Seq ( zipWithIndex , sorted , foldRight , filterNot ), che è molto generale: presuppone solo che la collezione abbia una dimensione definita e che gli elementi possano apparire più volte. Questo è tutto.

val s = Seq(5, 1, 9, 2, 6) // our test sequence

val sWithIndex = s zipWithIndex

val rejects = sWithIndex.sorted.foldRight(
  (Int.MaxValue, (
    (Int.MinValue, Int.MinValue), (Int.MinValue, Int.MinValue)))) {
  case ((el, i), (minDist, ((n1, i1), (n2, i2)))) => 
    if(math.abs(n2 - el) <= minDist)
      (math.abs(n2 - el), ((n2, i2), (el, i)))
    else
      (minDist, ((n1, i1), (n2, i2)))
  }._2

val rejectIndices = (rejects._1._2, rejects._2._2)

val result = sWithIndex filterNot { case (_, i) =>
  i == rejectIndices._1 || i == rejectIndices._2 } map (_._1)
// result = List(1, 9, 2)

Questo algoritmo è O (n * log n), perché sorted è un ordinamento stabile generico. Potresti renderlo O (n) usando un ordinamento radix, dato che gli elementi sono noti come interi.

    
risposta data 01.05.2012 - 17:53
fonte
5

Il problema con le strutture dati che hai citato è che hanno una semantica diversa. Cioè funzionano in modo diverso e non possono avere un'interfaccia comune. Per esempio. una lista potrebbe avere un metodo Add che aggiungerebbe un elemento alla lista. Un array, tuttavia, non può avere un tale metodo, dal momento che la lunghezza dell'array è stata riparata. Un array consente di accedere agli elementi in modo casuale tramite un indicizzatore ( a[i] = x; print(a[k]); ), ma non è possibile farlo con un elenco collegato. In un elenco collegato è possibile accedere solo all'elemento successivo o precedente a partire dall'intestazione o coda della lista. Una raccolta non ordinata avrebbe probabilmente un metodo Sort , una raccolta intrinsecamente ordinata (ad esempio un albero binario ordinato da una determinata chiave) ovviamente no, poiché gli elementi vengono ordinati all'inserimento.

Una singola interfaccia non funzionerà. Hai bisogno di una serie di interfacce, ognuna delle quali espone aspetti specifici delle collezioni.

Se vuoi confrontare diversi tipi di raccolta, ti suggerisco di creare wrapper usando un'interfaccia comune. Dovresti quindi eseguire i test sui wrapper.

Tale interfaccia wrapper potrebbe apparire come questa

public interface ICollectionTestWrapper<T>
{
    void FillWithTestData(IEnumerable<T> data);

    bool IsSortable { get; }
    void Sort();

    void FindElement(T element); // No return value needed for speed tests.

    // TODO: Add functionality for the tests you have in mind
    ...
}
    
risposta data 01.05.2012 - 16:18
fonte
4

Java lo fa un po '.

Vedi Elenco , Imposta o Mappa interfacce. Ogni interfaccia ha un numero di implementazioni, implementando le varie operazioni in modi diversi.

Python lo fa anche in virtù della digitazione anatra. L'interfaccia di oggetti diversi è simile, quindi si utilizza lo stesso codice su diverse strutture di dati.

    
risposta data 01.05.2012 - 06:18
fonte
2

C'è anche uno stile programmazione generica , che consente di dividere dati e algoritmi. Per maggiori dettagli vedi anche C ++ Standard Template Library

    
risposta data 01.05.2012 - 16:38
fonte
2

La sua tabella denominata nella maggior parte dei sistemi:

Sistemi OO che forniscono il concetto di supporto Interface che sostituisce in modo trasparente le implementazioni. Java fa esattamente questo Collection < - List < - ArrayList , LinkedList , ecc.

C ++ supporta anche questo. Python supporta questo. C non lo supporta direttamente.

Iniezione delle dipendenze nelle lingue che supportano questo renderebbe possibile il collegamento e l'implementazione di implementazioni concrete configurabili.

  • Java: per un'interfaccia estremamente generalizzata Guava ha il Table interfaccia e abbastanza implementazioni specifiche per coprire quasi tutto casi.

  • Lua ha davvero un tipo di raccolta e cioè Table .

Un'interfaccia Table può servire a tutti gli scopi generali di Associative Array e ad un accesso casuale normale Array se usi solo numeri per le chiavi.

    
risposta data 01.05.2012 - 16:26
fonte
1

Example problem: iterate through the container of numbers, find the two numbers that are nearest each other, and remove the pair of them. (If several pairs are equal distance, pick an arbitrary pair).

Le strutture dati hanno interfacce diverse perché fanno cose diverse. Le matrici e gli elenchi concatenati sono in qualche modo comparabili in quanto entrambi memorizzano una raccolta ordinata, ma ci sono differenze importanti anche qui: generalmente non si fa riferimento agli elementi di un elenco collegato per indice, la memoria viene gestita in modo molto diverso, ecc. guardando le altre strutture (cumuli, insiemi, mappe, alberi binari, ecc.) le differenze aumentano e le somiglianze diminuiscono.

Se sei disposto a rinunciare ad alcuni dei vantaggi di ciascuna delle strutture che collaudi per fornire un'interfaccia uniforme, non c'è niente che ti impedisca di definire un'interfaccia "Elenco" e di implementarla utilizzando ciascuno dei strutture che vuoi testare. Qualsiasi linguaggio che supporti il polimorfismo farà il lavoro, e puoi facilmente immaginare un insieme di operazioni che sono comuni a tutte le strutture (inserisci, rimuovi, ordina, cerca). Non dimenticarti che le condizioni di test saranno un po 'forzate: forzando le strutture a testare per conformarsi a un'interfaccia comune, ignorerai gli aspetti che differenziano ciascuna struttura dal gruppo.

    
risposta data 01.05.2012 - 18:57
fonte
0

Sembra che tu stia cercando il modello di strategia . Quello che farai è definire la tua interfaccia (come find-nearest-neighbors(NumericCollection) o find-closest-value(NumericCollection, Integer) ) e fornirne diverse implementazioni. I dettagli di chiamare le varie implementazioni sono necessariamente specifici della lingua, ma finché ognuno supporta l'interfaccia definita, si dovrebbe avere una buona base per il confronto.

    
risposta data 01.05.2012 - 16:18
fonte
0

L'STL di C ++ ha qualcosa in questo senso: tutte le raccolte supportano gli iteratori che puoi usare per accedere ai membri e l'iteratore è una classe standard stessa, quindi ha le operazioni che vuoi.

Quindi puoi camminare su un array usando begin () fino a end (), e puoi scambiare l'array per un elenco senza problemi.

Tuttavia, alcune collezioni supportano diverse operazioni - sebbene sia possibile ordinare sia array, elenco e coda, non è possibile ordinare una mappa o un set (sono già ordinati). Ciò significa che alcune operazioni non hanno senso per alcune raccolte.

Guarda la sezione algorithms dell'STL per un insieme di operazioni che si applicano a ogni diverso tipo di raccolta STL .

    
risposta data 01.05.2012 - 17:21
fonte
0

Dovresti ricercare il Pattern adattatore . Usando il Pattern dell'adapter, definiresti un'interfaccia, IContainer , che fornisce tutte le operazioni richieste, come l'indicizzazione, l'ordinamento, ecc. Dovresti quindi creare diverse implementazioni dell'interfaccia IContainer , come ArrayContainer , LinkedListContainer e così via. Successivamente, utilizzando una tecnica come Iniezione delle dipendenze , potresti scrivere i tuoi algoritmi attorno all'interfaccia IContainer , in tal modo disaccoppiando il tuo codice da un particolare tipo di contenitore. Ciò ti consentirebbe di sostituire e misurare le prestazioni di diversi tipi di contenitori.

    
risposta data 01.05.2012 - 17:30
fonte
0

Is there a language, or a language-agnostic design pattern, that makes it easy to define a generic public interface that works with any data structure?

Haskell sembra un suggerimento fantastico, ma forse un po 'hardcore. Almeno ho un po 'paura di Haskell (è un linguaggio il cui codice mi ispira sempre, ma non ho mai avuto il coraggio di provare qualcosa di sostanziale).

Personalmente raccomando C ++ per questo dato che la generazione del codice dei template C ++ ti permette di eliminare qualsiasi costo di astrazione (dispatch dinamico, cioè) fuori dall'immagine e concentrarti solo sull'efficienza delle strutture di dati stessi mentre allo stesso tempo muti la struttura dei dati facilmente (potrebbe essere un po 'difficile nei linguaggi funzionali senza cambiare l'intero modo in cui ci si avvicina alle strutture dati). È una delle poche lingue che conosco personalmente dove è possibile scrivere una struttura di dati contigua che si comporta altrettanto bene per un accesso casuale come un semplice vecchio array, ad esempio, poiché l'ottimizzatore schiaccia solo tutta la struttura di codice che hai costruito in frantumi.

Viene anche fornito con un set abbastanza completo di contenitori standard (anche se mancano alcuni semi-comuni come try) con un'interfaccia agnostica "vicina" alla struttura dei dati. Un po 'di wrapping dovrebbe darti un'interfaccia universale che ti permetta di fare quello che vuoi, almeno per i tipi di valore (set vs. mappa). I contenitori associativi chiave / valore sono un po 'più difficili da generalizzare e potrebbero aver bisogno di essere nella loro categoria, semplicemente perché tendono a richiedere un design dell'interfaccia molto diverso.

La chiave per farlo è definire la tua interfaccia universale. Una sequenza standard C ++ come vector ha queste funzioni:

push_back -- appends an element to the back of the container.
insert -- inserts an element to any place in the container.
erase -- erases an element from any place in the container.
range insert -- inserts multiple elements to any place in the container.
range erase -- removes a range of elements from any place in the container.
empty -- returns size() == 0.
size -- returns the number of elements.
clear -- removes all elements from the container.
begin -- returns an iterator pointing to the beginning of the container.
end -- returns an iterator pointing to the end of the container.

C'è anche un costruttore di riempimenti, un costruttore di intervalli, ecc. (e puoi anche passare il tuo allocatore di memoria che di solito non è così utile per qualcosa come vector ma può fare un'enorme differenza per le strutture collegate).

La lista concatenata, std::list , è conforme agli stessi requisiti di interfaccia tranne alcune piccole differenze in questo contesto come una funzione membro push_front che viene omessa da std::vector deliberatamente per scoraggiare il suo uso lì.

Puoi facilmente racchiudere tutti questi contenitori e, con poco sforzo, applicare un'interfaccia universale ... magari con funzioni come queste:

push_front -- insert to front.
push_back -- insert to back.
erase -- erase anywhere.
insert -- insert anywhere with no regard about where the element goes.
insert_sorted -- do an insertion sort.
...

Ecc. Non ci vorrà molto tempo per avvolgere tutti i contenitori C ++ esistenti applicabili per conformarsi a questa interfaccia universale del tuo design e implementarne di nuovi conformi, e quindi puoi valutare il tuo cuore e generare grafici e tutto ciò che desideri.

Una cosa importante da notare su C ++ è l'iteratore che progetta algoritmi di guida. È possibile implementare una struttura dati personalizzata e, semplicemente esponendo gli iteratori, centinaia di algoritmi esistenti (non solo standard ma anche di terze parti) diventano improvvisamente applicabili alla struttura dei dati senza ulteriori sforzi. Non è necessario implementare come un metodo sort per la struttura dei dati personalizzati, ad esempio, poiché std::sort funzionerà su di esso una volta che esporrai gli iteratori a esso.

    
risposta data 15.01.2016 - 04:55
fonte

Leggi altre domande sui tag