{ }codelper

学ぶ › JavaScript

アルゴリズム · 27

スタックとキュー

後から入れたものを先に出すスタック、先に入れたものを先に出すキュー。

スタックとキューは、値を入れておき、決まった順番で取り出すデータ構造です。
下の例は給食室です。
tray x は番号 x のトレーを山のいちばん上に置き、come 名前 はその人が列のいちばん後ろに並びます。
serve は列の先頭の人がいちばん上のトレーを取りますが、トレーか人がなければ - を表示します。
最後に、残ったトレーの数と待っている人の数を表示します。

const lines = require("fs").readFileSync(0, "utf8").split("\n");const q = Number(lines[0]);const pile = []; // 스택: 끝이 맨 위const waiting = []; // 큐: 뒤에 넣고, 맨 앞 번호(head)만 뒤로 옮겨요let head = 0;for (let i = 1; i <= q; i++) {  const parts = lines[i].trim().split(" ");  if (parts[0] === "tray") {    pile.push(Number(parts[1])); // 맨 위에 올리기  } else if (parts[0] === "come") {    waiting.push(parts[1]); // 맨 뒤에 서기  } else if (pile.length > 0 && head < waiting.length) { // 꺼내기 전에 비었는지 확인    const name = waiting[head++]; // 가장 먼저 온 사람    const tray = pile.pop(); // 가장 나중에 올린 식판    console.log(name, tray);  } else {    console.log("-");  }}console.log(pile.length, waiting.length - head);
stdin
9
tray 3
tray 8
come ann
come bob
serve
tray 5
serve
serve
come cho
stdout
ann 8
bob 5
-
1 1

ポイント

  • スタックは後から入れたものが先に出ます(後入れ先出し、LIFO)。
    積んだトレーのように、入れる所と出す所が同じ片方の端です。
  • キューは先に入れたものが先に出ます(先入れ先出し、FIFO)。
    行列のように、後ろに入れて前から出します。
  • スタックはリストの末尾だけを使えば作れます。
    末尾に入れて末尾から取れば、ほかの値は動きません。
  • リストの先頭を取り除くと、残りの値がすべて一つずつ前に詰まります。
    私たちのジャッジで 20 万個を先頭からすべて取り除くと、Python の list.pop(0) は約 3.5 秒、Java の ArrayList.remove(0) は約 2 秒、C# の List.RemoveAt(0) と C++ の vector.erase(begin()) は約 1.7 秒かかり、JavaScript の shift() は 3 秒を少し超えて制限時間(3 秒)に引っかかりました。
    同じことをキュー専用のデータ構造(deque・ArrayDeque・Queue・std::queue)ですると、プログラムの起動時間を入れても、どれも 0.05 秒以内に終わりました。
  • キュー専用のデータ構造がない JavaScript・TypeScript・C・Craykon では、配列(リスト)をキューとして使います。
    入れるときは末尾に入れ、取り出すときは値を取り除かずに waiting[head] を読んでから先頭の番号 head を 1 進めます。
    head が入れた個数と同じなら空のキューで、待っている個数は「入れた個数 − head」です。
    私たちのジャッジの JavaScript では、この方法で 20 万個を取り出すのに 0.01 秒もかかりませんでした。
  • 取り出す前に空かどうかを先に確かめます。
    空のスタックやキューから取り出すと、エラーで止まる言語もあれば(Python・C#・Craykon)、エラーなしで undefined や null が返るものもあります(JavaScript、Java の poll)。
    C・C++ では何が起こるか決まっていません。

言語ごとの書き方

Pythonスタックはリスト:pile.append(x)、pile.pop()、いちばん上を見るのは pile[-1]。
キューは from collections import deque:waiting.append(x)、waiting.popleft()、先頭を見るのは waiting[0]。
空かどうかは if pile:。
空から取り出すと IndexError。
queue.Queue はスレッドどうしで値を受け渡すためのもので、空のとき get() がエラーにならずに待ち続けるので、ここでは deque を使います
JavaScriptスタックは配列:pile.push(x)、pile.pop()、いちばん上を見るのは pile[pile.length - 1]。
キュー専用はないので、waiting.push(x) で入れて waiting[head++] で取り出します。
空の配列の pop()・shift() はエラーにならず undefined を返します
TypeScriptJavaScript と同じです。
pile.pop() の型は number | undefined なので、number の変数に入れるには、空でないことを確かめてから pile.pop()! と書きます
Javaどちらも ArrayDeque。
スタックは push・pop・peek、キューは add・poll・peek。
push は先頭に、add は末尾に入れ、pop・poll・peek はどれも先頭を扱います。
空かどうかは isEmpty()。
空のとき pop() は NoSuchElementException、poll()・peek() は null
C#Stack<int>:Push・Pop・Peek。
Queue<string>:Enqueue・Dequeue・Peek。
個数は Count。
空から取り出すと InvalidOperationException、TryPop(out x)・TryDequeue(out x) は止まらずに false を返します
C++#include <stack>:push・top・pop。
#include <queue>:push・front・pop。
pop() は値を返さないので、先に top()・front() で読みます。
空かどうかは empty()。
空のときに top()・front()・pop() を呼ぶと、エラーで止まる保証はなく、何が起こるか決まっていません(未定義動作)
C専用のデータ構造がないので、配列と番号で作ります。
スタック:pile[top++] = x; で入れ、pile[--top] で取り出します。
キュー:waiting[tail++] に入れ、waiting[head++] から取り出します。
空でない条件は top > 0、head < tail
Craykonどちらもリストです。
スタック:pile.追加(x)、いちばん上は pile[pile.個数 - 1]、取り除くのは pile.位置で取り除く(pile.個数 - 1)。
キュー:waiting.追加(x) で入れ、waiting[head] を読んでから head += 1。
空のリストから読んだり取り除いたりすると「リストが空です」と言って止まります

よくある間違い

空のスタックから取り出したのにエラーにならなかったJavaScript

const pile = [];
const tray = pile.pop();
console.log(tray + 1);

→ NaN — 空の配列の pop() は止まらずに undefined を返すので、間違った値が黙って計算に混ざります

取り出す前に pile.length > 0 を確かめます。
TypeScript は同じコードを 'tray' is possibly 'undefined' として実行前に止めてくれます。

add で入れて pop で取り出したJava

Deque<Integer> pile = new ArrayDeque<>();
pile.add(1);
pile.add(2);
pile.add(3);
System.out.println(pile.pop());

→ 1 — 最後に入れた 3 ではありません。add は後ろに入れ、pop は前から取り出すので、キューのように動きました

スタックは push・pop、キューは add・poll と、組をそろえて使います。
push は先頭に入れるので、push で 1、2、3 を入れれば pop() は 3 を返します。

pop() が値を返すと思ったC++

std::stack<int> pile;
pile.push(3);
int tray = pile.pop();

→ コンパイルエラー void value not ignored as it ought to be — std::stack と std::queue の pop() は取り除くだけで、何も返しません

int tray = pile.top(); で先に読み、pile.pop(); で取り除きます。
キューは front() で読みます。

言語ごとの違い
「いちばん新しいものから」扱う仕事(元に戻す、かっこの対応)はスタック、「来た順に」扱う仕事(行列、順番待ち)はキューです。
head だけを進めるキューは、取り出した値も配列に残ります。
C では配列を「同時に待つ最大数」ではなく「入れる回数」の分だけ用意します。
Craykon のリストは 1 万個までで、1 万個を 位置で取り除く(0) で先頭からすべて取り除いても、私たちのジャッジで 0.2 秒以内に終わりました。

練習問題