Try an interactive version of this dialog: Sign up at solve.it.com, click Upload, and pass this URL.
import sys
sys.path.append('.')
from leet_utils import lc_setup, get_leetcode_problem, lc_export
lc_setup("https://leetcode.com/problems/implement-queue-using-stacks/", add_cells=False)
from IPython.display import HTML
problem = get_leetcode_problem("https://leetcode.com/problems/implement-queue-using-stacks/")
HTML(problem["content"])
Implement a first in first out (FIFO) queue using only two stacks. The implemented queue should support all the functions of a normal queue (push, peek, pop, and empty).
Implement the MyQueue class:
void push(int x)Pushes element x to the back of the queue.int pop()Removes the element from the front of the queue and returns it.int peek()Returns the element at the front of the queue.boolean empty()Returnstrueif the queue is empty,falseotherwise.
Notes:
- You must use only standard operations of a stack, which means only
push to top,peek/pop from top,size, andis emptyoperations are valid. - Depending on your language, the stack may not be supported natively. You may simulate a stack using a list or deque (double-ended queue) as long as you use only a stack's standard operations.
Example 1:
Input ["MyQueue", "push", "push", "peek", "pop", "empty"] [[], [1], [2], [], [], []] Output [null, null, null, 1, 1, false] Explanation MyQueue myQueue = new MyQueue(); myQueue.push(1); // queue is: [1] myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue) myQueue.peek(); // return 1 myQueue.pop(); // return 1, queue is [2] myQueue.empty(); // return false
Constraints:
1 <= x <= 9- At most
100calls will be made topush,pop,peek, andempty. - All the calls to
popandpeekare valid.
Follow-up: Can you implement the queue such that each operation is amortized O(1) time complexity? In other words, performing n operations will take overall O(n) time even if one of those operations may take longer.
実装1
- 問題の感想
- 今まで当たり前のようにdequeなどをqueue(FIFO)として使ってきたが、確かにstackでどう実装するんだろう。
- 機械語レベルでは全てが1つのstackにより記憶されている。
- push to top, peek/pop from top, size, and is empty operations are valid. とのことで、インデックスアクセスも使えず難しい。
- 特に、peek, popは明らかに工夫が必要。
- アルゴリズムの選択
- peek, popが必要になったとき、stackの中身を別の容器に移し替えることを思いついた。
- この移し替えは必要に迫られて、まとめて行うことで、均し計算量を軽くする作用もあるはず。(follow-up)
- 実装
- 移し替えの操作は内部メソッドに分離する。
- 属性も、直接外部に公開しないような意図で書く。
- stack(容器)が2つ登場するが、お店の商品棚と倉庫に喩えた変数名にした。
- 計算量
- Time
Operation Worst Time Amortized Time push O(1) O(1) pop O(n) O(1) peek O(n) O(1) empty O(1) O(1) - Extra Space: O(n)
- Time
# storage: [1, 2, 3, 4]
# shelf: []
# ↓ _stock & pop
# storage: []
# shelf: [4, 3, 2, 1] -> pop: 1
# ↓
# storage: []
# shelf: [4, 3, 2]
# ↓ push x2, pop x2
# storage: [5, 6]
# shelf: [4]
class MyQueue:
def __init__(self):
self._storage = [] # LIFO
self._shelf = [] # FIFO
def _stock(self):
while self._storage:
self._shelf.append(self._storage.pop())
def push(self, x: int) -> None:
self._storage.append(x)
def pop(self) -> int:
if not self._shelf:
self._stock()
return self._shelf.pop()
def peek(self) -> int:
if not self._shelf:
self._stock()
return self._shelf[-1]
def empty(self) -> bool:
return not self._shelf and not self._storage
# Your MyQueue object will be instantiated and called as such:
# obj = MyQueue()
# obj.push(x)
# param_2 = obj.pop()
# param_3 = obj.peek()
# param_4 = obj.empty()
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
コードをレビューしますね!
良い点:
- 2つのスタックを使う基本アイデアは正しいです
_stockで移し替えをまとめて行う設計により、償却計算量O(1)を達成しています_storageと_shelfという命名も役割が分かりやすい- 計算量の分析も正確です
改善点:
-
peekとpopの重複コードCopied!if not self._shelf: self._stock()
この部分が2箇所にあります。
peekをpopで実装するか、共通化を検討できます:Copied!def peek(self) -> int: if not self._shelf: self._stock() return self._shelf[-1] def pop(self) -> int: self.peek() # _shelfに値があることを保証 return self._shelf.pop()
-
軽微:コメントの意図
Copied!self._storage = [] # LIFO self._shelf = [] # FIFO
両方ともPythonのlistをスタック(LIFO)として使っているので、「FIFOとして見える順番で格納」という意味なら問題ないですが、少し紛らわしいかも。
正しさの確認:
ロジックは正しいです!例のトレースも合っています。_shelfが空のときだけ移し替えるので、各要素は最大2回しか移動しない→償却O(1)が成立します。
動作確認してみますか?簡単なテストケースを書いて試すと安心できると思います。
- LLMレビューの感想
- 基本方針があっているとのことで安心した。
- popとpeekで在庫確認のロジックが被っているのはその通りだと思った。peekに一元化するのもありだと思う。
- コメント集
- https://discord.com/channels/1084280443945353267/1206101582861697046/1232739063891492895
-
お、ちょっと私の期待とは違うアルゴリズムでした。これは関数型言語のデータ構造という背景がある問題に見えます。
- https://www.slideshare.net/hamukazu/ss-10114639
- https://www.cs.cmu.edu/~rwh/students/okasaki.pdf
- 分かりやすく面白い資料だった。
- 「私の期待」というのは、補充でstackを反転させる操作に遅延評価を使うことで、均さなくてもO(1)を実現できることだと推測した。
- LLMに聞いたところ、遅延評価版の厳密な実装はPythonだと難しく、Haskellなどの純粋関数型言語が必要とのこと。
-
- https://discord.com/channels/1084280443945353267/1206101582861697046/1232739063891492895
- PR
- https://github.com/colorbox/leetcode/pull/15
- https://github.com/ryosuketc/leetcode_grind75/pull/13
- https://github.com/huyfififi/coding-challenges/pull/13
- https://github.com/rihib/leetcode/pull/32
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
{
"id": "toolu_016x48TGKqs8KSXtW95M1WwT",
"call": {
"function": "read_url",
"arguments": {
"url": "https://www.slideshare.net/hamukazu/ss-10114639"
}
},
"result": "'Download free for 30 daysSign in\\n\\n[Upload](https://www.slideshare.net/upload)[Language (EN)](http<TRUNCATED>"
}
スライドの内容を読み取れました!これは「純粋関数型アルゴリズム入門」というスライドで、Chris Okasaki の「Purely Functional Data Structures」の解説です。まさにこのLeetCode問題に直結する内容ですね。
要約:「純粋関数型キューのアルゴリズム」
基本アイデア(あなたの実装と同じ!)
- キューを2つのリスト (f, r) として表現する
f:front(取り出し用)、r:rear(追加用)snoc(push): rの先頭に追加 → O(1)head/tail(peek/pop): fから取得 → 通常O(1)- fが空になったら
reverse rを新しいfにする
計算量の分析:銀行家法 (Banker's Method)
reverseはO(n)だが、たまにしか起こらない- snocを1回するたびに「1の貯金」をすると考える
- reverseが必要な時、その貯金を使う
- → ならし計算量 (Amortized) はすべてO(1)
発展:遅延評価版
- 遅延評価を使うと、「ならし」なしで真のO(1)を達成できる
rotateという操作でf ++ reverse rを段階的に計算
あなたの実装との関係
あなたの _storage と _shelf はまさにこの (r, f) に対応しています!_stock() は reverse r を f に移す操作です。関数型プログラミングの古典的なデータ構造を、Pythonで手続き的に実装したものになっています。素晴らしい気づきですね!
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
4つのPRを確認しますね。
{
"id": "toolu_01PvUcJRYwUw1hyTGaRE63eg",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/colorbox/leetcode/pull/15"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
{
"id": "toolu_012oLTrccRPMh7qafc2FsiJS",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/ryosuketc/leetcode_grind75/pull/13"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
{
"id": "toolu_01MyVUsZYP8rrWkr5aAAk1zW",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/huyfififi/coding-challenges/pull/13"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
{
"id": "toolu_01StZrorfUYB5QufGLTsMUhx",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/rihib/leetcode/pull/32"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
4つのPRの内容をまとめますね。
1. colorbox/leetcode#15 (C++)
- 実装: 最初はpush時に毎回移し替えを行う非効率な実装だった
- レビュー指摘: nodchipさんから「pushは
push_stackに入れ、pop/peek時にpop_stackが空の場合のみ移し替えるとamortized O(1)になる」と改善を提案された - 学び: push時に移し替えは不要。関数型言語のデータ構造(Okasaki)に由来する問題との紹介あり
2. ryosuketc/leetcode_grind75#13 (C++)
- 実装:
stack_in/stack_outという分かりやすい命名。amortized O(1)版も実装 - レビュー指摘:
auxという略称についての議論(補助関数の意味で使われる)MyQueue() = defaultの提案bool empty() const noexceptの指摘- peek時に例外が飛ぶ可能性のキャッチについて
- Google Style Guideの関数名規約について
- 学び: Purely Functional Data Structures (Okasaki) の BankersQueue が言及された
3. huyfififi/coding-challenges#13 (Python)
- 実装: Step1で2パターン(毎回移し替え版 / 遅延移し替え版)を試した
- レビュー指摘:
- FILOよりLIFOが一般的
- 変数名の補足コメントがあると良い
pop()とpeek()の共通化について議論(著者は「視線が移動するので書き下す方が好み」との意見)- 空コレクションにtype hintを入れる提案
- 学び: Banker's Method(均し計算量の分析手法)についてのメモあり。「重そうな処理を毎回発生させず、その状況のまま利用する方法を探る」という教訓
4. rihib/leetcode#32 (Go)
- 実装:
pushStack/popStackを使った標準的な実装 - PR説明: Goの構造体初期化について詳しく解説(
MyQueue{}vsnew(MyQueue)vsvar q MyQueue) - レビュー: colorboxさん、odaさんから細かい指摘あり(詳細はShow resolved)
- 特徴: 言語特有の初期化パターンの学習メモが充実
共通の学び:
- 2つのスタックを使い、pop/peek時にのみ必要に応じて移し替えるのが最適解
- Chris Okasaki の "Purely Functional Data Structures" が元ネタとして複数回言及
- 変数名は
in/out,push/pop,input/outputなどが採用されている
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
PRで言及された主要トピックを深掘りしますね。
1. Amortized O(1) と Banker's Method(銀行家法)
「均し計算量」の直感的な理解:
操作の流れ例(n=4要素をpush後、4回pop):
push 1 → storage: [1] コスト: 1
push 2 → storage: [1,2] コスト: 1
push 3 → storage: [1,2,3] コスト: 1
push 4 → storage: [1,2,3,4] コスト: 1
pop → 移し替え発生! コスト: 4 (全要素を移動)
shelf: [4,3,2,1] → 1を返す
pop → shelf: [4,3,2] → 2 コスト: 1
pop → shelf: [4,3] → 3 コスト: 1
pop → shelf: [4] → 4 コスト: 1
─────────────────────────────────
合計: 11 / 8操作 ≈ 1.4 → O(1)
銀行家法の考え方:
- 各pushで「1コインを貯金」すると考える
- 移し替え(reverse)が必要なとき、貯金を使って支払う
- 各要素は最大2回しか移動しない(storage→shelf、shelf→外)
- したがって、どんな操作列でも1操作あたり平均O(1)
2. Okasaki のキューと遅延評価
あなたの実装は「Physicist's Queue」に相当します。さらに発展した「Banker's Queue」では:
通常版(あなたの実装):
- _stock() を呼ぶと、一気にO(n)の処理が走る
- 「ならし」でO(1)だが、最悪ケースはO(n)
遅延評価版:
- reverseを「予約」だけしておき、実際の計算は使う時まで遅延
- 毎回少しずつ計算を進める(インクリメンタル)
- 最悪ケースでもO(1)を保証
Pythonで遅延評価を模倣する例(概念的):
from functools import lru_cache
# イメージ:reverseを遅延させる
class LazyReverse:
def __init__(self, lst):
self._lst = lst
self._reversed = None
def get(self):
if self._reversed is None:
self._reversed = list(reversed(self._lst))
return self._reversed
ただし実用上、Pythonでは通常版で十分です。
3. C++ の const, noexcept, = default
class MyQueue {
public:
MyQueue() = default; // コンパイラ生成のデフォルトコンストラクタを使用
void push(int x);
int pop();
// const: このメソッドはオブジェクトの状態を変更しない
// noexcept: 例外を投げないことを保証(最適化のヒント)
int peek() const;
bool empty() const noexcept;
private:
std::stack<int> push_stack;
std::stack<int> pop_stack;
};
なぜ重要か:
const: 読み取り専用参照から呼べる、意図が明確noexcept: ムーブ操作の最適化、例外安全性の保証= default: 明示的に「デフォルト実装で良い」と示す
4. Go の構造体初期化パターン
// 3つの方法
q1 := MyQueue{} // ゼロ値で初期化(最も一般的)
q2 := new(MyQueue) // ポインタを返す(*MyQueue型)
var q3 MyQueue // 宣言と同時にゼロ値初期化
// Goではゼロ値が有効な状態になるよう設計するのが慣習
type MyQueue struct {
pushStack []int // nil slice はゼロ値、appendで自動初期化
popStack []int
}
Goの哲学: 「ゼロ値を有用にせよ」— コンストラクタなしでも動く設計が推奨される
5. peek/pop の共通化の是非
共通化する派:
def peek(self) -> int:
if not self._shelf:
self._stock()
return self._shelf[-1]
def pop(self) -> int:
val = self.peek() # 在庫確認を委譲
self._shelf.pop()
return val
- DRY原則(Don't Repeat Yourself)
- バグ修正が1箇所で済む
書き下す派(PRでの意見):
def pop(self) -> int:
if not self._shelf:
self._stock()
return self._shelf.pop()
- 視線の移動が減り、読みやすい
- 各メソッドが自己完結している
- 2行程度の重複なら許容範囲
私見: この規模ならどちらでもOK。チームの方針に合わせるのが良いでしょう。
6. 命名について
| 用語 | 意味 | 備考 |
|---|---|---|
| LIFO | Last In, First Out | スタックの標準的な呼び方 |
| FILO | First In, Last Out | 同じ意味だが、LIFOの方が一般的 |
| aux | auxiliary(補助)の略 | 関数型言語で補助関数によく使う |
変数名の選択肢:
push_stack/pop_stack— 用途で命名in_stack/out_stack— 入出力で命名storage/shelf— 比喩で命名(あなたの選択)
どれも意図が伝われば良いですが、input/outputやin/outが最も直感的かもしれません。
他に深掘りしたいトピックはありますか?
class MyQueue:
def __init__(self):
self._storage = []
self._shelf = [] # reversed
def _stock(self) -> None:
while self._storage:
self._shelf.append(self._storage.pop())
def push(self, x: int) -> None:
self._storage.append(x)
def pop(self) -> int:
self.peek()
return self._shelf.pop()
def peek(self) -> int:
if not self._shelf:
self._stock()
return self._shelf[-1]
def empty(self) -> bool:
return not self._shelf and not self._storage
🧠🧠🧠🧠🧠🧠
実装2のレビューです!
良くなった点:
popがpeekに委譲することでDRY原則を達成-> Noneのtype hintを明示(PEP484準拠)- コメントが
# reversedになり、意図がより正確に
改善点:
-
popの戻り値がpeekの後に別途popしているCopied!def pop(self) -> int: self.peek() return self._shelf.pop()
peek()の戻り値を捨てているのが少しもったいない。こう書く方法もあります:Copied!def pop(self) -> int: result = self.peek() self._shelf.pop() return result
ただし、現状でも正しく動作しますし、
_shelf.pop()が値を返すことを活用しているので、これは好みの範囲です。 -
_stockのtype hint 内部メソッドなので必須ではないですが、pushと_stockで-> Noneの扱いを統一しているのは良いです。 -
軽微:空のコレクションのtype hint(オプション) PRで言及されていた点:
Copied!def __init__(self): self._storage: list[int] = [] self._shelf: list[int] = []
明示的に型を書くと、IDEの補完やmypyでのチェックが効きます。
正しさ: ロジックは問題ありません。十分にクリーンな実装だと思います!
これで完成として良いと思いますが、他に気になる点はありますか?
from IPython.display import Markdown
md = lc_export(
next_url="https://leetcode.com/problems/first-bad-version/",
dry_run=False,
desc="",
notebook_url="https://share.solve.it.com/d/d599b9df42461594a24807e846105577"
)
# Markdown(md)