1つのスタック、2つのキュー


59

バックグラウンド

数年前、私が学部生だったとき、私たちは償却分析に関する宿題を与えられました。問題の1つを解決できませんでした。comp.theoryで尋ねましたが、満足のいく結果は得られませんでした。コースTAが彼が証明できないことを主張し、証拠を忘れたと言ったのを覚えています。

今日、私は問題を思い出した。私はまだ知りたいと思っていたので、ここにあります...

質問

2つのキューを使用してスタックを実装することは可能ですか?プッシュPOPの両方の操作が償却時間O(1)で実行さますか?はいの場合、どのように教えてもらえますか?

注意:私たちは実装したい場合は、状況は非常に簡単であるキューをして2つのスタック(対応する操作とENQUEUEDEQUEUE)。違いを観察してください。

PS:上記の問題は宿題そのものではありません。宿題は下限を必要としませんでした。実装と実行時間分析だけです。


2
2つのキュー(O(1)またはO(log n))以外の限られたスペースしか使用できないと思います。長い入力ストリームの順序を逆にする方法がないため、私には不可能に思えます。しかし、これは厳密な主張をすることができない限り、もちろん証拠ではありません…。
伊藤剛

@剛:限られたスペースの仮定についてはあなたは正しい。そして、はい、それは私がその(頑固な)TAに言ったことでしたが、彼は拒否しました:(
MS Dousti

2
@Tsuyoshi:一般にスペースの限界を想定する必要はないと思いますが、2つのキュー以外の場所でスタックからプッシュおよびポップされたオブジェクトを格納することは許可されていないと想定するだけです定数の変数)。
カベ

私の意見で@SadeqDousti、あなたはキューのリンクリストの実装を使用し、常に「スタック」のトップを指すようにいくつかのポインタを使用した場合、これは可能であろう唯一の方法がある
チャールズ・アディス

2
TAは実際には「2つのスタックを使用してキューを実装する」ことを望んでいたように聞こえますが、これは実際には「O(1)償却時間」で正確に可能です。
トーマスエール

回答:


45

実際の答えはありませんが、問題が未解決であることを示すいくつかの証拠があります。

  • Ming Li、LucLongpré、Paul MBVitányi、「キューの力」、Structures 1986には言及されていません。

  • それは、MartinHühne、「いくつかのキューの力について」、Theorでは言及されていません。比較 科学 1993、後続の論文。

  • Holger Petersen、「Stacks vs Deques」、COCOON 2001では言及されていません。

  • バートンローゼンバーグ、「2つのキューを使用したコンテキストフリー言語の高速非決定論的認識」、Inform。手続き レット。1998年、2つのキューを使用してCFLを認識するためのO(n log n)2キューアルゴリズムを提供します。しかし、非決定性プッシュダウンオートマトンは、線形時間でCFLを認識できます。したがって、操作ごとにO(log n)よりも高速な2つのキューを持つスタックのシミュレーションがあった場合、Rosenbergと彼のレフェリーはそれについて知っているはずです。


4
優れた参照のために+1。ただし、いくつかの技術があります。最初の論文のように、2つのキューを使用して1つのスタックをシミュレートする問題を考慮していない論文もあります(要約から言えます)。また、償却原価ではなく、最悪の場合の分析を検討する人もいます。
MS Dousti

13

以下の答えは「不正行為」です。オペレーション間でスペースを使用しませんが、オペレーション自体は以上のスペースを使用できます。この問題のない答えについては、このスレッドの他の場所を参照してください。O(1)

あなたの正確な質問に対する答えはありませんが、Oで機能するアルゴリズムを見つけましたOn)の代わりの時間。証拠はありませんが、これはきついと思います。どちらかといえば、アルゴリズムはOn)の下限を証明しようとすることは無益であることを示しているので、あなたの質問に答えるのに役立つかもしれません。O(n)O(n)O(n)

私は2つのアルゴリズムを提示します。1つはPopの実行時間を持つ単純なアルゴリズムで、2つ目はO O(n)ポップの実行時間。2番目の方がわかりやすいように、主にその単純さのために最初の方を説明します。O(n)

詳細を説明すると、最初は追加のスペースを使用せず、最悪ケース(および償却)プッシュとO n 最悪ケース(および償却)ポップがありますが、最悪の場合の動作は常にトリガーされません。2つのキューを超える追加スペースを使用しないため、Ross Sniderが提供するソリューションよりもわずかに「優れています」。O(1)O(n)

