Qual è lo scopo di implementare uno Stack usando due code?


34

Ho la seguente domanda per i compiti:

Implementare i metodi stack push (x) e pop () usando due code.

Mi sembra strano perché:

  • Uno stack è una coda (LIFO)
  • Non vedo perché occorrano due code per implementarlo

Ho cercato in giro:

e ho trovato un paio di soluzioni. Questo è quello che ho finito con:

public class Stack<T> {
    LinkedList<T> q1 = new LinkedList<T>();
    LinkedList<T> q2 = new LinkedList<T>();

    public void push(T t) {
        q1.addFirst(t);
    }

    public T pop() {
        if (q1.isEmpty()) {
            throw new RuntimeException(
                "Can't pop from an empty stack!");
        }

        while(q1.size() > 1) {
            q2.addFirst( q1.removeLast() );
        }

        T popped = q1.pop();

        LinkedList<T> tempQ = q1;
        q1 = q2;
        q2 = tempQ;

        return popped;
    }
}

Ma non capisco quale sia il vantaggio rispetto all'utilizzo di una singola coda; la versione a due code sembra inutilmente complicata.

Supponiamo che scegliamo che i push siano i più efficienti dei 2 (come ho fatto sopra), pushrimarrebbero gli stessi e poprichiederebbero semplicemente di ripetere l'ultimo elemento e di restituirlo. In entrambi i casi, pushsarebbe O(1)e popsarebbe O(n); ma la versione a coda singola sarebbe drasticamente più semplice. Dovrebbe richiedere solo un singolo ciclo.

Mi sto perdendo qualcosa? Qualsiasi approfondimento qui sarebbe apprezzato.


17
Una coda si riferisce in genere a una struttura FIFO mentre lo stack è una struttura LIFO. L'interfaccia di LinkedList in Java è quella di un deque (coda doppia) che consente l'accesso FIFO e LIFO. Prova a cambiare la programmazione per l' interfaccia della coda anziché per l'implementazione di LinkedList.

12
Il problema più comune è implementare una coda usando due stack. Potresti trovare interessante il libro di Chris Okasaki su strutture di dati puramente funzionali.
Eric Lippert,

2
Costruendo ciò che ha detto Eric, a volte puoi ritrovarti in un linguaggio basato sullo stack (come dc o un automa push down con due pile (equivalente a una macchina da turismo perché, beh, puoi fare di più)) dove potresti trovarti con pile multiple, ma nessuna coda.

1
@MichaelT: Oppure puoi anche trovarti in esecuzione su una CPU basata su stack
slebetman,

11
"Uno stack è una coda (LIFO)" ... uhm, una coda è una linea di attesa. Come la linea per l'utilizzo di un bagno pubblico. Le linee in cui aspetti si comportano mai in modo LIFO? Smetti di usare il termine "coda LIFO", non ha senso.
Mehrdad,

Risposte:


44

Non c'è alcun vantaggio: questo è un esercizio puramente accademico.

Un molto tempo fa, quando ero una matricola al college avevo un esercizio simile 1 . L'obiettivo era insegnare agli studenti come utilizzare la programmazione orientata agli oggetti per implementare algoritmi invece di scrivere soluzioni iterative usando forloop con contatori di loop. Invece, combina e riutilizza le strutture di dati esistenti per raggiungere i tuoi obiettivi.

Non userai mai questo codice in Real World TM . Quello che devi togliere da questo esercizio è come "pensare fuori dagli schemi" e riutilizzare il codice.


Tieni presente che dovresti utilizzare l' interfaccia java.util.Queue nel tuo codice anziché utilizzare direttamente l'implementazione:

Queue<T> q1 = new LinkedList<T>();
Queue<T> q2 = new LinkedList<T>();

