3つのスタックでキューを実装する方法は?


136

私はアルゴリズムの本(Robert SedgewickとKevin WayneによるAlgorithms、4th Edition)でこの質問に出くわしました。

3つのスタックを持つキュー。3つのスタックを持つキューを実装して、各キュー操作が一定(最悪の場合)のスタック操作をとるようにします。警告:難易度が高い。

2つのスタックでキューを作成する方法を知っていますが、3つのスタックで解決策を見つけることができません。何か案が ?

(ああ、これは宿題ではありません:))


30
ハノイの塔の変種だと思います。
ガンボ

14
@ジェイソン:この質問は、O(1)の償却時間を要求する一方で、この質問は各操作についてO(1)の最悪のケースを要求するため、重複ではありません。DuoSRXの2スタックソリューションは、すでにO(1)オペレーションあたりの償却時間です。
interjay

15
「警告:難易度が高い」と言ったとき、著者は確かに冗談ではありませんでした。
BoltClock

9
@Gumbo残念ながら、ハノイの塔の時間の複雑さは一定の時間にはほど遠いです!
prusswan 2011

12
注:テキスト内の質問が次のように更新されました。各キュー操作が一定(最悪の場合)のスタック操作を取るように、一定数のスタック ["3"ではない]でキューを実装します。警告:難易度が高い。algs4.cs.princeton.edu/13stacks-セクション1.3.43)。セッジウィック教授が最初の挑戦を認めたようです。
Mark Peters

回答:


44

概要

  • O(1)アルゴリズムは6つのスタックで知られています
  • O(1)アルゴリズムは3つのスタックで知られていますが、実際には追加の内部データ構造を持つことに対応する遅延評価を使用しているため、ソリューションを構成しません
  • セジウィックの近くの人々は、元の質問のすべての制約内で3スタックソリューションを認識していないことを確認しました

詳細

このリンクの背後には2つの実装があります。http//www.eecs.usma.edu/webs/people/okasaki/jfp95/index.html

それらの1つは、3つのスタックを持つO(1)ですが、実際には追加の中間データ構造(クロージャー)を作成する遅延実行を使用します。

それらのもう1つはO(1)ですが、SIXスタックを使用します。ただし、遅延実行なしで機能します。

更新:岡崎の論文はここにあります:http : //www.eecs.usma.edu/webs/people/okasaki/jfp95.psそして、彼は実際には遅延評価のあるO(1)バージョンに2スタックしか使用していないようです。問題は、それが本当に遅延評価に基づいているということです。問題は、遅延評価なしで3スタックアルゴリズムに変換できるかどうかです。

更新:別の関連するアルゴリズムは、第7回コンピューティングおよび組み合わせ論に関する年次会議で発行されたHolger Petersenによる論文「Stacks vs Deques」で説明されています。Googleブックスから記事を見つけることができます。225-226ページを確認してください。ただし、アルゴリズムは実際にはリアルタイムシミュレーションではなく、3つのスタックの両端キューの線形時間シミュレーションです。

gusbro:「@Leonelが先日言ったように、私はSedgewick教授に解決策を知っているか、何か間違いがあるかどうかを確認するのは公平だと思いました。それで私は彼に手紙を書きました。彼自身は、プリンストンの同僚から)なので、皆さんと共有したいのですが、彼は基本的に、3つのスタックを使用するアルゴリズムはなく、他の制約(遅延評価を使用しないなど)も知らないことを知っていたと述べました。ここで答えを見てすでに知っている6スタック。したがって、アルゴリズムを見つける(またはアルゴリズムが見つからないことを証明する)ための質問はまだ開かれていると思います。」


私はあなたのリンクにある論文やプログラムを飛んだだけです。しかし、私が正しく見ると、彼らはスタックを使用していないので、基本的なタイプとしてリストを使用しています。そして特に。これらのリストでは、ヘッダーと残りで構成されているため、基本的には私のソリューションに似ています(そして私は正しくないと思います)。
flolo 2011

1
こんにちは。実装は関数型言語で記述されており、ポインタが共有されていない限り、リストはスタックに対応します。6スタックバージョンは、6つの「プレーン」スタックを使用して実際に実装できます。2/3スタックバージョンの問題は、非表示のデータ構造(クロージャー)を使用することです。
アンティウイマ

6スタックソリューションがポインターを共有していないことを確認しますか?ではrotatefrontリストが両方に割り当てられているようですoldfrontf、これらはその後、個別に変更されています。
interjay

14
algs4.cs.princeton.edu/13stacksのソースマテリアルが変更されました:43. [「3」ではない)スタック数が一定のキューを実装して、各キュー操作が一定の(最悪の場合の)スタック数をとるようにします。操作。警告:難易度が高い。 チャレンジのタイトルには、「スタックが3つあるキュー」とありますが、これは:-)です。
マークピーターズ