2番目は単一の整数フィールドを使用するため(余分なスペース)、O 1 ワーストケース(および償却)プッシュとO O(1)O(1)償却されたポップ。したがって、実行時間は「単純な」アプローチよりもはるかに優れていますが、余分なスペースを使用します。O(n)

最初のアルゴリズム

キューとキューs e c o n dの 2つのキューがあります。f i r s tは「プッシュキュー」になり、s e c o n dはすでに「スタック順」にあるキューになります。firstsecondfirstsecond

  • プッシュは、単にパラメーターをエンキューすることで行われます。first
  • ポッピングは次のように行われます。もし空である、我々は単にデキュー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 TSのE部のC 、O 、N 、D。次に、s e c oをデキューしますfirstsecondfirstsecondfirstfirstsecondおよびデキューの結果を返します。second

最初のアルゴリズムのC#コード

C#を見たことがない場合でも、これは非常に読みやすいはずです。ジェネリックが何なのかわからない場合は、頭の中で「T」のすべてのインスタンスを「string」に置き換えて、文字列のスタックにします。

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();
        }
    }
}

分析

明らかに、Pushは時間で動作します。Popは、f i r s tおよびs e c o n d内のすべてに一定の回数接触する可能性があるため、最悪の場合にはO n があります。アルゴリズムは、n個の要素をスタックにプッシュしてから、単一のプッシュ操作と単一のポップ操作を連続して繰り返し実行すると、この動作を示します(たとえば)。O(1)firstsecondO(n)n

2番目のアルゴリズム

キューとキューs e c o n dの 2つのキューがあります。f i r s tは「プッシュキュー」になり、s e c o n dはすでに「スタック順」にあるキューになります。firstsecondfirstsecond

これは、最初のアルゴリズムの適応バージョンであり、内容をすぐにs e c o n dに「シャッフル」しません。場合代わりに、F iは、rは、SをTはに比べ要素の十分に小さな数を含んでいるS 、E 、C 、O 、N個のD(の要素の数の、すなわち平方根のS EのCのO N D)、我々は唯一の再編成、F iはrのS Tをスタック順に並べ、マージしないfirstsecondfirstsecondsecondfirstsecond

  • プッシュは、単にパラメーターをエンキューするだけで行われます。first
  • ポッピングは次のように行われます。もし空である、我々は単にデキューS E C 、O 、N 、D、結果を返します。それ以外の場合、f i r s tの内容を再編成して、スタック順になります。もし| f i r s t | < firstsecondfirstfirstを単にデキューし、結果を返します。そうでなければ、我々は、追加のSEのCの入出力のnDへのFiはrのSTは、スワップfはiはrのSTSのE部のC、O、NDデキュー、S、E、C、O、N個のD及び結果を返すを。|first|<|second|firstsecondfirstfirstsecondsecond

最初のアルゴリズムのC#コード

C#を見たことがない場合でも、これは非常に読みやすいはずです。ジェネリックが何なのかわからない場合は、頭の中で「T」のすべてのインスタンスを「string」に置き換えて、文字列のスタックにします。

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();
            }
        }
    }
}

分析

明らかに、Pushは時間で動作します。O(1)

ポップはOで動作します償却時間。次の2つの場合がありますfirst| <O(n)、その後、firstO|first|=O|first|<|second|first時間。もし| first| O(|first|)=O(n)、その後、少なくとも|first||second|はプッシュを要求します。したがって、このケースはすべてnプッシュとポップを呼び出します。この場合の実際の実行時間はOnなので、償却時間はO nnO(n)O(nn)=O(n)

最後のメモ

PopをOにすることで余分な変数を削除することが可能です操作。Pushにすべての作業を行わせる代わりに、呼び出しごとにfirstを再編成させます。O(n)first


最初の段落を編集したので、私の答えは質問に対する実際の答えとして定式化されました。
アレックス10ブリンク

6
あなたは反転のために配列(リバーサー)を使用しています!あなたがこれを行うことは許されていないと思います。
カベ

確かに、メソッドの実行中に余分なスペースを使用しますが、それは許可されると考えました:簡単な方法で2つのスタックを使用してキューを実装する場合は、1つの時点でスタックの1つを反転する必要があり、私はあなたがそれを行うために余分なスペースが必要であることを知っていますので、この質問は似ているので、メソッド呼び出しの間に追加のスペースを使用しない限り、メソッドの実行中に余分なスペースを使用することができます。
アレックス10ブリンク

6
「単純な方法で2つのスタックを使用してキューを実装する場合、1つの時点でスタックの1つをリバースする必要がありますが、それを行うには余分なスペースが必要であることがわかっています」---しません。1つのメモリセルと2つのスタックで、エンキューの償却コストを3に、デキューの償却コストを1(つまり、O(1))にする方法があります。難しい部分は実際には証明であり、アルゴリズムの設計ではありません。
アーロンスターリング

