私はアルゴリズムの本(Robert SedgewickとKevin WayneによるAlgorithms、4th Edition)でこの質問に出くわしました。
3つのスタックを持つキュー。3つのスタックを持つキューを実装して、各キュー操作が一定(最悪の場合)のスタック操作をとるようにします。警告:難易度が高い。
2つのスタックでキューを作成する方法を知っていますが、3つのスタックで解決策を見つけることができません。何か案が ?
(ああ、これは宿題ではありません:))
私はアルゴリズムの本(Robert SedgewickとKevin WayneによるAlgorithms、4th Edition)でこの質問に出くわしました。
3つのスタックを持つキュー。3つのスタックを持つキューを実装して、各キュー操作が一定(最悪の場合)のスタック操作をとるようにします。警告:難易度が高い。
2つのスタックでキューを作成する方法を知っていますが、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スタック。したがって、アルゴリズムを見つける(またはアルゴリズムが見つからないことを証明する)ための質問はまだ開かれていると思います。」
rotate
、front
リストが両方に割り当てられているようですoldfront
とf
、これらはその後、個別に変更されています。
わかりました、これは本当に難しいです。私が思いつくことができる唯一の解決策は、小林丸テストのカークスの解決策を私に思い出させます(どういうわけか不正解):アイデアは、スタックのスタックを使用することです)。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を操作することはできません)。
私はそれができないことを証明する証明を試みます。
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)/20
pop が必要になります。これは無制限です。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スタックです。もちろん、スタックの数は有限でなければなりません。
最後に、証明がないというのが正しければ、より簡単な帰納的証明が利用できるはずです。おそらく、各キューの後に何が起こるかに基づいています(キュー内のすべての要素のセットが与えられた場合、デキューの最悪のケースにどのように影響するかを追跡します)。
注:これは、単一リンクリストを持つリアルタイム(最悪の場合は一定の時間)キューの機能実装へのコメントを意味します。私は評判のためコメントを追加できませんが、誰かが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は引き続き参照できます。
まだs1を覗いて1を取得できますが、適切なスタック実装では、要素1はs1からなくなっています。
これは何を意味するのでしょうか?
作成された追加のリンクリストは、それぞれ参照/ポインタとして機能します。有限数のスタックはそれを行うことができません。
私が論文/コードで見るところから、アルゴリズムはすべて、リンクリストのこのプロパティを利用して参照を保持します。
編集:私が最初に読んだとき、リンクリストのこのプロパティを利用する2および3のリンクリストアルゴリズムのみを参照しています(それらはより単純に見えました)。これは、リンクされたリストが必ずしも同一ではないことを説明するためだけに、それらが該当する、または該当しないことを示すものではありません。私が空いているときに私は6のものを読みます。@Welbog:訂正ありがとうございます。
怠惰は、同様の方法でポインター機能をシミュレートすることもできます。
リンクリストを使用すると、別の問題が解決します。この戦略は、Lisp(または少なくともリンクリストからすべてを構築することを主張するLisp)にリアルタイムキューを実装するために使用できます。これは、O(1)操作時間と共有(不変)構造を持つ不変リストを設計するための優れた方法でもあります。
java.util.Stack
オブジェクトを使用してJavaで再実装したため。この機能が使用される唯一の場所は、不変スタックを一定の時間で「複製」できる最適化です。これは、基本Javaスタックが実行できない(しかしJavaで実装できる)変更可能な構造であるためです。
2つのスタックを使用して、一定の償却期間でそれを行うことができます。
------------- --------------
| |
------------- --------------
追加する側O(1)
と削除O(1)
する側は、取得する側が空でO(n)
ない場合(その他のスタックを2つに分割する場合)です。
トリックは、O(n)
操作が毎回のみ行われるO(n)
ことを確認することです(たとえば、半分に分割した場合)。したがって、操作の平均時間はO(1)+O(n)/O(n) = O(1)
です。
これは問題のように繋がる可能性がありますが、配列ベースのスタック(最速)で命令型言語を使用している場合、とにかく一定の時間だけを償却することになります。