questions F-L

This commit is contained in:
2025-05-25 11:47:04 +01:00
parent ecf95bd23d
commit 917c371529
54 changed files with 11235 additions and 0 deletions

View File

@@ -0,0 +1,204 @@
title: Implement Queue using Stacks
slug: implement-queue-using-stacks
difficulty: easy
leetcode_id: 232
leetcode_url: https://leetcode.com/problems/implement-queue-using-stacks/
categories:
- stack
- queue
patterns:
- monotonic-stack
description: |
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()` Returns `true` if the queue is empty, `false` otherwise.
**Notes:**
- You must use **only** standard operations of a stack, which means only `push to top`, `peek/pop from top`, `size`, and `is empty` operations 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.
constraints: |
- `1 <= x <= 9`
- At most `100` calls will be made to `push`, `pop`, `peek`, and `empty`
- All the calls to `pop` and `peek` are valid
examples:
- 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
explanation:
intuition: |
Think of this problem like having two buckets to simulate a conveyor belt.
A **queue** follows First-In-First-Out (FIFO) order — the first item added is the first to leave, like a line at a coffee shop. A **stack** follows Last-In-First-Out (LIFO) order — the last item added is the first to leave, like a stack of plates.
The key insight is that **reversing a stack gives you the opposite order**. If you push elements `1, 2, 3` onto a stack, they come out as `3, 2, 1`. But if you pop them all onto a *second* stack, they reverse again to `1, 2, 3` — exactly FIFO order!
So we use two stacks:
- An **input stack** where new elements are pushed
- An **output stack** from which elements are popped
When we need to pop or peek and the output stack is empty, we transfer all elements from the input stack to the output stack. This reversal converts the LIFO order to FIFO order.
approach: |
We solve this using a **Two-Stack Approach** with lazy transfer:
**Step 1: Initialise two stacks**
- `input_stack`: Where we push new elements
- `output_stack`: Where we pop/peek elements from
&nbsp;
**Step 2: Push operation**
- Simply push the element onto `input_stack`
- This is always O(1)
&nbsp;
**Step 3: Pop/Peek operation**
- If `output_stack` is empty, transfer all elements from `input_stack` to `output_stack`
- Each transfer reverses the order, converting LIFO to FIFO
- Pop or peek from `output_stack`
&nbsp;
**Step 4: Empty check**
- Queue is empty only when both stacks are empty
&nbsp;
The lazy transfer approach is key: we only move elements when necessary, which gives us amortised O(1) time per operation.
common_pitfalls:
- title: Transferring on Every Operation
description: |
A naive approach might transfer elements between stacks on every push or pop. This leads to O(n) for every operation.
The correct approach uses **lazy transfer**: only move elements from input to output when output is empty and we need to pop/peek. Each element is moved at most twice (once to input, once to output), giving amortised O(1).
wrong_approach: "Transfer between stacks on every operation"
correct_approach: "Lazy transfer only when output stack is empty"
- title: Forgetting to Check Output Stack First
description: |
When implementing pop/peek, you must first check if the output stack has elements before transferring from input. If you always transfer, you'll break the FIFO order.
For example, if output has `[1]` and input has `[2, 3]`, transferring would make output `[1, 3, 2]` which is wrong.
wrong_approach: "Always transfer from input to output"
correct_approach: "Only transfer when output is empty"
- title: Not Handling Peek Efficiently
description: |
Some implementations might pop, save the value, then push back for peek. This is unnecessary.
Since we have access to the top of the output stack, we can simply return the top element without modification.
key_takeaways:
- "**Data structure simulation**: You can simulate one data structure with another by understanding their fundamental properties"
- "**Amortised analysis**: Each element is pushed and popped at most twice total, so n operations take O(n) time overall"
- "**Lazy evaluation**: Deferring work (transfers) until necessary often improves average performance"
- "**Related problem**: The inverse problem — implementing a stack using queues (LeetCode 225) — uses similar reversal logic"
time_complexity: "O(1) amortised for all operations. Each element is moved between stacks at most once, so n operations take O(n) total."
space_complexity: "O(n). We store all n elements across the two stacks."
solutions:
- approach_name: Two Stacks with Lazy Transfer
is_optimal: true
code: |
class MyQueue:
def __init__(self):
# Input stack: where we push new elements
self.input_stack = []
# Output stack: where we pop/peek from
self.output_stack = []
def push(self, x: int) -> None:
# Always push to input stack - O(1)
self.input_stack.append(x)
def pop(self) -> int:
# Ensure output stack has elements
self._transfer_if_needed()
# Pop from output stack - FIFO order
return self.output_stack.pop()
def peek(self) -> int:
# Ensure output stack has elements
self._transfer_if_needed()
# Return top of output stack without removing
return self.output_stack[-1]
def empty(self) -> bool:
# Queue is empty only when both stacks are empty
return not self.input_stack and not self.output_stack
def _transfer_if_needed(self) -> None:
# Only transfer when output is empty - lazy evaluation
if not self.output_stack:
# Move all elements from input to output
# This reverses order: LIFO -> FIFO
while self.input_stack:
self.output_stack.append(self.input_stack.pop())
explanation: |
**Time Complexity:** O(1) amortised for all operations.
- `push`: O(1) — direct append
- `pop`/`peek`: O(1) amortised — each element is transferred at most once
- `empty`: O(1) — two boolean checks
**Space Complexity:** O(n) — storing n elements across both stacks.
The key insight is lazy transfer: we only move elements when the output stack is empty. Since each element moves from input to output exactly once, the amortised cost per operation is O(1).
- approach_name: Two Stacks with Eager Transfer
is_optimal: false
code: |
class MyQueue:
def __init__(self):
self.stack1 = [] # Main storage
self.stack2 = [] # Temporary for reversal
def push(self, x: int) -> None:
# Move everything to stack2
while self.stack1:
self.stack2.append(self.stack1.pop())
# Push new element to bottom of stack1
self.stack1.append(x)
# Move everything back
while self.stack2:
self.stack1.append(self.stack2.pop())
def pop(self) -> int:
# Top of stack1 is front of queue
return self.stack1.pop()
def peek(self) -> int:
return self.stack1[-1]
def empty(self) -> bool:
return not self.stack1
explanation: |
**Time Complexity:** O(n) for push, O(1) for pop/peek/empty.
**Space Complexity:** O(n) — storing n elements across both stacks.
This approach maintains FIFO order at all times by doing expensive work during push. Every push transfers all elements twice. While pop/peek become O(1), push is O(n), making this less efficient than lazy transfer when pushes are frequent.