Ciò consente di utilizzare altre Queueimplementazioni, se lo si desidera, oltre a nascondere 2 metodi LinkedListche potrebbero aggirare lo spirito Queuedell'interfaccia. Questo include get(int)e pop()(mentre il tuo codice viene compilato, c'è un errore logico in là dato i vincoli del tuo compito. Dichiarare le tue variabili come Queueinvece di LinkedListrivelarlo). Lettura correlata: Comprensione della "programmazione di un'interfaccia" e Perché le interfacce sono utili?

1 Ricordo ancora: l'esercizio era di invertire uno Stack utilizzando solo metodi nell'interfaccia Stack e nessun metodo di utilità in java.util.Collectionso altre classi di utilità "solo statiche". La soluzione corretta prevede l'utilizzo di altre strutture di dati come oggetti di memorizzazione temporanei: devi conoscere le diverse strutture di dati, le loro proprietà e come combinarle per farlo. La maggior parte della mia classe CS101 non ha mai programmato prima.

2 I metodi sono ancora presenti, ma non è possibile accedervi senza i tipi cast o reflection. Quindi non è facile usare quei metodi non in coda.


1
Grazie. Immagino abbia senso. Mi sono anche reso conto che sto usando operazioni "illegali" nel codice sopra (spingendo verso la parte anteriore di un FIFO), ma non credo che cambi nulla. Ho invertito tutte le operazioni e funziona ancora come previsto. Aspetterò un po 'prima di accettare perché non voglio scoraggiare altre persone dal dare input. Grazie ugualmente.
Carcigenicato,

19

Non ci sono vantaggi Hai capito correttamente che l'uso di Code per implementare uno Stack porta a un'orribile complessità temporale. Nessun programmatore (competente) farebbe mai qualcosa del genere nella "vita reale".

Ma è possibile Puoi usare un'astrazione per implementarne un'altra e viceversa. Uno Stack può essere implementato in termini di due code e allo stesso modo è possibile implementare una coda in termini di due stack. Il vantaggio di questo esercizio è:

  • ricapitoli Stack
  • ricapitolando le code
  • ti abitui al pensiero algoritmico
  • impari algoritmi relativi allo stack
  • puoi pensare a compromessi negli algoritmi
  • realizzando l'equivalenza di code e pile, colleghi vari argomenti del tuo corso
  • ottieni esperienza pratica di programmazione

In realtà, questo è un ottimo esercizio. Dovrei farlo da solo adesso :)


3
@JohnKugelman Grazie per la modifica, ma intendevo davvero "orribile complessità temporale". Per una pila di lista concatenata base push, peeke pople operazioni sono O (1). Lo stesso per uno stack ridimensionabile basato su array, tranne per il fatto che pushè in ammortamento O (1), con il caso peggiore O (n). Rispetto a ciò, uno stack basato sulla coda è notevolmente inferiore con O (n) push, O (1) pop e peek, o in alternativa O (1) push, O (n) pop e peek.
Amon,

1
"orribile complessità temporale" e "enormemente inferiore" non sono esattamente giusti. La complessità ammortizzata è ancora O (1) per push e pop. C'è una domanda divertente in TAOCP (vol1?) A riguardo (in pratica devi mostrare che il numero di volte in cui un elemento può passare da uno stack all'altro è costante). Le peggiori prestazioni del caso per un'operazione sono diverse, ma raramente sento qualcuno parlare delle prestazioni O (N) per push in ArrayLists - non il numero solitamente interessante.
Voo,

5

C'è sicuramente un vero scopo per fare una fila di due pile. Se si utilizzano strutture di dati immutabili da un linguaggio funzionale, è possibile inserire in una pila di oggetti pushable ed estrarre da un elenco di oggetti poppable. Gli oggetti visualizzabili vengono creati quando tutti gli oggetti sono stati eliminati e la nuova pila estraibile è il contrario della pila sospendibile, dove la nuova pila sospendibile è vuota. È efficiente.