3
@AnttiHuima 6スタックのリンクが死んでいる、これがどこかに存在するかどうか知っていますか?
Quentin Pradet

12

わかりました、これは本当に難しいです。私が思いつくことができる唯一の解決策は、小林丸テストのカークスの解決策を私に思い出させます(どういうわけか不正解):アイデアは、スタックのスタックを使用することです)。en / dequeue、push、popの操作を呼び出すと、次のようになります。

queue.new() : Stack1 = Stack.new(<Stack>);  
              Stack2 = Stack1;  

enqueue(element): Stack3 = Stack.new(<TypeOf(element)>); 
                  Stack3.push(element); 
                  Stack2.push(Stack3);
                  Stack3 = Stack.new(<Stack>);
                  Stack2.push(Stack3);
                  Stack2 = Stack3;                       

dequeue(): Stack3 = Stack1.pop(); 
           Stack1 = Stack1.pop();
           dequeue() = Stack1.pop()
           Stack1 = Stack3;

isEmtpy(): Stack1.isEmpty();

(StackX = StackYはコンテンツのコピーではなく、参照のコピーです。簡単に説明するためです。3つのスタックの配列を使用し、インデックスを介してそれらにアクセスすることもできます。そこでは、インデックス変数の値を変更するだけです。 )。すべては、stack-operation-termsのO(1)にあります。

そして、はい、私は暗黙のうちに3つ以上のスタックを持っているので、その議論の余地があることを知っていますが、おそらく他の人に良いアイデアを与えるでしょう。

編集:説明の例:

 | | | |3| | | |
 | | | |_| | | |
 | | |_____| | |
 | |         | |
 | |   |2|   | |
 | |   |_|   | |
 | |_________| |
 |             |
 |     |1|     |
 |     |_|     |
 |_____________|

Stack1を表示するために、少しのASCIIアートでここで試しました。

すべての要素は単一の要素スタックにラップされます(そのため、スタックのタイプセーフスタックしかありません)。

削除すると、最初の要素(here要素1と2を含むスタック)が最初にポップされます。次に、次の要素をポップして1をアンラップします。その後、最初にポップされたスタックが新しいStack1であると言います。もう少し機能的に言​​えば、これらは2つの要素のスタックによって実装されるリストであり、最初の要素はcdrで、最初と下の要素はcarです。他の2つはスタックを支援しています。

Espのトリッキーなのは挿入です。別の要素を追加するには、なんとかしてネストされたスタックを深く掘り下げる必要があります。Stack2が存在するのはそのためです。Stack2は常に最も内側のスタックです。追加とは単に要素を押し込んでから新しいStack2を押し込むことです(そのため、デキュー操作でStack2を操作することはできません)。


それがどのように機能するかを説明してもらえますか?「A」、「B」、「C」、「D」を押してから4回ポップすることをトレースするのでしょうか?
MAK

1
@Iceman:正しいStack2はありません。Stackは常にStack1の最も内側のスタックを参照するため、これらは失われません。したがって、Stack1でも暗黙的に存在します。
flolo

3
私はそれが不正行為だと同意します:-)。これは3つのスタックではなく、3つのスタック参照です。しかし、楽しい読書。
Mark Peters

1
その賢いスキームですが、私がそれを正しく理解すると、キューにn個の要素がプッシュされたときにn個のスタックが必要になります。質問は正確に3つのスタックを要求します。
MAK

2
@MAK:わかっています。それが、だまされたと明示的に述べた理由です(実際の解決策にも興味があるので、賞金に評判を費やしました)。しかし、少なくともprusswanのコメントには答えることができます。スタックの数は重要です。私のソリューションは実際に有効なので、必要なだけ使用できるからです。
flolo

4

私はそれができないことを証明する証明を試みます。


3つのスタックA、B、CによってシミュレートされるキューQがあるとします。

アサーション

  • ASRT0:=さらに、QがO(1)の操作{queue、dequeue}をシミュレートできると仮定します。これは、シミュレーションされるすべてのキュー/デキュー操作に対して、特定のシーケンスのスタックプッシュ/ポップが存在することを意味します。

  • 一般性を失うことなく、キュー操作は決定論的であると想定します。

Qのキューに入れられた要素には、キューの順序に基づいて1、2、...の番号が付けられ、Qのキューに入れられた最初の要素は1、2番目の要素は2などと定義されます。

定義する

  • Q(0) := Qに0要素がある場合のQの状態(したがって、A、B、Cに0要素)
  • Q(1) := 1つのキュー操作後のQ(およびA、B、C)の状態 Q(0)
  • Q(n) := n個のキュー操作後のQ(およびA、B、C)の状態 Q(0)

定義する

  • |Q(n)| :=Q(n)(したがって|Q(n)| = n)の要素数
  • A(n) := Qの状態がAのときのスタックAの状態 Q(n)
  • |A(n)| := の要素数 A(n)

