2つのキューを使用してスタックを実装する意味は何ですか?


34

次の宿題の質問があります。

2つのキューを使用して、スタックメソッドpush(x)およびpop()を実装します。

これは私には奇妙に思えます:

  • スタック(LIFO)キューです
  • 実装に2つのキューが必要な理由がわかりません

私は周りを検索しました:

そして、いくつかの解決策を見つけました。これが私がやったことです:

public class Stack<T> {
    LinkedList<T> q1 = new LinkedList<T>();
    LinkedList<T> q2 = new LinkedList<T>();

    public void push(T t) {
        q1.addFirst(t);
    }

    public T pop() {
        if (q1.isEmpty()) {
            throw new RuntimeException(
                "Can't pop from an empty stack!");
        }

        while(q1.size() > 1) {
            q2.addFirst( q1.removeLast() );
        }

        T popped = q1.pop();

        LinkedList<T> tempQ = q1;
        q1 = q2;
        q2 = tempQ;

        return popped;
    }
}

しかし、単一のキューを使用することの利点はわかりません。2つのキューバージョンは無意味に複雑に見えます。

上記のように、プッシュのほうが2のほうが効率的でありpush、同じままでpop、最後の要素を繰り返してそれを返す必要があるとします。両方の場合において、pushあろうO(1)、とpopなるであろうO(n)。ただし、単一キューバージョンは大幅にシンプルになります。単一のforループのみが必要です。

何か不足していますか?ここで洞察をいただければ幸いです。


17
通常、キューはFIFO構造を参照しますが、スタックはLIFO構造です。JavaのLinkedListのインターフェースは、FIFOとLIFOの両方のアクセスを許可する両端キュー(両端キュー)のインターフェースです。LinkedList実装ではなく、Queueインターフェイスにプログラミングを変更してみてください。

12
より一般的な問題は、2つのスタックを使用してキューを実装することです。純粋に機能的なデータ構造に関するChris Okasakiの本は興味深いかもしれません。
エリックリッパー

2
エリックが言ったことを基に、スタックベースの言語(dcや2つのスタックを持つプッシュダウンオートマトン(チューリングマシンに相当する、もっと多くのことができるため)など)に自分自身を見つけることができます。複数のスタックがありますが、キューはありません。

1
@MichaelT:または、スタックベースのCPUで実行している自分自身を見つけることもできます
-slebetman

11
「スタックは(LIFO)キューです」 ...うーん、キューは待ち行列です。公衆トイレを使用するための行のように。待っている行は、LIFOのように振る舞いますか?「LIFOキュー」という用語の使用を停止してください。これは無意味です。
Mehrdad

回答:


44

利点はありません。これは純粋にアカデミックな演習です。

非常に私は新入生が大学にいたとき、長い時間前、私は同様の運動持っていた1。目標はfor、ループカウンター付きのループを使用して反復ソリューションを作成する代わりに、オブジェクト指向プログラミングを使用してアルゴリズムを実装する方法を生徒に教えることでした。代わりに、既存のデータ構造を組み合わせて再利用して、目標を達成してください。

Real World TMでこのコードを使用することはありません。この演習から取り去る必要があるのは、「箱の外で考えて」コードを再利用する方法です。


実装を直接使用する代わりに、コードでjava.util.Queueインターフェースを使用する必要があることに注意してください。

Queue<T> q1 = new LinkedList<T>();
Queue<T> q2 = new LinkedList<T>();

これによりQueue、必要に応じて他の実装を使用したり、インターフェイスの精神を回避する可能性のある2つのメソッドを非表示にLinkedListしたりできQueueます。これは、get(int)pop()(あなたのコードがコンパイルされている間、あなたの割り当ての制約が与えられた中で、論理エラーがあります。としてあなたの変数を宣言するQueue代わりにLinkedList、それを明らかにします)。関連資料:「インターフェイスへのプログラミング」の理解インターフェイスが役立つ理由

1 私は今でも覚えています:演習は、Stackインターフェイスのメソッドのみを使用し、java.util.Collections他の「静的専用」ユーティリティクラスのユーティリティメソッドを使用せずに、スタックを逆にすることでした。正しい解決策は、他のデータ構造を一時的な保持オブジェクトとして使用することです。さまざまなデータ構造、それらのプロパティ、およびそれらを結合する方法を知っている必要があります。以前プログラミングしたことがなかったCS101クラスのほとんどを困惑させました。

2 メソッドはまだありますが、型キャストまたはリフレクションなしではアクセスできません。したがって、これらの非キューメソッドを使用するのは簡単ではありません。


1
ありがとう。それは理にかなっていると思います。また、上記のコードで「違法」な操作を使用していることに気づきました(FIFOの前に押します)が、何も変わらないと思います。すべての操作を元に戻しましたが、意図したとおりに機能します。他の人が意見を述べることを思いとどまらせたくないので、受け入れる前に少し待つつもりです。ありがとう、結構です。
発癌性

19

利点はありません。キューを使用してスタックを実装すると、時間が非常に複雑になることを正しく認識しました。(有能な)プログラマーが「実生活」でこのようなことをすることはありません。

しかし、それは可能です。ある抽象化を使用して別の抽象化を実装でき、その逆も可能です。スタックは2つのキューの観点から実装できます。同様に、2つのスタックの観点からキューを実装できます。この演習の利点は次のとおりです。

  • あなたはスタックを要約します
  • キューを要約します
  • アルゴリズム的思考に慣れる
  • スタック関連のアルゴリズムを学ぶ
  • アルゴリズムのトレードオフについて考えるようになります
  • キューとスタックの等価性を実現することにより、コースのさまざまなトピックを結び付けます
  • あなたは実用的なプログラミングの経験を積む

