A che serve implementare uno stack usando due code?

34

Ho la seguente domanda a casa:

Implement the stack methods push(x) and pop() using two queues.

Mi sembra strano perché:

  • Una pila è una coda (LIFO)
  • Non vedo perché ti occorrono due code per implementarlo

Ho cercato in giro:

e 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 in due code sembra inutilmente complicata.

Diciamo che per i push è il più efficiente del 2 (come ho fatto sopra), push sarebbe rimasto lo stesso, e pop avrebbe semplicemente bisogno di iterare fino all'ultimo elemento e di restituirlo. In entrambi i casi, push sarebbe O(1) e pop sarebbe O(n) ; ma la versione a coda singola sarebbe drasticamente più semplice. Dovrebbe richiedere solo un ciclo for.

Mi manca qualcosa? Qualsiasi comprensione qui sarà apprezzata.

    
posta Carcigenicate 16.06.2015 - 22:05
fonte

6 risposte

44

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

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

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

Nota che dovresti usare interfaccia java.util.Queue nel codice anziché utilizzare direttamente l'implementazione:

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

Ciò ti consente di utilizzare altre implementazioni Queue se lo desideri, oltre a nascondere i metodi 2 che sono su LinkedList che potrebbero aggirare lo spirito dell'interfaccia Queue . Questo include get(int) e pop() (mentre il tuo codice viene compilato, c'è un errore logico là dato i vincoli del tuo compito. Dichiarare le tue variabili come Queue invece di LinkedList lo rivelerà). Lettura correlata: Informazioni su "programmazione su un'interfaccia" e Perché le interfacce sono utili?

1 Ricordo ancora: l'esercizio era di invertire una pila usando i metodi solo sull'interfaccia dello Stack e senza metodi di utilità in java.util.Collections o altre classi di utilità "solo statiche". La soluzione corretta prevede l'utilizzo di altre strutture di dati come oggetti di attesa temporanei: devi conoscere le diverse strutture di dati, le loro proprietà e come combinarle per farlo. Ho bloccato la maggior parte della mia classe CS101 che non avevo mai programmato prima.

2 I metodi sono ancora lì, ma non è possibile accedervi senza cast di tipo o riflessione. Quindi non è facile usare questi metodi non in coda.

    
risposta data 16.06.2015 - 22:13
fonte
19

Non c'è alcun vantaggio. Hai capito correttamente che l'uso delle code per implementare uno stack porta a una complessità orribile nel tempo. Nessun programmatore (competente) farebbe mai qualcosa di simile nella "vita reale".

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

  • ricapitola Stacks
  • riepiloghi le code
  • ti abitui al ragionamento algoritmico
  • impari gli algoritmi relativi allo stack
  • si arriva a pensare ai compromessi negli algoritmi
  • realizzando l'equivalenza di Code e Stack, colleghi vari argomenti del tuo corso
  • ottieni esperienza di programmazione pratica

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

    
risposta data 16.06.2015 - 22:20
fonte
5

C'è sicuramente un vero scopo per fare una coda di due stack. Se si utilizzano strutture di dati immutabili da un linguaggio funzionale, è possibile inserire una pila di elementi pusibili e prelevare da un elenco di elementi modificabili. Gli oggetti poppabili vengono creati quando tutti gli oggetti sono stati estratti e il nuovo stack poppable è il retro dello stack pushable, dove il nuovo stack pushable è ora vuoto. È efficiente

Per quanto riguarda una pila composta da due code? Ciò potrebbe avere senso in un contesto in cui hai a disposizione un mucchio di code grandi e veloci. È decisamente inutile come questo tipo di esercizio di Java. Ma potrebbe avere senso se quelli sono canali o code di messaggistica. (es .: N messaggi accodati, con un'operazione O (1) per spostare (N-1) elementi in primo piano in una nuova coda.)

    
risposta data 17.06.2015 - 00:32
fonte
2

L'esercizio è inutilmente escogitato da un punto di vista pratico. Il punto è costringerti a usare l'interfaccia della coda in modo intelligente per implementare lo stack. Ad esempio, la soluzione "Una coda" richiede di eseguire iterazioni sulla coda per ottenere l'ultimo valore di input per l'operazione "pop" di stack. Tuttavia, una struttura dati della coda non consente di scorrere i valori, si è limitati ad accedervi in FIFO (first-in, first-out).

    
risposta data 16.06.2015 - 22:19
fonte
2

Come altri già hanno notato: non c'è alcun vantaggio nel mondo reale.

In ogni caso, una risposta alla seconda parte della tua domanda, perché non usare semplicemente una coda, va oltre Java.

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

Questo non è necessariamente vero per la lista dei collegamenti ingenui / canonici come implementerebbe un programmatore C / C ++, che manterrebbe solo i puntatori al primo e all'ultimo elemento e ogni elemento un puntatore all'elemento successivo.

In questo caso size() è O (n) e dovrebbe essere evitato nei loop. Oppure l'implementazione è opaca e fornisce solo il minimo add() e remove() .

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

Detto questo, probabilmente non si inventerebbe qualcosa di simile se vivessi in Java-land.

    
risposta data 16.06.2015 - 23:47
fonte
0

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

In termini di usi effettivi 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 redstone di Minecraft rappresentano un sistema completo di Turing, che le persone hanno utilizzato per implementare circuiti logici e persino intere CPU. Nei primi giorni dei giochi basati su script, anche molti dei primi robot di gioco sono stati implementati in questo modo.

Ma puoi anche applicare questo principio al contrario, assicurandoti che non sia possibile in un sistema quando non vuoi che sia . Questo può verificarsi in contesti di sicurezza: ad esempio, potenti sistemi di configurazione possono essere una risorsa, ma ci sono ancora gradi di potenza che potresti non dare agli utenti. Questo limita ciò che puoi consentire al linguaggio di configurazione di fare, per evitare che venga sovvertito da un utente malintenzionato, ma in questo caso, è ciò che desideri.

    
risposta data 17.06.2015 - 15:17
fonte

Leggi altre domande sui tag