Una pila, due code


59

sfondo

Diversi anni fa, quando ero un laureando, ci veniva dato un compito di analisi ammortizzata. Non sono stato in grado di risolvere uno dei problemi. L'avevo chiesto in teoria comp. , Ma non è emerso alcun risultato soddisfacente. Ricordo che il corso TA ha insistito su qualcosa che non poteva provare, e ha detto che aveva dimenticato la prova, e ... [sai cosa].

Oggi ho ricordato il problema. Ero ancora desideroso di sapere, quindi eccolo ...

La domanda

È possibile implementare uno stack utilizzando due code , in modo che entrambe le operazioni PUSH e POP vengano eseguite nel tempo ammortizzato O (1) ? Se sì, potresti dirmi come?

Nota: la situazione è abbastanza semplice se vogliamo implementare una coda con due stack (con le corrispondenti operazioni ENQUEUE & DEQUEUE ). Si prega di osservare la differenza.

PS: Il problema sopra riportato non è lo stesso compito. I compiti non richiedevano limiti inferiori; solo un'implementazione e l'analisi del tempo di esecuzione.


2
Suppongo che sia possibile utilizzare solo una quantità limitata di spazio diverso dalle due code (O (1) o O (registro n)). Mi sembra impossibile, perché non abbiamo modo di invertire l'ordine di un lungo flusso di input. Ma ovviamente questa non è una prova a meno che non possa essere trasformata in un'affermazione rigorosa ...
Tsuyoshi Ito,

@Tsuyoshi: hai ragione sul presupposto di spazio limitato. E sì, era quello che ho detto a
quell'AT

2
@Tsuyoshi: non penso che tu debba assumere un limite allo spazio in generale, devi solo supporre che non ti è permesso conservare gli oggetti spinti e popati dallo stack in qualsiasi posto diverso dalle due code (e probabilmente un numero costante di variabili).
Kaveh,

@SadeqDousti Secondo me, l'unico modo per farlo sarebbe se si usasse un'implementazione dell'elenco collegato di una coda e si utilizzassero alcuni puntatori per puntare sempre in cima allo "stack"
Charles Addis,

2
Sembra che l'AT avrebbe potuto effettivamente voler dire "Implementa una coda usando due stack" che è effettivamente possibile proprio in "O (1) tempo ammortizzato".
Thomas Ahle,

Risposte:


45

Non ho una risposta effettiva, ma ecco alcune prove che il problema è aperto:

  • Non è menzionato in Ming Li, Luc Longpré e Paul MB Vitányi, "Il potere della coda", Structures 1986, che considera molte altre simulazioni strettamente correlate

  • Non è menzionato in Martin Hühne, "Il potere di diverse code", Theor. Comp. Sci. 1993, un articolo successivo.

  • Non è menzionato in Holger Petersen, "Stacks versus Deques", COCOON 2001.

  • Burton Rosenberg, "Riconoscimento non deterministico rapido di linguaggi senza contesto che utilizzano due code", Inform. Proc. Lett. 1998, fornisce un algoritmo O (n log n) a due code per riconoscere qualsiasi CFL usando due code. Ma un automa pushdown non deterministico è in grado di riconoscere i CFL in tempo lineare. Quindi, se ci fosse una simulazione di uno stack con due code più veloci di O (log n) per operazione, Rosenberg e i suoi arbitri avrebbero dovuto saperlo.