実際、これは素晴らしい練習です。私は今すぐ自分でやるべきです:)


3
@JohnKugelman編集していただきありがとうございますが、私は本当に「恐ろしい時間の複雑さ」を意味していました。リンクリストベースのスタックのpush場合peekpop操作はO(1)にあります。サイズ変更可能な配列ベースのスタックについても同じですpushが、O(1)が償却され、O(n)が最悪の場合です。それと比較して、キューベースのスタックは、O(n)プッシュ、O(1)ポップとピーク、またはO(1)プッシュ、O(n)ポップとピークで非常に劣っています。
アモン

1
「恐ろしい時間の複雑さ」と「非常に劣る」は正確ではありません。償却された複雑さは、プッシュおよびポップでは依然としてO(1)です。TAOCP(vol1?)には、それについて楽しい質問があります(基本的に、要素が1つのスタックから他のスタックに切り替えることができる回数が一定であることを示す必要があります)。1つの操作の最悪の場合のパフォーマンスは異なりますが、ArrayListsのプッシュのO(N)パフォーマンスについて話す人はほとんどいません。通常は興味深い数ではありません。
Voo

5

2つのスタックからキューを作成することには、間違いなく本当の目的があります。関数型言語の不変のデータ構造を使用する場合、プッシュ可能なアイテムのスタックにプッシュし、ポップ可能なアイテムのリストからプルできます。すべてのアイテムがポップアウトされると、ポップ可能なアイテムが作成されます。新しいポップ可能なスタックは、新しいプッシュ可能なスタックが空になったプッシュ可能なスタックの逆です。効率的です。

2つのキューで構成されたスタックについては?これは、大量の高速キューを使用できる場合に意味があります。この種のJavaの演習としては、まったく役に立ちません。ただし、それらがチャネルまたはメッセージングキューである場合は意味があります。(つまり:N個のメッセージがキューに入れられ、O(1)操作で(N-1)個のアイテムを先頭に移動して新しいキューに入れます。)


うーん、これにより、シフトレジスタをコンピューティングの基礎として使用すること、およびベルト/ミルアーキテクチャについて考えるようになりました
-slebetman

うわー、ミルCPUは確かに興味深いです。確かに「キューマシン」。
ロブ

2

演習、実用的な観点から不必要に考案されています。ポイントは、スタックを実装するためにキューのインターフェイスを巧妙に使用することを強制することです。たとえば、「1つのキュー」ソリューションでは、キューを反復処理して、スタック「ポップ」操作の最後の入力値を取得する必要があります。ただし、キューデータ構造では値を反復処理できません。先入れ先出し(FIFO)で値にアクセスすることは制限されています。


2

他の人がすでに指摘したように、実世界の利点はありません。

とにかく、質問の2番目の部分に対する1つの答え、単に1つのキューを使用するのはなぜかということは、Javaを超えています。

Javaでは、Queueインターフェースにもsize()メソッドがあり、そのメソッドの標準実装はすべてO(1)です。

これは、C / C ++プログラマーが実装する単純/正規リンクリストには必ずしも当てはまりません。最初と最後の要素へのポインターと、各要素へのポインターが次の要素へのポインターになります。

その場合size()はO(n)であり、ループ内では避ける必要があります。または、実装は不透明で、最低限のadd()とのみを提供しますremove()

このような実装ではn-1、最初に要素を2番目のキューに転送して要素の数をカウントし、要素を最初のキューに転送して残りの要素を返す必要があります。

そうは言っても、Javaランドに住んでいると、おそらくこのようなものを構成しないでしょう。


size()メソッドについての良い点。ただし、O(1)size()メソッドがない場合、スタックが現在のサイズ自体を追跡するのは簡単です。1つのキューの実装を停止するものはありません。
cmaster

それはすべて実装に依存します。フォワードポインターと最初と最後の要素へのポインターのみで実装されたキューがある場合でも、要素を削除してローカル変数に保存し、その変数の以前の要素を同じキューに追加するアルゴリズムを書くことができます最初の要素が再び見られます。これは、同じ値を持つものだけでなく、要素を一意に識別できる場合(ポインタなど)にのみ機能します。O(n)。add()とremove()のみを使用します。とにかく、アルゴリズムについて考えることを除いて、実際にそれを行う理由を見つけることは、最適化する方が簡単です。
スレイド

0

そのような実装の使用を想像するのは難しいです、それは本当です。しかし、ポイントのほとんどは、それができることを証明することです。

しかし、これらのものの実際の使用に関しては、2つ考えることができます。これの用途の1つは、そのために設計されていない制約された環境でシステムを実装することです:たとえば、Minecraftのレッドストーンブロックは、論理回路やCPU全体を実装するために使用されたチューリング完全なシステムを表します。スクリプト駆動型ゲームの初期の頃、最初のゲームボットの多くもこの方法で実装されていました。

しかし、この原則を逆に適用することもできます。そうしないと、システムで何かが不可能になります。これは、セキュリティコンテキストで発生する可能性があります。たとえば、強力な構成システムは資産になる可能性がありますが、ユーザーには与えない程度のパワーがまだあります。これにより、攻撃者によって設定言語が破壊されないように、設定言語に許可する内容が制限されますが、この場合はそれが目的です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.