それについてさらに考えた後、私は実際に不正行為であり、以前のコメントが実際に間違っていることに気付きました。私はそれを修正する方法を見つけました:余分なスペースをまったく使用せずに、上記の2つと同じ実行時間で2つのアルゴリズムを考えました(プッシュは現在時間がかかる操作であり、ポップは一定時間で行われています)。すべてを書き留めたら、新しい回答を投稿します。
アレックス10ブリンク

12

以前の回答に関するいくつかのコメントの後、私は多かれ少なかれ不正行為であることが明らかになりました:私は余分なスペースを使用しました(2番目のアルゴリズムの余分なスペース)Popメソッドの実行中。O(n)

次のアルゴリズムは、プッシュとポップの実行中にメソッド間の追加スペースを使用せず、追加スペースのみを使用します。プッシュにはO O(1)実行時間を償却し、PopにはO1最悪のケース(および償却)実行時間があります。O(n)O(1)

司会者への注意:これを別の回答にするという私の決定が正しいものかどうかは完全にはわかりません。元の回答はまだ質問に関連している可能性があるため、削除しないでください。

アルゴリズム

キューとキューs e c o n dの 2つのキューがあります。f i r s tが「キャッシュ」になり、s e c o n dがメインの「ストレージ」になります。両方のキューは常に「スタック順」になります。f i r s tにはスタックの最上部に要素が含まれ、s e c o n dにはスタックの最下部に要素が含まれます。f i rのサイズfirstsecondfirstsecondfirstsecondは常にせいぜい s e c o n d 平方根になります。firstsecond

  • プッシュは、次のようにキューの先頭にパラメーターを「挿入」することによって行われます。パラメーターをにエンキューし、その後、f i r s t内の他のすべての要素をデキューおよび再エンキューします。このようにして、パラメーターはf i r s tの開始時に終わります。firstfirstfirst
  • 場合の平方根より大きくなり、S 、E 、C 、O 、N個のD、我々はすべての要素エンキューS E C O のn dは上にF iがrはS 、T一つずつ、次に交換fはiがrはS 、T及びS e c o n d。このようにして、f i r s t(スタックの最上部)の要素はs eの先頭になりますfirstsecondsecondfirstfirstsecondfirstsecond
  • ポップは、をデキューし、f i r s tが空でない場合に結果を返すことによって行われます。それ以外の場合は、s e c o n dをデキューして結果を返します。firstfirstsecond

最初のアルゴリズムのC#コード

このコードは、C#を一度も見たことがなくても、非常に読みやすいはずです。ジェネリックが何なのかわからない場合は、頭の中で「T」のすべてのインスタンスを「string」に置き換えて、文字列のスタックにします。

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();
    }
}

分析

最悪の場合、明らかにPopは時間で動作します。O(1)

プッシュはOで動作しますO(n)|first|<|second|O(n)|first||second|O(n)firstO(n)O(nn)=O(n)


回答の削除については、meta.cstheory.stackexchange.com/q/386/873をご覧ください。
MS Dousti

行が分からないfirst.Enqueue(first.Dequeue())。何か間違って入力しましたか?
MS Dousti

リンクをありがとう、それに応じて元の回答を更新しました。第二に、アルゴリズムの実行中に何が起こっているかを説明するコードに多くのコメントを追加しました。混乱が解消されることを願っています。
アレックス10ブリンク10

私にとって、アルゴリズムは編集前に読みやすく、理解しやすいものでした。
カベ

9

Θ(N)

NNNNN

PUSHN(PUSHNPOPN)N

NN/2

N

N/2N/2

N/22N

N/22NNNN/23NΩ(N)


NN

nQ1N/2Q22nn4:1+2++n+n2n

どうやらピーターの答えはこの下限に矛盾していますか?
ジョー14年

PUSHNPOPNO(N)

O(N)

6

O(lgn)pushpoppopO(lgn)pop が要求され、出力キューが空の場合、完全なシャッフルのシーケンスを実行して入力キューを反転し、出力キューに格納します。

O(1)

私の知る限り、これは新しいアイデアです...



ああ!更新された質問または関連する質問を探す必要がありました。以前の回答でリンクした論文は、kスタックとk + 1スタックの関係を仮定していました。このトリックは、kとk + 1スタックの間にk個のキューのパワーを置くことになりますか?もしそうなら、それは一種のきちんとしたサイドノートです。いずれにせよ、あなたの答えに私をリンクしてくれてありがとう。そうすれば、別の会場のためにこれを書くのに時間を浪費しなかった。
ピーターブース

