Hintergrund
Vor einigen Jahren, als ich ein Student war, erhielten wir eine Hausaufgabe über die amortisierte Analyse. Ich konnte eines der Probleme nicht lösen. Ich hatte es in comp.theory gefragt , aber es kam kein zufriedenstellendes Ergebnis zustande. Ich erinnere mich an den Kurs, in dem TA auf etwas bestand, das er nicht beweisen konnte, und sagte, er habe den Beweis vergessen, und ... [weißt du was].
Heute habe ich mich an das Problem erinnert. Ich war immer noch gespannt darauf, also hier ist es ...
Die Frage
Ist es möglich, einen Stack mit zwei Warteschlangen zu implementieren , sodass sowohl PUSH- als auch POP- Operationen in der amortisierten Zeit O (1) ausgeführt werden ? Wenn ja, können Sie mir sagen, wie?
Anmerkung: Die Situation ist recht einfach, wenn wir eine Warteschlange mit zwei Stapeln implementieren möchten (mit entsprechenden Operationen ENQUEUE & DEQUEUE ). Bitte beachten Sie den Unterschied.
PS: Das obige Problem ist nicht die Hausaufgabe selbst. Die Hausaufgaben erforderten keine Untergrenzen; Nur eine Implementierung und die Laufzeitanalyse.
Antworten:
Ich habe keine wirkliche Antwort, aber hier sind einige Beweise dafür, dass das Problem offen ist:
Es wird in Ming Li, Luc Longpré und Paul MB Vitányi, "Die Macht der Warteschlange", Structures 1986, nicht erwähnt, in dem mehrere andere eng verwandte Simulationen betrachtet werden
Es wird in Martin Hühne nicht erwähnt, "Über die Macht mehrerer Warteschlangen", Theor. Comp. Sci. 1993 ein Nachfolgepapier.
Es ist nicht in Holger Petersen, "Stacks versus Deques", COCOON 2001 erwähnt.
Burton Rosenberg, "Schnelle nicht deterministische Erkennung kontextfreier Sprachen mit zwei Warteschlangen", Inform. Proc. Lette. 1998 gibt einen O (n log n) Zwei-Warteschlangen-Algorithmus zum Erkennen einer CFL unter Verwendung von zwei Warteschlangen an. Ein nicht deterministischer Pushdown-Automat kann jedoch CFLs in linearer Zeit erkennen. Wenn es also eine Simulation eines Stacks mit zwei Warteschlangen gegeben hätte, die schneller als O (log n) pro Operation sind, hätten Rosenberg und seine Schiedsrichter davon wissen müssen.
quelle
Die Antwort unten lautet "Betrug", da die Operationen selbst zwar keinen Abstand zwischen den Operationen belegen, aber mehr als Leerzeichen verwenden können. An anderer Stelle in diesem Thread finden Sie eine Antwort, bei der dieses Problem nicht auftritt.O ( 1 )
Obwohl ich keine Antwort auf Ihre genaue Frage habe, habe ich einen Algorithmus gefunden, der in Zeit anstelle vonO(n). Ich glaube, das ist eng, obwohl ich keinen Beweis habe. Wenn überhaupt, zeigt der Algorithmus, dass der Versuch, eine Untergrenze vonO(n)zu beweisen,vergeblich ist. Daher kann er bei der Beantwortung Ihrer Frage hilfreich sein.O ( n--√) O ( n ) O ( n )
Ich präsentiere zwei Algorithmen, der erste ist ein einfacher Algorithmus mit einer Laufzeit für Pop und der zweite mit einem O ( √)O ( n ) Laufzeit für Pop. Ich beschreibe den ersten hauptsächlich wegen seiner Einfachheit, damit der zweite leichter zu verstehen ist.O ( n--√)
Um genauer zu sein: Der erste verwendet keinen zusätzlichen Platz, hat einen Worst-Case- (und amortisierten) Push und einen O ( n ) Worst-Case- (und amortisierten) Pop, aber das Worst-Case-Verhalten wird nicht immer ausgelöst. Da es keinen zusätzlichen Platz außerhalb der beiden Warteschlangen beansprucht, ist es etwas "besser" als die von Ross Snider angebotene Lösung.O ( 1 ) O ( n )
Die zweite verwendet ein einzelnes ganzzahliges Feld (also zusätzliches Leerzeichen), hat einen O ( 1 ) Worst Case (und amortisiert) Push und einen O ( √)O ( 1 ) O ( 1 ) Pop abgeschrieben. Die Laufzeit ist daher erheblich besser als die des "einfachen" Ansatzes, benötigt jedoch zusätzlichen Platz.O ( n--√)
Der erste Algorithmus
Wir haben zwei Warteschlangen: Warteschlange und Warteschlangen s e c o n d . f i r s t wird unsere 'Push - Warteschlange' sein, während s e c o n d die Warteschlange bereits in 'Stapelordnung' sein wird.fi r s t s e c o n d fi r s t s e c o n d
C # -Code für den ersten Algorithmus
Dies könnte durchaus lesbar sein, auch wenn Sie noch nie C # gesehen haben. Wenn Sie nicht wissen, was Generika sind, ersetzen Sie einfach alle Instanzen von 'T' durch 'string' in Ihrem Kopf, um einen Stapel von Strings zu erhalten.
Analyse
Offensichtlich funktioniert Push in Zeit. Pop kann alles im Innern berührt f i r s t und s e c o n d eine konstante Menge an Zeit, so haben wir O ( n ) im schlechtesten Fall. Der Algorithmus zeigt dieses Verhalten (zum Beispiel), wenn man n drücktO ( 1 ) fi r s t s e c o n d O ( n ) n Elemente auf den Stapel und dann wiederholt nacheinander einen einzigen Push- und einen einzelnen Pop-Vorgang durchführt.
Der zweite Algorithmus
Wir haben zwei Warteschlangen: Warteschlange und Warteschlangen s e c o n d . f i r s t wird unsere 'Push - Warteschlange' sein, während s e c o n dfi r s t s e c o n d fi r s t s e c o n d die Warteschlange bereits in 'Stapelordnung' sein wird.
Dies ist eine angepasste Version des ersten Algorithmus, bei der der Inhalt von nicht sofort in s e c o n d 'gemischt' wird . Stattdessen, wenn f i r s t eine ausreichend kleine Anzahl von Elementen enthält im Vergleich zu s e c o n d (nämlich der Quadratwurzel der Anzahl der Elemente in s e c o n d ), reorganisieren wir nur f i r s t in Stapelreihenfolge und verschmelze es nicht mitfi r s t s e c o n d fi r s t s e c o n d s e c o n d fi r s t .s e c o n d
C # -Code für den ersten Algorithmus
Dies könnte durchaus lesbar sein, auch wenn Sie noch nie C # gesehen haben. Wenn Sie nicht wissen, was Generika sind, ersetzen Sie einfach alle Instanzen von 'T' durch 'string' in Ihrem Kopf, um einen Stapel von Strings zu erhalten.
Analyse
Offensichtlich funktioniert Push in Zeit.O ( 1 )
Pop arbeitet in Amortisierte Zeit Es gibt zwei Fälle: if| first| < √O(n−−√) Und mische dann wirfirstin Stapelordnung inO(|first|)=O(√|first|<|second|−−−−−−−√ first zeit. Wenn| first| ≥ √O(|first|)=O(n−−√) , dann müssen wir mindestens√gehabt haben|first|≥|second|−−−−−−−√ ruft nach Push. Daher können wir diesen Fall nur alle √ treffenn−−√ Anrufe zu Push and Pop. Die tatsächliche Laufzeit für diesen Fall istO(n), die amortisierte Zeit ist alsoO( n)n−−√ O(n) .O(nn√)=O(n−−√)
Schlussbemerkung
Es ist möglich, die zusätzliche Variable zu eliminieren, um Pop zu einem Betrieb, indem Pop reorganisiertfirstbei jedem Aufruf anstelle von Pushdie ganzen Arbeit.O(n−−√) first
quelle
Nach einigen Kommentaren zu meiner vorherigen Antwort wurde mir klar, dass ich mehr oder weniger betrogen habe: Ich habe zusätzlichen Platz (O(n−−√) zusätzlicher Platz im zweiten Algorithmus) während der Ausführung meiner Pop-Methode.
Der folgende Algorithmus verwendet keinen zusätzlichen Abstand zwischen Methoden und nur zusätzlichen Abstand während der Ausführung von Push und Pop. Push hat ein O ( √O(1) Laufzeit abgeschrieben und Pop hat einO(1)O(n−−√) O(1) Worst-Case-Laufzeit (und amortisierte Laufzeit).
Hinweis für Moderatoren: Ich bin mir nicht ganz sicher, ob meine Entscheidung, eine separate Antwort zu erstellen, richtig ist. Ich dachte, ich sollte meine ursprüngliche Antwort nicht löschen, da sie für die Frage immer noch relevant sein könnte.
Der Algorithmus
Wir haben zwei Warteschlangen: Warteschlange und Warteschlangen s e c o n d . f i r s t wird unsere 'Cache' sein, während s e c o n d unsere 'Lagerung' sein wird. Beide Warteschlangen werden immer in "Stapelreihenfolge" sein. f i r s t enthält die Elemente am oberen Rand des Stapels und s e c o n d enthält die Elemente am unteren Rand des Stapels. Die Größe von f i sfirst second first second first second wird immer höchstens die Quadratwurzel von s e c o n d sein .first second
C # -Code für den ersten Algorithmus
Dieser Code sollte gut lesbar sein, auch wenn Sie noch nie C # gesehen haben. Wenn Sie nicht wissen, was Generika sind, ersetzen Sie einfach alle Instanzen von 'T' durch 'string' in Ihrem Kopf, um einen Stapel von Strings zu erhalten.
Analyse
Offensichtlich arbeitet Pop im schlimmsten Fall in -Zeit.O(1)
Push funktioniert in Amortisierte Zeit Es gibt zwei Fälle: if| first| < √O(n−−√) dann nimmt PushO(√|first|<|second|−−−−−−−√ zeit. Wenn| first| ≥ √O(n−−√) |first|≥|second|−−−−−−−√ O(n) first O(n−−√) O(nn√)=O(n−−√)
quelle
first.Enqueue(first.Dequeue())
. Hast du etwas falsch geschrieben?quelle
Soweit ich weiß, ist dies eine neue Idee ...
quelle
Ohne zusätzlichen Speicherplatz zu belegen, möglicherweise eine priorisierte Warteschlange verwenden und jeden neuen Push zwingen, dieser eine höhere Priorität als der vorherigen zuzuweisen? Wäre trotzdem nicht O (1).
quelle
Ich kann die Warteschlangen nicht dazu bringen, einen Stack in amortisierter konstanter Zeit zu implementieren. Ich kann mir jedoch eine Möglichkeit vorstellen, zwei Warteschlangen zum Implementieren eines Stacks im schlimmsten Fall in linearer Zeit zu erstellen.
Natürlich können wir einen weiteren externen Status hinzufügen, der uns sagt, ob die letzte Operation ein Push oder ein Pop war. Wir können die Verschiebung aller Vorgänge von einer Warteschlange in die andere verzögern, bis wir zwei Push-Vorgänge hintereinander erhalten. Dies macht auch die Pop-Operation etwas komplizierter. Dies gibt uns O (1) amortisierte Komplexität für die Pop-Operation. Push bleibt leider linear.
All dies funktioniert, da jedes Mal, wenn eine Push-Operation ausgeführt wird, das neue Element an den Anfang einer leeren Warteschlange gestellt wird und die volle Warteschlange am hinteren Ende hinzugefügt wird, wodurch die Elemente effektiv umgekehrt werden.
Wenn Sie dauerhafte Operationen amortisieren möchten, müssen Sie wahrscheinlich etwas schlaueres tun.
quelle
Es gibt eine triviale Lösung, wenn Ihre Warteschlange Frontloading zulässt, das nur eine Warteschlange (oder genauer gesagt Deque) erfordert. Vielleicht ist dies die Art von Warteschlange, die der Kurs TA in der ursprünglichen Frage im Auge hatte?
Hier ist eine andere Lösung, ohne Frontloading zuzulassen:
Dieser Algorithmus erfordert zwei Warteschlangen und zwei Zeiger. Wir nennen sie Q1, Q2, primär und sekundär. Bei der Initialisierung sind Q1 und Q2 leer, Primärpunkte zu Q1 und Sekundärpunkte zu Q2.
Die PUSH-Operation ist trivial und besteht lediglich aus:
Die POP-Operation ist etwas komplizierter. Sie müssen alle Elemente außer dem letzten Element der primären Warteschlange in die sekundäre Warteschlange spoolen, die Zeiger austauschen und das letzte verbleibende Element aus der ursprünglichen Warteschlange zurückgeben:
Es wird keine Grenzüberprüfung durchgeführt, und es ist nicht O (1).
Während ich dies schreibe, sehe ich, dass dies mit einer einzelnen Warteschlange unter Verwendung einer for-Schleife anstelle einer while-Schleife erfolgen kann, wie dies Alex getan hat. In beiden Fällen ist die PUSH-Operation O (1) und die POP-Operation wird O (n).
Hier ist eine andere Lösung, die zwei Warteschlangen und einen Zeiger mit den Bezeichnungen Q1, Q2 und queue_p verwendet:
Bei der Initialisierung sind Q1 und Q2 leer und queue_p zeigt auf Q1.
Auch hier ist die PUSH-Operation trivial, erfordert jedoch einen zusätzlichen Schritt, um queue_p auf die andere Warteschlange zu verweisen:
Die POP-Operation ähnelt der vorherigen, aber jetzt müssen n / 2 Elemente in der Warteschlange gedreht werden:
Die PUSH-Operation ist immer noch O (1), aber jetzt ist die POP-Operation O (n / 2).
Persönlich bevorzuge ich für dieses Problem die Idee, eine einfache, doppelte Warteschlange (Deque) zu implementieren und sie als Stack zu bezeichnen, wenn wir dies möchten.
quelle
quelle
Ein Stapel kann unter Verwendung von zwei Warteschlangen implementiert werden, indem die zweite Warteschlange als Puffer verwendet wird. Wenn Elemente auf den Stapel verschoben werden, werden sie am Ende der Warteschlange hinzugefügt. Bei jedem Auftauchen eines Elements müssen die n - 1 Elemente der ersten Warteschlange in die zweite verschoben werden, während das verbleibende Element zurückgegeben wird. öffentliche Klasse QueueStack implementiert IStack {private IQueue q1 = new Queue (); private IQueue q2 = neue Queue (); öffentlicher nichtiger Push (E e) {q1.enqueue (e) // O (1)} öffentlicher 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; }}
quelle