Per quanto riguarda uno stack composto da due code? Ciò potrebbe avere senso in un contesto in cui sono disponibili molte code veloci e di grandi dimensioni. È decisamente inutile come questo tipo di esercizio Java. Ma potrebbe avere senso se si tratta di canali o code di messaggistica. (ovvero: N messaggi accodati, con un'operazione O (1) per spostare (N-1) gli elementi in primo piano in una nuova coda.)


Hmm .. questo mi ha fatto pensare all'utilizzo dei registri a scorrimento come base del calcolo e all'architettura della cinghia / mulino
slebetman,

caspita, la CPU del mulino è davvero interessante. Un "Queue Machine" di sicuro.
Rob,

2

L'esercizio è inutilmente concepito da un punto di vista pratico. Il punto è costringerti a utilizzare l'interfaccia della coda in modo intelligente per implementare lo stack. Ad esempio, la soluzione "Una coda" richiede che si esegua l'iterazione sulla coda per ottenere l'ultimo valore di input per l'operazione "pop" dello stack. Tuttavia, una struttura di dati in coda non consente di scorrere i valori, è necessario accedervi nel primo in, primo in uscita (FIFO).


2

Come altri hanno già notato: non esiste alcun vantaggio nel mondo reale.

Ad ogni modo, una risposta alla seconda parte della tua domanda, perché non semplicemente usare una coda, è oltre Java.

In Java anche l' Queueinterfaccia ha un size()metodo e tutte le implementazioni standard di quel metodo sono O (1).

Ciò non è necessariamente vero per l'elenco link ingenuo / canonico implementato da un programmatore C / C ++, che manterrebbe solo i puntatori al primo e ultimo elemento e ogni elemento un puntatore al successivo elemento.

In tal caso size()è O (n) e dovrebbe essere evitato nei cicli. O l'implementazione è opaco e fornisce solo il minimo indispensabile add()e remove().

Con una tale implementazione dovresti prima contare il numero di elementi trasferendoli nella seconda coda, trasferire gli n-1elementi nella prima coda e restituire l'elemento rimanente.

Detto questo, probabilmente non inventerebbe qualcosa del genere se vivi in ​​Java-land.


Un buon punto sul size()metodo. Tuttavia, in assenza di un size()metodo O (1) , è banale per lo stack tenere traccia delle dimensioni attuali. Niente per fermare un'implementazione a una coda.
cmaster

Tutto dipende dall'implementazione. Se si dispone di una coda, implementata con solo puntatori e puntatori in avanti verso il primo e l'ultimo elemento, è comunque possibile scrivere e un algoritmo che rimuove un elemento, lo salva in una variabile locale, aggiunge l'elemento precedentemente in quella variabile alla stessa coda fino a quando il il primo elemento viene visto di nuovo. Funziona solo se è possibile identificare in modo univoco un elemento (ad es. Tramite puntatore) e non solo qualcosa con lo stesso valore. O (n) e utilizza solo add () e remove (). Ad ogni modo, è più facile ottimizzare, quello di trovare un motivo per farlo, tranne pensare agli algoritmi.
Thraidh,

0

È difficile immaginare un uso per tale implementazione, è vero. Ma la maggior parte del punto è dimostrare che può essere fatto .

In termini di usi reali per queste cose, tuttavia, posso pensare a due. Un uso per questo è l'implementazione di sistemi in ambienti vincolati che non sono stati progettati per questo : ad esempio, i blocchi di pietra rossa di Minecraft si presentano per rappresentare un sistema completo di Turing, che le persone hanno usato per implementare circuiti logici e persino intere CPU. Nei primi giorni di gioco basato su script, molti dei primi robot di gioco sono stati implementati in questo modo.

Ma puoi anche applicare questo principio al contrario, assicurandoti che qualcosa non sia possibile in un sistema quando non vuoi che sia . Ciò può emergere in contesti di sicurezza: ad esempio, potenti sistemi di configurazione possono essere una risorsa, ma ci sono ancora gradi di potenza che potresti preferire non dare agli utenti. Ciò limita ciò che è possibile consentire al linguaggio di configurazione, affinché non venga sovvertito da un utente malintenzionato, ma in questo caso è quello che si desidera.

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.