スタックBとCの同様の定義。

ささいに、

|Q(n)| = |A(n)| + |B(n)| + |C(n)|

---

|Q(n)| 明らかにnに制限がない。

したがって、少なくともの一つ|A(n)||B(n)|または|C(n)|Nに無制限です。

WLOG1、スタックAがバインドされておらず、スタックBとCがバインドされているとします。

定義* B_u :=B C_u :=の上限* Cの上限*K := B_u + C_u + 1

WLOG2、そのようなnの場合|A(n)| > K、からK個の要素を選択しますQ(n)。これらの要素の1つがにあると仮定しA(n + x)ますx >= 0。つまり、実行されるキュー操作の数に関係なく、要素は常にスタックAにあります。

  • X := その要素

次に定義できます

  • Abv(n) :=A(n)Xを超えるスタック内のアイテムの数
  • Blo(n) :=スタック内のA(n)X未満の要素の数

    | A(n)| = Abv(n)+ Blo(n)

ASRT1 :=Xをデキューするために必要なポップの数はQ(n)少なくともAbv(n)

ASRT0)と(ASRT1)から、ASRT2 := Abv(n)制限付きである必要があります。

Abv(n)が無制限の場合、Xをからデキューするために20のデキューが必要なQ(n)場合、少なくともAbv(n)/20pop が必要になります。これは無制限です。20は任意の定数です。

したがって、

ASRT3 := Blo(n) = |A(n)| - Abv(n)

無制限でなければなりません。


WLOG3、の下部からK要素を選択できA(n)、そのうちの1つはA(n + x)すべてのx >= 0

X(n) := その要素、任意の与えられたn

ASRT4 := Abv(n) >= |A(n)| - K

要素がキューに入れられるたびにQ(n)...

WLOG4、BとCがすでに上限に達しているとします。上記の要素の上限にX(n)達したとします。次に、新しい要素がAに入ります。

WLOG5その結果、新しい要素が以下に入らなければならないと仮定しますX(n)

ASRT5 := 要素を下に置くために必要なポップの数 X(n) >= Abv(X(n))

から(ASRT4)Abv(n)nに無制限です。

したがって、要素を下に配置するために必要なポップの数X(n)は無制限です。


これは矛盾するASRT1ため、O(1)3つのスタックを持つキューをシミュレートすることはできません。


すなわち

少なくとも1つのスタックは無制限でなければなりません。

そのスタックにとどまっている要素の場合、その上の要素の数を制限する必要があります。そうしないと、その要素を削除するためのデキュ​​ー操作が制限されなくなります。

ただし、その上の要素の数が制限されている場合は、制限に達します。ある時点で、新しい要素がその下に入る必要があります。

スタックの最下位の要素の1つから常に古い要素を選択できるため、その上に無制限の数の要素が存在する可能性があります(無制限のスタックの無制限のサイズに基づく)。

その下に新しい要素を入力するには、その上に無限の数の要素があるため、新しい要素をその下に置くために無限の数のポップが必要です。

そして、この矛盾。


5つのWLOG(一般性を失うことなく)ステートメントがあります。ある意味で、それらは真実であると直感的に理解できます(ただし、5であることを考えると、多少時間がかかる場合があります)。一般性が失われていないという正式な証明は導き出すことができますが、非常に長いものです。省略されています。

私は、そのような省略がWLOGステートメントに問題を残す可能性があることを認めます。プログラマーのバグに対するパラノイアで、必要に応じてWLOGステートメントを確認してください。

3番目のスタックも無関係です。重要なのは、一連の制限付きスタックと一連の制限なしスタックがあることです。例に必要な最小値は2スタックです。もちろん、スタックの数は有限でなければなりません。

最後に、証明がないというのが正しければ、より簡単な帰納的証明が利用できるはずです。おそらく、各キューの後に何が起こるかに基づいています(キュー内のすべての要素のセットが与えられた場合、デキューの最悪のケースにどのように影響するかを追跡します)。


2
私は証明がそれらの仮定で機能すると思います-しかし、キューが空になるためにはすべてのスタックが空である必要があるかどうか、またはスタックのサイズの合計がキューのサイズと等しくなければならないかどうかはわかりません。
Mikeb

3
「WLOG1、スタックAがバインドされておらず、スタックBおよびCがバインドされているとします。」スタックの一部が制限されていると想定することはできません。スタックが無価値になるためです(それらはO(1)追加のストレージと同じです)。
interjay