4
+1 per riferimenti eccellenti. Ci sono alcuni aspetti tecnici, tuttavia: alcuni dei documenti, come il primo, non considerano il problema di simulare una pila usando due code (per quanto posso dire dall'abstract). Altri considerano l'analisi del caso peggiore, non il costo ammortizzato.
MS Dousti,

13

La risposta che segue è "barare", in quanto mentre non utilizza alcuno spazio tra le operazioni, le operazioni stesse possono usare più dello spazio . Vedi altrove in questa discussione per una risposta che non presenta questo problema.O(1)

Anche se non ho una risposta alla tua domanda esatta, ho trovato un algoritmo che funziona nel tempo invece di . Credo che questo sia stretto, anche se non ho una prova. Semmai, l'algoritmo mostra che cercare di dimostrare un limite inferiore di è inutile, quindi potrebbe aiutare a rispondere alla tua domanda.O(n)O(n)O(n)O(n)O(n)

Vi presento due algoritmi, il primo è un semplice algoritmo con un tempo di esecuzione per Pop e il secondo con un tempo di esecuzione per Pop. Descrivo il primo principalmente per la sua semplicità, in modo che il secondo sia più facile da capire.O ( O(n)O(n)

Per essere più dettagliati: il primo non usa spazio aggiuntivo, ha un caso peggiore (e ammortizzato) Push e un caso peggiore (e ammortizzato) Pop, ma il comportamento del caso peggiore non è sempre attivato. Dal momento che non utilizza spazio aggiuntivo oltre le due code, è leggermente "migliore" della soluzione offerta da Ross Snider.O ( n )O(1)O(n)

Il secondo utilizza un singolo campo intero (quindi spazio extra), ha un caso peggiore (e ammortizzato) Push e un Pop ammortizzato. Il suo tempo di esecuzione è quindi significativamente migliore di quello dell'approccio "semplice", eppure utilizza un po 'di spazio in più.O ( 1 ) O ( O(1)O(1)O(n)

Il primo algoritmo

Abbiamo due code: la coda e la coda . sarà la nostra "coda di invio", mentre la sarà la coda già in "ordine di stack".s e c o n d f i r s t s e c o n dfirstsecondfirstsecond

  • La spinta viene eseguita semplicemente accodando il parametro sul .first
  • Il popping viene eseguito come segue. Se il è vuoto, dobbiamo semplicemente rimuovere il e restituire il risultato. Altrimenti, invertiamo , aggiungiamo tutto il al e scambiamo il e il . Abbiamo poi dequeue e restituire il risultato della dequeue.s e c o n d f i r s t s e c o n d f i r s t f i r s t s e c o n dfirstsecondfirstsecondfirstfirstsecondsecond

Codice C # per il primo algoritmo

Questo potrebbe essere abbastanza leggibile, anche se non hai mai visto C # prima. Se non sai cosa sono i generici, sostituisci semplicemente tutte le istanze di "T" con "stringa" nella tua mente, per una pila di stringhe.

public class Stack<T> {
    private Queue<T> first = new Queue<T>();
    private Queue<T> second = new Queue<T>();
    public void Push(T value) {
        first.Enqueue(value);
    }
    public T Pop() {
        if (first.Count == 0) {
            if (second.Count > 0)
                return second.Dequeue();
            else
                throw new InvalidOperationException("Empty stack.");
        } else {
            int nrOfItemsInFirst = first.Count;
            T[] reverser = new T[nrOfItemsInFirst];

            // Reverse first
            for (int i = 0; i < nrOfItemsInFirst; i++)
                reverser[i] = first.Dequeue();    
            for (int i = nrOfItemsInFirst - 1; i >= 0; i--)
                first.Enqueue(reverser[i]);

            // Append second to first
            while (second.Count > 0)
                first.Enqueue(second.Dequeue());

            // Swap first and second
            Queue<T> temp = first; first = second; second = temp;

            return second.Dequeue();
        }
    }
}

Analisi

Ovviamente Push funziona in tempo. Pop può toccare tutto dentro per e per un numero costante di volte, quindi abbiamo nel peggiore dei casi. L'algoritmo mostra questo comportamento (per esempio) se uno spinge elementi nello stack e quindi esegue ripetutamente una singola Push e una singola operazione Pop in successione.f i r s t s e c o n d O ( n ) nO(1)firstsecondO(n)n

Il secondo algoritmo

Abbiamo due code: la coda e la coda . sarà la nostra "coda di invio", mentre la sarà la coda già in "ordine di stack".s e c o n d f i r s t s e c o n dfirstsecondfirstsecond

Questa è una versione adattata del primo algoritmo, in cui non "rimescoliamo" immediatamente il contenuto del in . Invece, se contiene un numero sufficientemente piccolo di elementi rispetto al (vale a dire la radice quadrata del numero di elementi in ), riorganizziamo solo il in ordine di stack e non lo uniamo al .firstsecondfirstsecondsecondfirstsecond

  • La spinta viene comunque eseguita semplicemente accodando il parametro sul .first
  • Il popping viene eseguito come segue. Se il è vuoto, dobbiamo semplicemente rimuovere il e restituire il risultato. Altrimenti, riorganizziamo i contenuti di modo che siano nell'ordine dello stack. Se semplicemente dequeue e restituiamo il risultato. Altrimenti, aggiungiamo il al , scambiamo il e il , dequeue il e restituiamo il risultato.firstsecondfirst|first|<|second|firstsecondfirstfirstsecondsecond

Codice C # per il primo algoritmo

Questo potrebbe essere abbastanza leggibile, anche se non hai mai visto C # prima. Se non sai cosa sono i generici, sostituisci semplicemente tutte le istanze di "T" con "stringa" nella tua mente, per una pila di stringhe.

public class Stack<T> {
    private Queue<T> first = new Queue<T>();
    private Queue<T> second = new Queue<T>();
    int unsortedPart = 0;
    public void Push(T value) {
        unsortedPart++;
        first.Enqueue(value);
    }
    public T Pop() {
        if (first.Count == 0) {
            if (second.Count > 0)
                return second.Dequeue();
            else
                throw new InvalidOperationException("Empty stack.");
        } else {
            int nrOfItemsInFirst = first.Count;
            T[] reverser = new T[nrOfItemsInFirst];

            for (int i = nrOfItemsInFirst - unsortedPart - 1; i >= 0; i--)
                reverser[i] = first.Dequeue();

            for (int i = nrOfItemsInFirst - unsortedPart; i < nrOfItemsInFirst; i++)
                reverser[i] = first.Dequeue();

            for (int i = nrOfItemsInFirst - 1; i >= 0; i--)
                first.Enqueue(reverser[i]);

            unsortedPart = 0;
            if (first.Count * first.Count < second.Count)
                return first.Dequeue();
            else {
                while (second.Count > 0)
                    first.Enqueue(second.Dequeue());

                Queue<T> temp = first; first = second; second = temp;

                return second.Dequeue();
            }
        }
    }
}

Analisi

Ovviamente Push funziona in tempo.O(1)

Pop funziona in tempo ammortizzato . Esistono due casi: if , quindi spostiamo per nell'ordine dello stack in . Se , quindi abbiamo avuto almeno chiamate per Push. Quindi, possiamo solo toccare questo caso ogni chiamate a Push and Pop. Il tempo di esecuzione effettivo per questo caso è , quindi il tempo ammortizzato è .O(n)|first|<|second|firstO(|first|)=O(n)|first||second|n O(n)O( nnO(n)O(nn)=O(n)

Nota finale

È possibile eliminare la variabile aggiuntiva al costo di eseguire un'operazione Pop an , facendo in modo che Pop riorganizzi ad ogni chiamata invece che Push faccia tutto il lavoro.firstO(n)first


Ho modificato i primi paragrafi in modo che la mia risposta sia formulata come una risposta effettiva alla domanda.
Alex ten Brink,

6
Stai usando un array (reverser) per invertire! Non penso che ti sia permesso farlo.
Kaveh,

È vero, utilizzo lo spazio extra durante l'esecuzione dei metodi, ma ho pensato che sarebbe stato permesso: se si desidera implementare una coda usando due stack in modo semplice, è necessario invertire uno degli stack in un punto e fino a So che hai bisogno di spazio aggiuntivo per farlo, quindi poiché questa domanda è simile, ho pensato di usare spazio extra durante l'esecuzione di un metodo, a condizione che tu non usi spazio aggiuntivo tra le chiamate del metodo.
Alex ten Brink,

6
"se vuoi implementare una coda usando due pile in modo semplice, devi invertire una delle pile in un punto, e per quanto ne so hai bisogno di spazio extra per farlo" --- Non lo fai. C'è un modo per ottenere il costo ammortizzato di Enqueue su 3 e il costo ammortizzato di Dequeue su 1 (cioè entrambi O (1)) con una cella di memoria e due pile. La parte difficile è davvero la prova, non il design dell'algoritmo.
Aaron Sterling,

Dopo averci pensato un po 'di più, mi rendo conto che sto davvero tradendo e il mio commento precedente è davvero sbagliato. Ho trovato il modo di correggerlo: ho escogitato due algoritmi con gli stessi tempi di esecuzione dei due precedenti (anche se Push è ora l'operazione che richiede molto tempo e Pop è ora eseguita a tempo costante) senza utilizzare spazio aggiuntivo. Pubblicherò una nuova risposta dopo che avrò scritto tutto.
Alex ten Brink,

12

Dopo alcuni commenti sulla mia risposta precedente, mi è diventato chiaro che stavo più o meno tradendo: ho usato spazio extra ( spazio extra nel secondo algoritmo) durante l'esecuzione del mio metodo Pop.O(n)

Il seguente algoritmo non utilizza alcuno spazio aggiuntivo tra i metodi e solo spazio extra durante l'esecuzione di Push e Pop. Push ha un tempo di esecuzione ammortizzato e Pop ha un tempo di esecuzione peggiore (e ammortizzato).O ( O(1)O(1)O(n)O(1)

Nota per i moderatori: non sono del tutto sicuro che la mia decisione di rendere questa risposta separata sia corretta. Ho pensato che non avrei dovuto eliminare la mia risposta originale poiché potrebbe essere ancora rilevante per la domanda.

L'algoritmo

Abbiamo due code: la coda e la coda . sarà la nostra "cache", mentre la sarà la nostra "memoria" principale. Entrambe le code saranno sempre in "ordine stack". conterrà gli elementi nella parte superiore della pila e il conterrà gli elementi nella parte inferiore della pila. La dimensione del sarà sempre al massimo la radice quadrata del .firstsecondfirstsecondfirstsecondfirstsecond

  • Spinta è fatto da 'inserendo' il parametro all'inizio della coda come segue: abbiamo Enqueue il parametro da , e poi dequeue e ri-Enqueue tutti gli altri elementi di . In questo modo, il parametro termina all'inizio del .firstfirstfirst
  • Se il diventa più grande della radice quadrata del , accodiamo tutti gli elementi del sul uno per uno e quindi scambiamo il e il . In questo modo, gli elementi del (la parte superiore della pila) finiscono in testa al .firstsecondsecondfirstfirstsecondfirstsecond
  • Il pop viene eseguito dequeueando per e restituendo il risultato se il non è vuoto, oppure in caso di dequeueing per e restituendo il risultato.firstfirstsecond

Codice C # per il primo algoritmo

Questo codice dovrebbe essere abbastanza leggibile, anche se non hai mai visto C # prima. Se non sai cosa sono i generici, sostituisci semplicemente tutte le istanze di "T" con "stringa" nella tua mente, per una pila di stringhe.

public class Stack<T> {
    private Queue<T> first = new Queue<T>();
    private Queue<T> second = new Queue<T>();
    public void Push(T value) {
        // I'll explain what's happening in these comments. Assume we pushed
        // integers onto the stack in increasing order: ie, we pushed 1 first,
        // then 2, then 3 and so on.

        // Suppose our queues look like this:
        // first: in 5 6 out
        // second: in 1 2 3 4 out
        // Note they are both in stack order and first contains the top of
        // the stack.

        // Suppose value == 7:
        first.Enqueue(value);
        // first: in 7 5 6 out
        // second: in 1 2 3 4 out

        // We restore the stack order in first:
        for (int i = 0; i < first.Count - 1; i++)
            first.Enqueue(first.Dequeue());
        // first.Enqueue(first.Dequeue()); is executed twice for this example, the 
        // following happens:
        // first: in 6 7 5 out
        // second: in 1 2 3 4 out
        // first: in 5 6 7 out
        // second: in 1 2 3 4 out

        // first exeeded its capacity, so we merge first and second.
        if (first.Count * first.Count > second.Count) {
            while (second.Count > 0)
                first.Enqueue(second.Dequeue());
            // first: in 4 5 6 7 out
            // second: in 1 2 3 out
            // first: in 3 4 5 6 7 out
            // second: in 1 2 out
            // first: in 2 3 4 5 6 7 out
            // second: in 1 out
            // first: in 1 2 3 4 5 6 7 out
            // second: in out

            Queue<T> temp = first; first = second; second = temp;
            // first: in out
            // second: in 1 2 3 4 5 6 7 out
        }
    }
    public T Pop() {
        if (first.Count == 0) {
            if (second.Count > 0)
                return second.Dequeue();
            else
                throw new InvalidOperationException("Empty stack.");
        } else
            return first.Dequeue();
    }
}

Analisi

Ovviamente Pop lavora nel tempo nel peggiore dei casi.O(1)

Push funziona in tempo ammortizzato . Esistono due casi: if quindi Push impiega tempo. Se quindi Push impiega tempo, ma dopo questa operazione sarà vuota. Ci vorrà prima di ottenere nuovamente questo caso, quindi il tempo ammortizzato è .O(n)|first|<|second|O(n)|first||second|O(n)firstO(n)O(nn)=O(n)


sull'eliminazione di una risposta, dai un'occhiata a meta.cstheory.stackexchange.com/q/386/873 .
MS Dousti,

Non riesco a capire la linea first.Enqueue(first.Dequeue()). Hai sbagliato a scrivere qualcosa?
MS Dousti,

Grazie per il link, ho aggiornato la mia risposta originale di conseguenza. In secondo luogo, ho aggiunto molti commenti al mio codice che descrivono cosa sta succedendo durante l'esecuzione del mio algoritmo, spero che chiarisca qualsiasi confusione.
Alex ten Brink

per me l'algoritmo era più leggibile e più facile da capire prima della modifica.
Kaveh,

9

Dichiaro che abbiamo costo ammortizzato per operazione. L'algoritmo di Alex fornisce il limite superiore. Per dimostrare il limite inferiore, fornisco una sequenza nel caso peggiore di mosse PUSH e POP.Θ(N)

La sequenza del caso peggiore è composta da operazioni PUSH, seguite da operazioni PUSH e operazioni POP, di nuovo seguite da operazioni PUSH e operazioni POP, ecc. Che è:NNNNN

PUSHN(PUSHNPOPN)N

Considerare la situazione dopo le operazioni PUSH iniziali . Indipendentemente da come funziona l'algoritmo, almeno una delle code deve contenere almeno voci.NN/2

Consideriamo ora il compito di gestire le (prime serie di) PUSH e le operazioni POP. Qualsiasi tattica algoritmica deve rientrare in uno dei due casi:N

Nel primo caso, l'algoritmo utilizzerà entrambe le code. La più grande di queste code ha almeno voci al suo interno, quindi dobbiamo sostenere un costo di almeno operazioni al fine di recuperare anche un singolo elemento che ENQUEUE e successivamente DEQUEUE da questa coda più grande.N/2N/2

Nel secondo caso, l'algoritmo non utilizza entrambe le code. Ciò riduce il problema alla simulazione di uno stack con una singola coda. Anche se questa coda è inizialmente vuota, non possiamo fare di meglio che utilizzare la coda come un elenco circolare con accesso sequenziale, e sembra chiaro che dobbiamo usare almeno operazioni di coda in media per ognuna di le operazioni dello stack .N/22N

In entrambi i casi, abbiamo richiesto almeno tempo (operazioni in coda) per gestire le operazioni di stack . Poiché possiamo ripetere questo processo volte, abbiamo bisogno di volte per elaborare le operazioni dello stack in totale, dando un limite inferiore di tempo ammortizzato per operazione .N/22NNNN/23NΩ(N)


Jack lo ha modificato in modo che il numero di round (esponente tra parentesi) sia anziché come l'avevo. Questo perché ciò che avevo prima di dimostrare che non potevi ammortizzare sull'intera sequenza era "overkill", e puoi vederlo solo dalle iterazioni . Grazie Jack! NN
Shaun Harker,

Che dire del mix di questi due casi? Ad esempio, inseriamo le voci alternativamente in (quella con almeno voci) e (l'altra coda)? Immagino che questo modello costi di più, ma come discuterne? E nel secondo caso, penso che il costo medio (ammortizzato) per ciascuna delle operazioni dello stack sia almeno . Q1N / 2Q22nQ1N/2Q22nn4:1+2++n+n2n
hengxin,

Apparentemente la risposta di Peter contraddice questo limite inferiore?
Joe

@Joe Non credo che la risposta di Peter sia in contraddizione con questo limite inferiore poiché le prime N spinte non sono mai spuntate in questa sequenza. Qualsiasi procedura di shuffle richiede almeno O (N) tempo, quindi se deve aver luogo ogni `` fase '' (sequenza di operazioni ) continuiamo ha ammortizzato per la fase. In particolare, un simile algoritmo rientra nel "primo caso" della mia analisi. PUSHNPOPNO(N)
Shaun Harker,

@hengxin Il tuo commento mi ha fatto capire che non avevo espresso la mia argomentazione in modo chiaro come avrei voluto. L'ho modificato, quindi ora dovrebbe essere chiaro che il modello proposto è coperto dal primo caso. L'argomento è che se ENQUEUE anche un singolo elemento nella coda più grande, nel caso uno, dobbiamo richiedere operazioni per recuperarlo alla fine. O(N)
Shaun Harker,

6

Puoi ottenere un rallentamento (ammortizzato) se, dopo molti e nessun , quando vedi un esegui una sequenza di shuffles perfetti usando le due code. È stato dimostrato da Diaconis, Graham e Cantor in "The Mathematics of Perfect Shuffles" nel 1983 che con shuffles perfetti si può riordinare il "mazzo" in qualsiasi ordine. Pertanto, è possibile mantenere una coda come "coda di input" e una coda come "coda di output" (simile al caso di due stack) e quindi quando viene richiesto un e la coda di output è vuota, si esegue una sequenza di perfetta mescola per invertire la coda di input e memorizzarla nella coda di output.O(lgn)pushpoppopO(lgn)pop

L'unica domanda rimasta è se il particolare modello di shuffle perfetto sia abbastanza regolare da non richiedere più di memoria.O(1)

Per quanto ne so, questa è una nuova idea ...



Argh! Avrei dovuto cercare una domanda aggiornata o correlata. I documenti a cui ti sei collegato nella tua precedente risposta indicavano una relazione tra k pile e k + 1 pile. Questo trucco finisce per mettere la potenza di k code tra k e k + 1 pile? Se è così, è una specie di sidenote pulito. Ad ogni modo, grazie per avermi collegato alla tua risposta, quindi non ho perso troppo tempo a scrivere questo per un'altra sede.
Peter Boothe,

1

Senza usare spazio extra, forse usando una coda prioritaria e forzando ogni nuova spinta a darle una priorità maggiore rispetto alla precedente? Comunque non sarebbe O (1) però.


0

Non riesco a ottenere le code per implementare uno stack in tempo costante ammortizzato. Tuttavia, posso pensare a un modo per ottenere due code per implementare uno stack nel peggiore dei casi tempo lineare.

  • Utilizzando un bit di dati esterna, tenere un registro di cui coda è stato utilizzato lo scorso, la coda a sinistra o la coda di destra .AB
  • Ogni volta che viene eseguita un'operazione push, capovolgere il bit e inserire l'elemento nella coda che il bit ora delimita. Pop tutto dall'altra coda e spingerlo sulla coda corrente.
  • Un'operazione pop toglie la parte anteriore della coda corrente e non tocca il bit di stato esterno.

Naturalmente, possiamo aggiungere un altro bit di stato esterno che ci dice se l'ultima operazione è stata una spinta o un pop. Possiamo ritardare lo spostamento di tutto da una coda all'altra fino a quando non otteniamo due operazioni push di fila. Questo rende anche l'operazione pop leggermente più complicata. Questo ci dà O (1) complessità ammortizzata per l'operazione pop. Sfortunatamente la spinta rimane lineare.

Tutto ciò funziona perché ogni volta che viene eseguita un'operazione push, il nuovo elemento viene messo in testa a una coda vuota e l'intera coda viene aggiunta alla sua coda, invertendo efficacemente gli elementi.

Se vuoi ottenere operazioni ammortizzate a tempo costante, probabilmente dovrai fare qualcosa di più intelligente.


4
Sicuramente, posso usare una singola coda con la stessa complessità temporale peggiore e senza complicazioni, essenzialmente trattando la coda come un elenco circolare con un elemento di coda aggiuntivo che rappresenta la parte superiore dello stack.
Dave Clarke,

Sembra che puoi! Tuttavia, sembra che sia necessaria più di una coda classica per simulare uno stack in questo modo.
Ross Snider,

0

Esiste una soluzione banale, se la tua coda consente il caricamento frontale, che richiede solo una coda (o, più specificamente, deque). Forse questo è il tipo di coda che il corso TA nella domanda originale aveva in mente?

Senza consentire il caricamento frontale, ecco un'altra soluzione:

Questo algoritmo richiede due code e due puntatori, li chiameremo rispettivamente Q1, Q2, primario e secondario. All'inizializzazione Q1 e Q2 sono vuoti, i punti primari a Q1 e i punti secondari a Q2.

L'operazione PUSH è banale, consiste semplicemente:

*primary.enqueue(value);

L'operazione POP è leggermente più coinvolta; richiede lo spooling di tutti tranne l'ultimo elemento della coda primaria sulla coda secondaria, lo scambio dei puntatori e la restituzione dell'ultimo elemento rimanente dalla coda originale:

while(*primary.size() > 1)
{
    *secondary.enqueue(*primary.dequeue());
}

swap(primary, secondary);
return(*secondary.dequeue());

Non viene eseguito alcun controllo dei limiti e non è O (1).

Mentre sto scrivendo questo, vedo che questo potrebbe essere fatto con una singola coda usando un ciclo for al posto di un ciclo while, come ha fatto Alex. In entrambi i casi, l'operazione PUSH è O (1) e l'operazione POP diventa O (n).


Ecco un'altra soluzione che utilizza due code e un puntatore, chiamati Q1, Q2 e queue_p, rispettivamente:

All'inizializzazione, Q1 e Q2 sono vuoti e queue_p punta a Q1.

Ancora una volta, l'operazione PUSH è banale, ma richiede un ulteriore passaggio di puntamento queue_p sull'altra coda:

*queue_p.enqueue(value);
queue_p = (queue_p == &Q1) ? &Q2 : &Q1;

L'operazione POP è simile a prima, ma ora ci sono n / 2 elementi che devono essere ruotati attraverso la coda:

queue_p = (queue_p == &Q1) ? &Q2 : &Q1;
for(i=0, i<(*queue_p.size()-1, i++)
{
    *queue_p.enqueue(*queue_p.dequeue());
}
return(*queue_p.dequeue());

L'operazione PUSH è ancora O (1), ma ora l'operazione POP è O (n / 2).

Personalmente, per questo problema, preferisco l'idea di implementare una singola coda doppia (deque) e chiamarla stack quando vogliamo.


Il tuo secondo algoritmo è utile per capire quello più coinvolto di Alex.
hengxin,

0

La simulazione ottimale di uno stack con code richiede tempo per operazione indipendentemente dal fatto che: - il tempo sia il caso peggiore o ammortizzato, - simuliamo uno stack o qualsiasi numero fisso di stack con code totale, - è il numero di operazioni o il numero massimo simultaneo totale di elementi, - push (ma non pop) deve essere oppure no.kΘ(n1/k)

k
n
O(1)

In una direzione (cioè limite superiore), la esima coda avranno dimensioni , e insieme con code di numero inferiore, avrà la più recente oggetti per pila (tranne se una pila ha meno elementi; anche gli oggetti vengono spostati e non copiati). Manteniamo questo vincolo spostando gli elementi tra le code, eseguendo gli spostamenti in blocco in modo da ottenere tempo per elemento spostato nella coda e nella coda . Ogni oggetto è annotato dall'identità della sua pila e dalla sua altezza all'interno della pila; questo non è necessario se permettiamo che push sia (o se abbiamo solo uno stack e consentiamo il tempo pop ammortizzato).iΘ(ni/k)Θ(ni/k)O(1)i+1O(n1/k)i1Θ(n1/k)

Nella direzione opposta (ovvero limite inferiore), possiamo continuare ad aggiungere elementi fino a quando per alcuni , il elemento più recente è elementi lontano dalla fine di ogni coda che lo contiene, e quindi lo chiediamo e lo ripetiamo. Supponiamo che ciò non accada abbastanza. Quindi, un nuovo elemento deve in genere essere aggiunto a una coda di dimensioni . Per mantenere questa dimensione della coda, gli elementi devono essere spostati con frequenza in un'altra coda, la cui dimensione deve essere normalmente per consentire il recupero degli elementi abbastanza velocemente dopo lo spostamento . Ripetendo questo argomento, otteniamo code con una dimensione totale di (e crescente), come richiesto.m Ω ( m n 1 / k ) o ( n 1 / k ) Ω ( n 1 / k ) o ( n 2 / k ) k o ( n )mmΩ(mn1/k)o(n1/k)Ω(n1/k)o(n2/k)ko(n)

Inoltre, se uno stack deve essere svuotato tutto in una volta (prima di ricominciare ad aggiungere elementi), mi aspetto che la prestazione ammortizzata ottimale sia (uno stack che utilizza due o più code); questa prestazione può essere ottenuta usando (essenzialmente) unisci ordinamento.Θ(logn)


-3

Uno stack può essere implementato usando due code usando la seconda coda come buffer. Quando gli oggetti vengono inseriti nella pila, vengono aggiunti alla fine della coda. Ogni volta che un elemento viene visualizzato, gli elementi n - 1 della prima coda devono essere spostati sul secondo, mentre l'elemento rimanente viene restituito. classe pubblica QueueStack implementa IStack {private IQueue q1 = new Queue (); private IQueue q2 = new Queue (); public void push (E e) {q1.enqueue (e) // O (1)} public E pop (E e) {while (1 <q1.size ()) // O (n) {q2.enqueue ( q1.dequeue ()); } sw apQueues (); return q2.dequeue (); } p rivate void swapQueues () {IQueue Q = q2; q2 = q1; q1 = Q; }}


2
Hai perso la parte della domanda sul tempo ammortizzato O (1)?
David Eppstein,
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.