1

余分なスペースを使用せずに、優先順位付けされたキューを使用して、新しいプッシュごとに前のキューよりも高い優先度を強制することはできますか?それでも、O(1)ではありません。


0

償却された一定時間でスタックを実装するキューを取得できません。ただし、最悪の場合線形時間で2つのキューを取得してスタックを実装する方法を考えることができます。

  • AB
  • プッシュ操作が行われるたびに、ビットを反転させ、キューに要素を挿入することで、ビットが区別されます。他のキューからすべてをポップし、それを現在のキューにプッシュします。
  • ポップ操作は、現在のキューの先頭を離陸し、外部状態ビットに触れません。

もちろん、最後の操作がプッシュかポップかを示す外部状態をもう少し追加できます。2つのプッシュ操作が連続して行われるまで、すべてを1つのキューから別のキューに移動するのを遅らせることができます。これにより、ポップ操作が少し複雑になります。これにより、ポップ操作のO(1)償却済み複雑度が得られます。残念ながら、プッシュは線形のままです。

プッシュ操作が行われるたびに、新しい要素が空のキューの先頭に置かれ、完全なキューがその末尾に追加されて、要素を効果的に反転するため、これらすべてが機能します。

償却された一定時間の操作を取得したい場合は、おそらくもっと賢いことをする必要があります。


4
確かに、同じ最悪の時間の複雑さで複雑さのない単一のキューを使用でき、基本的にキューをスタックの最上部を表す追加のキュー要素を持つ循環リストとして扱います。
デイブクラーク

できるように見えます!ただし、この方法でスタックをシミュレートするには、複数のクラシックキューが必要なようです。
ロススナイダー

0

キューがフロントローディングを許可する場合、1つのキュー(またはより具体的にはdeque)のみを必要とする簡単なソリューションがあります。おそらく、これは元の質問のコースTAが念頭に置いていたタイプのキューですか?

フロントローディングを許可せずに、別のソリューションを次に示します。

このアルゴリズムには2つのキューと2つのポインターが必要です。これらをそれぞれQ1、Q2、プライマリ、セカンダリと呼びます。初期化すると、Q1とQ2は空になり、プライマリポイントはQ1、セカンダリポイントはQ2になります。

PUSH操作は簡単で、単に次のもので構成されます。

*primary.enqueue(value);

POP操作はやや複雑です。プライマリキューの最後のアイテムを除くすべてをセカンダリキューにスプールし、ポインタを交換し、元のキューから最後の残りのアイテムを返す必要があります。

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

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

境界チェックは行われず、O(1)ではありません。

これを入力しているときに、Alexが行ったように、whileループの代わりにforループを使用して単一のキューでこれを実行できることがわかります。いずれにしても、PUSH操作はO(1)で、POP操作はO(n)になります。


次に、それぞれQ1、Q2、およびqueue_pと呼ばれる2つのキューと1つのポインターを使用する別のソリューションを示します。

初期化時に、Q1とQ2は空であり、queue_pはQ1を指します。

繰り返しますが、PUSH操作は簡単ですが、他のキューでqueue_pを指す追加の手順が1つ必要です。

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

POP操作の操作は以前と似ていますが、現在はキューを介してローテーションする必要があるn / 2個のアイテムがあります。

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());

PUSH操作はまだO(1)ですが、POP操作はO(n / 2)です。

個人的には、この問題のために、単一の両端キュー(deque)を実装し、必要に応じてスタックを呼び出すというアイデアを好みます。


2番目のアルゴリズムは、Alexのより複雑なアルゴリズムを理解するのに役立ちます。
hengxin

0

kΘ(n1/k)

k
n
O(1)

iΘ(ni/k)Θ(ni/k)O(1)i+1O(n1/k)i1Θ(n1/k)

mmΩ(mn1/k)o(n1/k)Ω(n1/k)o(n2/k)ko(n)

Θ(logn)


-3

スタックは、2番目のキューを使用して2つのキューを使用して実装できます。アイテムがスタックにプッシュされると、それらはキューの最後に追加されます。アイテムがポップされるたびに、最初のキューのn – 1個の要素を2番目に移動し、残りのアイテムを返します。パブリッククラスQueueStack実装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(); } private void swapQueues(){IQueue Q = q2; q2 = q1; q1 = Q; }}


2
償却時間O(1)に関する質問の一部を見逃しましたか?
デビッドエップシュタイン14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.