3
些細なことはそれほど簡単ではない場合があります:| Q | = | A | + | B | + | C | Qのすべてのエントリについて正確に1つをA、B、またはCに追加すると想定した場合にのみ適切ですが、theerが常に2つのスタックに要素を2回または3つすべてに追加するアルゴリズムである可能性があります。そして、それがこのように機能する場合、WLOG1はもはや保持しません(たとえば、CがAのコピーを想像します(意味がないというわけではありませんが、おそらく異なる順序などのアルゴリズムが存在します...)
flolo

@floloと@mikeb:どちらも正しいです。| Q(n)| | A(n)| + | B(n)| + | C(n)|として定義する必要があります。そして| Q(n)| > = n。その後、証明はnで機能し、| Q(n)|である限り、大きいほど、結論が適用されます。
Dingfeng Quek

@interjay:制限なしスタックは3つ、制限なしスタックは3つ持つことができます。次に、「B_u + C_u + 1」の代わりに、証明は「1」を使用できます。基本的に、式は「境界付きスタックの上限の合計+ 1」を表すため、境界付きスタックの数は関係ありません。
Dingfeng Quek

3

注:これは、単一リンクリストを持つリアルタイム(最悪の場合は一定の時間)キューの機能実装へのコメントを意味します。私は評判のためコメントを追加できませんが、誰かがantti.huimaによって回答に追加されたコメントにこれを変更できればいいでしょう。繰り返しになりますが、コメントには少し時間がかかります。

@ antti.huima:リンクリストはスタックと同じではありません。

  • s1 =(1 2 3 4)---それぞれが右側のノードを指し、値1、2、3、4を保持する4つのノードを持つリンクリスト

  • s2 = popped(s1)--- s2は(2 3 4)になりました

この時点で、s2はpopped(s1)と同等で、スタックのように動作します。ただし、s1は引き続き参照できます。

  • s3 = popped(popped(s1))--- s3は(3 4)

まだs1を覗いて1を取得できますが、適切なスタック実装では、要素1はs1からなくなっています。

これは何を意味するのでしょうか?

  • s1はスタックの最上位への参照です
  • s2はスタックの2番目の要素への参照です
  • s3は3番目の...

作成された追加のリンクリストは、それぞれ参照/ポインタとして機能します。有限数のスタックはそれを行うことができません。

私が論文/コードで見るところから、アルゴリズムはすべて、リンクリストのこのプロパティを利用して参照を保持します。

編集:私が最初に読んだとき、リンクリストのこのプロパティを利用する2および3のリンクリストアルゴリズムのみを参照しています(それらはより単純に見えました)。これは、リンクされたリストが必ずしも同一ではないことを説明するためだけに、それらが該当する、または該当しないことを示すものではありません。私が空いているときに私は6のものを読みます。@Welbog:訂正ありがとうございます。


怠惰は、同様の方法でポインター機能をシミュレートすることもできます。


リンクリストを使用すると、別の問題が解決します。この戦略は、Lisp(または少なくともリンクリストからすべてを構築することを主張するLisp)にリアルタイムキューを実装するために使用できます。これは、O(1)操作時間と共有(不変)構造を持つ不変リストを設計するための優れた方法でもあります。


1
私はアンティの答えで他のアルゴリズムと話すことはできませんが、6スタックの一定時間ソリューション(eecs.usma.edu/webs/people/okasaki/jfp95/queue.hm.sml)はこのリストのプロパティを使用しません、java.util.Stackオブジェクトを使用してJavaで再実装したため。この機能が使用される唯一の場所は、不変スタックを一定の時間で「複製」できる最適化です。これは、基本Javaスタックが実行できない(しかしJavaで実装できる)変更可能な構造であるためです。
ウェルボグ2011

計算の複雑さを軽減しない最適化であれば、結論に影響を与えるべきではありません。ようやく解決策を手に入れて、今それを検証できることを嬉しく思います。しかし、私はSMLを読むのが好きではありません。あなたのJavaコードを共有することを心に留めていますか?(:
Dingfeng Quek 2011

残念ながら3つではなく6つのスタックを使用するため、これは最終的なソリューションではありません。ただし、6つのスタックが最小のソリューションであることを証明できる可能性があります...
Welbog

@ウェルボッグ!6スタックの実装を共有できますか?:)それを見るのはかっこいいです:)
Antti

1

2つのスタックを使用して、一定の償却期間でそれを行うことができます。

------------- --------------
            | |
------------- --------------

追加する側O(1)と削除O(1)する側は、取得する側が空でO(n)ない場合(その他のスタックを2つに分割する場合)です。

トリックは、O(n)操作が毎回のみ行われるO(n)ことを確認することです(たとえば、半分に分割した場合)。したがって、操作の平均時間はO(1)+O(n)/O(n) = O(1)です。

これは問題のように繋がる可能性がありますが、配列ベースのスタック(最速)で命令型言語を使用している場合、とにかく一定の時間だけを償却することになります。


元の質問について:スタックを分割するには、実際には追加の中間スタックが必要です。これが、タスクに3つのスタックが含まれている理由かもしれません。
Thomas Ahle、2011
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.