Stacks & Queues: Cấu Trúc Dữ Liệu LIFO và FIFO

Giới Thiệu
Stacks và queues là hai trong những cấu trúc dữ liệu cơ bản và được sử dụng rộng rãi nhất trong khoa học máy tính. Chúng xuất hiện ở khắp nơi — từ nút Back của trình duyệt đến hệ thống lập lịch tác vụ, từ đánh giá biểu thức đến duyệt đồ thị BFS.
Điểm đặc biệt của chúng là mô hình truy cập hạn chế: không giống mảng nơi bạn có thể truy cập bất kỳ phần tử nào, stacks và queues áp đặt một thứ tự hoạt động cụ thể. Chính ràng buộc này làm cho chúng trở nên mạnh mẽ.
Bạn Sẽ Học Được Gì
✅ Hiểu nguyên lý LIFO và FIFO
✅ Cài đặt stacks và queues bằng mảng và danh sách liên kết
✅ Nắm vững pattern Monotonic Stack cho các bài phỏng vấn
✅ Dùng Deque cho bài toán sliding window maximum
✅ Cài đặt queue bằng hai stacks
✅ Giải các bài toán phỏng vấn kinh điển từng bước
Yêu Cầu Trước
- Hiểu về Ký hiệu Big O & Phân tích Độ phức tạp
- Quen thuộc với Các Pattern Giải Quyết Vấn Đề
- Hoàn thành Arrays & Strings và Danh Sách Liên Kết
- Kiến thức lập trình cơ bản bằng TypeScript hoặc Python
1. Stack — Last In, First Out (LIFO)
Stack là cấu trúc dữ liệu tuyến tính nơi các phần tử được chèn và xóa từ cùng một đầu — gọi là đỉnh (top). Phần tử được thêm vào cuối cùng luôn là phần tử được xóa đầu tiên.
Hãy nghĩ đến chồng đĩa: bạn thêm đĩa lên trên cùng, và bạn lấy đĩa từ trên cùng.
Các Thao Tác Cơ Bản của Stack
| Thao tác | Mô tả | Độ phức tạp thời gian |
|---|---|---|
push(x) | Thêm phần tử vào đỉnh | O(1) |
pop() | Xóa và trả về phần tử đỉnh | O(1) |
peek() / top() | Xem phần tử đỉnh không xóa | O(1) |
isEmpty() | Kiểm tra stack rỗng | O(1) |
size() | Trả về số phần tử | O(1) |
1.1 Cài Đặt Stack — Dựa Trên Mảng
Cách đơn giản nhất: dùng cuối mảng làm đỉnh.
// TypeScript - Stack dùng Array
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
if (this.isEmpty()) throw new Error("Stack rỗng");
return this.items.pop();
}
peek(): T {
if (this.isEmpty()) throw new Error("Stack rỗng");
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
toString(): string {
return `[${this.items.join(", ")}] <- đỉnh`;
}
}
// Sử dụng
const stack = new Stack<number>();
stack.push(10);
stack.push(20);
stack.push(30);
console.log(stack.toString()); // [10, 20, 30] <- đỉnh
console.log(stack.peek()); // 30
console.log(stack.pop()); // 30
console.log(stack.size()); // 2# Python - Stack dùng List
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def pop(self):
if self.is_empty():
raise IndexError("Stack rỗng")
return self.items.pop()
def peek(self):
if self.is_empty():
raise IndexError("Stack rỗng")
return self.items[-1]
def is_empty(self):
return len(self.items) == 0
def size(self):
return len(self.items)
def __repr__(self):
return f"{self.items} <- đỉnh"
# Sử dụng
stack = Stack()
stack.push(10)
stack.push(20)
stack.push(30)
print(stack) # [10, 20, 30] <- đỉnh
print(stack.peek()) # 30
print(stack.pop()) # 30
print(stack.size()) # 21.2 Cài Đặt Stack — Dựa Trên Danh Sách Liên Kết
Dùng danh sách liên kết với head luôn là đỉnh:
// TypeScript - Stack dùng Linked List
class ListNode<T> {
val: T;
next: ListNode<T> | null;
constructor(val: T) {
this.val = val;
this.next = null;
}
}
class LinkedStack<T> {
private head: ListNode<T> | null = null;
private _size = 0;
push(item: T): void {
const node = new ListNode(item);
node.next = this.head;
this.head = node;
this._size++;
}
pop(): T {
if (!this.head) throw new Error("Stack rỗng");
const val = this.head.val;
this.head = this.head.next;
this._size--;
return val;
}
peek(): T {
if (!this.head) throw new Error("Stack rỗng");
return this.head.val;
}
isEmpty(): boolean {
return this.head === null;
}
size(): number {
return this._size;
}
}Mảng vs Danh sách liên kết cho Stack:
| Khía cạnh | Dựa trên mảng | Dựa trên danh sách liên kết |
|---|---|---|
| Bộ nhớ | Khối liên tục, có thể resize | Động, overhead con trỏ |
| push/pop | O(1) amortized | O(1) luôn luôn |
| Hiệu suất cache | Tốt hơn (locality) | Kém hơn (rải rác) |
| Lựa chọn thực tế | ✅ Thường ưu tiên hơn | Cho kích thước cố định hoặc kiểm soát bộ nhớ |
2. Queue — First In, First Out (FIFO)
Queue là cấu trúc dữ liệu tuyến tính nơi các phần tử được chèn ở cuối (rear) và xóa từ đầu (front). Phần tử được thêm vào đầu tiên luôn là phần tử được xóa đầu tiên.
Hãy nghĩ đến hàng chờ thanh toán ở cửa hàng: người đầu tiên trong hàng là người đầu tiên được phục vụ.
Các Thao Tác Cơ Bản của Queue
| Thao tác | Mô tả | Độ phức tạp thời gian |
|---|---|---|
enqueue(x) | Thêm phần tử vào cuối | O(1) |
dequeue() | Xóa và trả về phần tử đầu | O(1) |
front() / peek() | Xem phần tử đầu | O(1) |
isEmpty() | Kiểm tra queue rỗng | O(1) |
size() | Trả về số phần tử | O(1) |
2.1 Cài Đặt Queue — Dựa Trên Mảng (Naïve)
Cách đơn giản nhất dùng mảng nhưng có vấn đề: shift() là O(n).
// TypeScript - Queue Naïve (O(n) dequeue — tránh trong phỏng vấn)
class NaiveQueue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item); // O(1) amortized
}
dequeue(): T | undefined {
return this.items.shift(); // ⚠️ O(n) — dịch chuyển tất cả phần tử
}
}2.2 Cài Đặt Queue — Dựa Trên Danh Sách Liên Kết (Đúng)
Dùng danh sách liên kết với cả con trỏ head (đầu) và tail (cuối) cho O(1) ở tất cả các thao tác:
// TypeScript - Queue dùng Linked List (O(1) cho tất cả thao tác)
class QueueNode<T> {
val: T;
next: QueueNode<T> | null = null;
constructor(val: T) { this.val = val; }
}
class Queue<T> {
private head: QueueNode<T> | null = null; // đầu
private tail: QueueNode<T> | null = null; // cuối
private _size = 0;
enqueue(item: T): void {
const node = new QueueNode(item);
if (!this.tail) {
this.head = this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this._size++;
}
dequeue(): T {
if (!this.head) throw new Error("Queue rỗng");
const val = this.head.val;
this.head = this.head.next;
if (!this.head) this.tail = null; // queue trở nên rỗng
this._size--;
return val;
}
front(): T {
if (!this.head) throw new Error("Queue rỗng");
return this.head.val;
}
isEmpty(): boolean {
return this.head === null;
}
size(): number {
return this._size;
}
}
// Sử dụng
const q = new Queue<number>();
q.enqueue(10);
q.enqueue(20);
q.enqueue(30);
console.log(q.front()); // 10
console.log(q.dequeue()); // 10
console.log(q.front()); // 20
console.log(q.size()); // 2# Python - Queue dùng collections.deque (O(1) cho tất cả thao tác)
from collections import deque
class Queue:
def __init__(self):
self.items = deque()
def enqueue(self, item):
self.items.append(item) # Thêm vào phải (cuối)
def dequeue(self):
if self.is_empty():
raise IndexError("Queue rỗng")
return self.items.popleft() # Xóa từ trái (đầu) — O(1)
def front(self):
if self.is_empty():
raise IndexError("Queue rỗng")
return self.items[0]
def is_empty(self):
return len(self.items) == 0
def size(self):
return len(self.items)
# Sử dụng
q = Queue()
q.enqueue(10)
q.enqueue(20)
q.enqueue(30)
print(q.front()) # 10
print(q.dequeue()) # 10
print(q.front()) # 20Mẹo Python: Luôn dùng
collections.dequecho queues. Python lists cópop(0)là O(n) — không bao giờ dùng list thường làm queue.
2.3 Circular Queue
Circular queue tái sử dụng các ô trống từ các phần tử đã dequeue, giúp tiết kiệm bộ nhớ với dung lượng cố định:
// TypeScript - Circular Queue (ring buffer)
class CircularQueue<T> {
private items: (T | null)[];
private head = 0;
private tail = 0;
private _size = 0;
private capacity: number;
constructor(capacity: number) {
this.capacity = capacity;
this.items = new Array(capacity).fill(null);
}
enqueue(item: T): boolean {
if (this._size === this.capacity) return false; // đầy
this.items[this.tail] = item;
this.tail = (this.tail + 1) % this.capacity; // wrap around
this._size++;
return true;
}
dequeue(): T | null {
if (this._size === 0) return null; // rỗng
const val = this.items[this.head] as T;
this.items[this.head] = null;
this.head = (this.head + 1) % this.capacity; // wrap around
this._size--;
return val;
}
isFull(): boolean {
return this._size === this.capacity;
}
isEmpty(): boolean {
return this._size === 0;
}
}3. Deque — Hàng Đợi Hai Đầu
Deque (phát âm là "deck") cho phép chèn và xóa ở cả hai đầu trong O(1). Nó tổng quát hóa cả stacks và queues.
Cài Đặt Deque
// TypeScript - Deque dùng Doubly Linked List
class DequeNode<T> {
val: T;
prev: DequeNode<T> | null = null;
next: DequeNode<T> | null = null;
constructor(val: T) { this.val = val; }
}
class Deque<T> {
private head: DequeNode<T> | null = null;
private tail: DequeNode<T> | null = null;
private _size = 0;
addFront(item: T): void {
const node = new DequeNode(item);
if (!this.head) {
this.head = this.tail = node;
} else {
node.next = this.head;
this.head.prev = node;
this.head = node;
}
this._size++;
}
addRear(item: T): void {
const node = new DequeNode(item);
if (!this.tail) {
this.head = this.tail = node;
} else {
node.prev = this.tail;
this.tail.next = node;
this.tail = node;
}
this._size++;
}
removeFront(): T {
if (!this.head) throw new Error("Deque rỗng");
const val = this.head.val;
this.head = this.head.next;
if (this.head) this.head.prev = null;
else this.tail = null;
this._size--;
return val;
}
removeRear(): T {
if (!this.tail) throw new Error("Deque rỗng");
const val = this.tail.val;
this.tail = this.tail.prev;
if (this.tail) this.tail.next = null;
else this.head = null;
this._size--;
return val;
}
peekFront(): T {
if (!this.head) throw new Error("Deque rỗng");
return this.head.val;
}
peekRear(): T {
if (!this.tail) throw new Error("Deque rỗng");
return this.tail.val;
}
isEmpty(): boolean { return this._size === 0; }
size(): number { return this._size; }
}# Python - Deque dùng collections.deque
from collections import deque
dq = deque()
dq.appendleft(10) # addFront
dq.append(20) # addRear
dq.appendleft(5) # addFront
print(list(dq)) # [5, 10, 20]
dq.popleft() # removeFront -> 5
dq.pop() # removeRear -> 20
print(list(dq)) # [10]4. Ứng Dụng Thực Tế
Hiểu khi nào dùng stacks vs queues quan trọng không kém biết cách cài đặt chúng.
Ứng Dụng của Stack
Ứng Dụng của Queue
5. Các Bài Toán Phỏng Vấn Kinh Điển
Bài Toán 1: Valid Parentheses — Ngoặc Hợp Lệ (Stack)
LeetCode #20 — Cho chuỗi chứa (, ), {, }, [, ], xác định xem chuỗi có hợp lệ không.
Chuỗi hợp lệ khi:
- Mỗi ngoặc mở có một ngoặc đóng tương ứng
- Ngoặc đóng đúng thứ tự
Ý tưởng chính: Dùng stack. Push ngoặc mở. Khi thấy ngoặc đóng, kiểm tra xem nó có khớp với đỉnh stack không.
// TypeScript
function isValid(s: string): boolean {
const stack: string[] = [];
const matching: Record<string, string> = {
')': '(',
'}': '{',
']': '[',
};
for (const char of s) {
if ('({['.includes(char)) {
stack.push(char); // Push ngoặc mở
} else {
// Ngoặc đóng: phải khớp với đỉnh stack
if (stack.length === 0 || stack[stack.length - 1] !== matching[char]) {
return false;
}
stack.pop();
}
}
return stack.length === 0; // Stack phải rỗng ở cuối
}
// Ví dụ
console.log(isValid("()")); // true
console.log(isValid("()[]{}")); // true
console.log(isValid("(]")); // false
console.log(isValid("([)]")); // false
console.log(isValid("{[]}")); // true# Python
def is_valid(s: str) -> bool:
stack = []
matching = {')': '(', '}': '{', ']': '['}
for char in s:
if char in '({[':
stack.append(char)
else:
if not stack or stack[-1] != matching[char]:
return False
stack.pop()
return len(stack) == 0
# Ví dụ
print(is_valid("()")) # True
print(is_valid("()[]{}")) # True
print(is_valid("(]")) # FalseTrace qua "([)]":
char='(' → push → stack=['(']
char='[' → push → stack=['(', '[']
char=')' → matching[')']='(' nhưng top='[' → KHÔNG KHỚP → return falseĐộ phức tạp: Thời gian O(n), Không gian O(n)
Bài Toán 2: Evaluate Reverse Polish Notation — Đánh Giá Ký Pháp Ba Lan Ngược (Stack)
LeetCode #150 — Đánh giá biểu thức ký pháp Ba Lan ngược (postfix). Toán tử: +, -, *, /.
Ví dụ:
["2","1","+","3","*"]→((2 + 1) * 3)=9["4","13","5","/","+"]→(4 + (13 / 5))=6
// TypeScript
function evalRPN(tokens: string[]): number {
const stack: number[] = [];
const ops = new Set(['+', '-', '*', '/']);
for (const token of tokens) {
if (ops.has(token)) {
const b = stack.pop()!; // Toán hạng thứ hai
const a = stack.pop()!; // Toán hạng thứ nhất
let result: number;
if (token === '+') result = a + b;
else if (token === '-') result = a - b;
else if (token === '*') result = a * b;
else result = Math.trunc(a / b); // Cắt về phía không
stack.push(result);
} else {
stack.push(parseInt(token));
}
}
return stack[0];
}
console.log(evalRPN(["2","1","+","3","*"])); // 9
console.log(evalRPN(["4","13","5","/","+"])); // 6# Python
def eval_rpn(tokens: list[str]) -> int:
stack = []
ops = {'+', '-', '*', '/'}
for token in tokens:
if token in ops:
b = stack.pop()
a = stack.pop()
if token == '+': stack.append(a + b)
elif token == '-': stack.append(a - b)
elif token == '*': stack.append(a * b)
else: stack.append(int(a / b)) # Cắt về phía không
else:
stack.append(int(token))
return stack[0]Độ phức tạp: Thời gian O(n), Không gian O(n)
Bài Toán 3: Daily Temperatures — Nhiệt Độ Hàng Ngày (Monotonic Stack)
LeetCode #739 — Cho nhiệt độ mỗi ngày, tìm xem cần bao nhiêu ngày nữa đến ngày ấm hơn. Trả về 0 nếu không có ngày ấm hơn.
Ví dụ: [73,74,75,71,69,72,76,73] → [1,1,4,2,1,1,0,0]
Ý tưởng chính: Dùng monotonic decreasing stack lưu trữ các chỉ số. Khi nhiệt độ hiện tại ấm hơn nhiệt độ ở đỉnh stack, chúng ta đã tìm được câu trả lời cho chỉ số đó.
// TypeScript - Monotonic Stack
function dailyTemperatures(temperatures: number[]): number[] {
const n = temperatures.length;
const result = new Array(n).fill(0);
const stack: number[] = []; // lưu chỉ số
for (let i = 0; i < n; i++) {
// Khi nhiệt độ hiện tại ấm hơn nhiệt độ ở đỉnh stack
while (stack.length > 0 && temperatures[i] > temperatures[stack[stack.length - 1]]) {
const idx = stack.pop()!;
result[idx] = i - idx; // số ngày đến khi ấm hơn
}
stack.push(i);
}
return result;
}
console.log(dailyTemperatures([73,74,75,71,69,72,76,73]));
// [1, 1, 4, 2, 1, 1, 0, 0]# Python
def daily_temperatures(temperatures: list[int]) -> list[int]:
n = len(temperatures)
result = [0] * n
stack = [] # lưu chỉ số
for i, temp in enumerate(temperatures):
while stack and temp > temperatures[stack[-1]]:
idx = stack.pop()
result[idx] = i - idx
stack.append(i)
return resultTrace qua [73, 74, 75, 71, 69, 72, 76, 73]:
i=0, temp=73 → stack=[] → push 0 → stack=[0]
i=1, temp=74 → 74>73(idx=0) → result[0]=1-0=1, stack=[] → push 1 → stack=[1]
i=2, temp=75 → 75>74(idx=1) → result[1]=2-1=1, stack=[] → push 2 → stack=[2]
i=3, temp=71 → 71<75 → push 3 → stack=[2,3]
i=4, temp=69 → 69<71 → push 4 → stack=[2,3,4]
i=5, temp=72 → 72>69(idx=4)→result[4]=1, 72>71(idx=3)→result[3]=2, 72<75 → push 5 → stack=[2,5]
i=6, temp=76 → 76>72(idx=5)→result[5]=1, 76>75(idx=2)→result[2]=4, stack=[] → push 6 → stack=[6]
i=7, temp=73 → 73<76 → push 7 → stack=[6,7]
Kết quả: result = [1,1,4,2,1,1,0,0]Độ phức tạp: Thời gian O(n), Không gian O(n)
Bài Toán 4: Implement Queue Using Two Stacks — Cài Đặt Queue Bằng Hai Stack (Stack + Queue)
LeetCode #232 — Cài đặt queue FIFO chỉ dùng hai stacks.
Ý tưởng chính: Hai lần đảo ngược bằng thứ tự ban đầu. Stack inbox nhận push. Stack outbox phục vụ pop — chỉ nạp lại từ inbox khi outbox rỗng (lazy transfer).
// TypeScript
class MyQueue {
private inbox: number[] = []; // cho push
private outbox: number[] = []; // cho pop/peek
push(x: number): void {
this.inbox.push(x);
}
pop(): number {
this.transfer();
return this.outbox.pop()!;
}
peek(): number {
this.transfer();
return this.outbox[this.outbox.length - 1];
}
empty(): boolean {
return this.inbox.length === 0 && this.outbox.length === 0;
}
private transfer(): void {
// Chỉ chuyển khi outbox rỗng (lazy)
if (this.outbox.length === 0) {
while (this.inbox.length > 0) {
this.outbox.push(this.inbox.pop()!);
}
}
}
}
// Sử dụng
const queue = new MyQueue();
queue.push(1);
queue.push(2);
queue.push(3);
console.log(queue.peek()); // 1 (FIFO: push đầu tiên)
console.log(queue.pop()); // 1
console.log(queue.pop()); // 2# Python
class MyQueue:
def __init__(self):
self.inbox = [] # cho push
self.outbox = [] # cho pop/peek
def push(self, x: int) -> None:
self.inbox.append(x)
def pop(self) -> int:
self._transfer()
return self.outbox.pop()
def peek(self) -> int:
self._transfer()
return self.outbox[-1]
def empty(self) -> bool:
return not self.inbox and not self.outbox
def _transfer(self) -> None:
if not self.outbox:
while self.inbox:
self.outbox.append(self.inbox.pop())Độ phức tạp amortized: Mỗi phần tử được push và pop từ mỗi stack nhiều nhất một lần → O(1) amortized mỗi thao tác.
Bài Toán 5: Sliding Window Maximum — Giá Trị Lớn Nhất Trong Cửa Sổ Trượt (Deque)
LeetCode #239 — Cho mảng nums và kích thước cửa sổ k, tìm giá trị lớn nhất trong mỗi cửa sổ trượt.
Brute force: O(n·k) — kiểm tra mỗi cửa sổ. Có thể làm O(n) không?
Ý tưởng chính: Dùng monotonic decreasing deque lưu trữ các chỉ số. Đầu luôn giữ chỉ số của giá trị lớn nhất trong cửa sổ hiện tại. Xóa từ cuối các phần tử nhỏ hơn hiện tại (chúng không bao giờ là max). Xóa từ đầu các chỉ số ngoài cửa sổ.
// TypeScript - Monotonic Deque
function maxSlidingWindow(nums: number[], k: number): number[] {
const result: number[] = [];
const deque: number[] = []; // lưu chỉ số, đầu = max
for (let i = 0; i < nums.length; i++) {
// Xóa các chỉ số ngoài cửa sổ hiện tại
while (deque.length > 0 && deque[0] < i - k + 1) {
deque.shift();
}
// Xóa các phần tử nhỏ hơn từ cuối (không thể là max)
while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
deque.push(i);
// Bắt đầu ghi kết quả khi cửa sổ đầu tiên hoàn chỉnh
if (i >= k - 1) {
result.push(nums[deque[0]]); // đầu luôn là max
}
}
return result;
}
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3));
// [3, 3, 5, 5, 6, 7]# Python
from collections import deque
def max_sliding_window(nums: list[int], k: int) -> list[int]:
result = []
dq = deque() # lưu chỉ số
for i, num in enumerate(nums):
# Xóa chỉ số ngoài cửa sổ từ đầu
while dq and dq[0] < i - k + 1:
dq.popleft()
# Xóa phần tử nhỏ hơn từ cuối
while dq and nums[dq[-1]] < num:
dq.pop()
dq.append(i)
if i >= k - 1:
result.append(nums[dq[0]])
return resultTrace qua [1,3,-1,-3,5,3,6,7], k=3:
i=0, num=1 → deque=[0]
i=1, num=3 → 3>1 xóa 0, deque=[1]
i=2, num=-1 → deque=[1,2], cửa sổ=[1,3,-1] → result=[nums[1]]=3
i=3, num=-3 → deque=[1,2,3], cửa sổ=[3,-1,-3] → result=[3,3]
i=4, num=5 → 5>-3 xóa 3, 5>-1 xóa 2, 5>3 xóa 1, deque=[4] → result=[3,3,5]
i=5, num=3 → deque=[4,5] → result=[3,3,5,nums[4]]=5
i=6, num=6 → 6>3 xóa 5, 6>5 xóa 4, deque=[6] → result=[3,3,5,5,6]
i=7, num=7 → 7>6 xóa 6, deque=[7] → result=[3,3,5,5,6,7]Độ phức tạp: Thời gian O(n), Không gian O(k)
Bài Toán 6: Min Stack — Stack với getMin O(1) (Stack)
LeetCode #155 — Thiết kế stack hỗ trợ push, pop, top, và getMin trong O(1).
Ý tưởng chính: Duy trì stack "min" thứ hai theo dõi giá trị nhỏ nhất ở mỗi mức.
// TypeScript
class MinStack {
private stack: number[] = [];
private minStack: number[] = [];
push(val: number): void {
this.stack.push(val);
const currentMin = this.minStack.length === 0
? val
: Math.min(val, this.minStack[this.minStack.length - 1]);
this.minStack.push(currentMin);
}
pop(): void {
this.stack.pop();
this.minStack.pop();
}
top(): number {
return this.stack[this.stack.length - 1];
}
getMin(): number {
return this.minStack[this.minStack.length - 1];
}
}
// Sử dụng
const minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
console.log(minStack.getMin()); // -3
minStack.pop();
console.log(minStack.top()); // 0
console.log(minStack.getMin()); // -2# Python
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, val: int) -> None:
self.stack.append(val)
current_min = val if not self.min_stack else min(val, self.min_stack[-1])
self.min_stack.append(current_min)
def pop(self) -> None:
self.stack.pop()
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def get_min(self) -> int:
return self.min_stack[-1]Độ phức tạp: Thời gian O(1) cho tất cả thao tác, Không gian O(n)
Bài Toán 7: Next Greater Element — Phần Tử Lớn Hơn Tiếp Theo (Monotonic Stack)
LeetCode #496 — Với mỗi phần tử trong nums1, tìm phần tử lớn hơn tiếp theo trong nums2. Nếu không có, trả về -1.
// TypeScript - Monotonic Stack
function nextGreaterElement(nums1: number[], nums2: number[]): number[] {
// Xây dựng map next-greater cho nums2
const nextGreater = new Map<number, number>();
const stack: number[] = [];
for (const num of nums2) {
while (stack.length > 0 && stack[stack.length - 1] < num) {
nextGreater.set(stack.pop()!, num);
}
stack.push(num);
}
// Các phần tử còn lại trong stack không có next greater → -1 (mặc định Map)
return nums1.map(n => nextGreater.get(n) ?? -1);
}
console.log(nextGreaterElement([4,1,2], [1,3,4,2])); // [-1,3,-1]
console.log(nextGreaterElement([2,4], [1,2,3,4])); // [3,-1]# Python
def next_greater_element(nums1: list[int], nums2: list[int]) -> list[int]:
next_greater = {}
stack = []
for num in nums2:
while stack and stack[-1] < num:
next_greater[stack.pop()] = num
stack.append(num)
return [next_greater.get(n, -1) for n in nums1]Độ phức tạp: Thời gian O(n + m), Không gian O(n)
6. Hướng Dẫn Nhận Biết Pattern
Biết pattern nào cần áp dụng là một nửa trận chiến trong phỏng vấn.
| Pattern bài toán | Dùng | Dấu hiệu / Từ khóa |
|---|---|---|
| Kiểm tra ngoặc | Stack | "hợp lệ", "cân bằng", dấu ngoặc |
| Lịch sử undo/redo | Stack | "undo", "quay lại", "lịch sử" |
| Đánh giá biểu thức | Stack | postfix, toán tử, ưu tiên |
| Phần tử lớn/nhỏ hơn tiếp theo | Monotonic Stack | "tiếp theo", "trước đó", "ấm hơn", "lớn hơn" |
| Xử lý theo thứ tự nhận | Queue | "thứ tự", "lập lịch", "phục vụ" |
| Duyệt BFS | Queue | theo mức, đường đi ngắn nhất |
| Max/min cửa sổ trượt | Monotonic Deque | "maximum cửa sổ", "minimum cửa sổ" |
| getMin/getMax O(1) | Stack + phụ trợ | "thời gian hằng số", "nhỏ nhất", "lớn nhất" |
| FIFO chỉ với stack ops | Hai Stack | "queue dùng stacks" |
7. Tóm Tắt Độ Phức Tạp
Stack
| Thao tác | Dựa trên mảng | Dựa trên linked list |
|---|---|---|
| push | O(1) amortized | O(1) |
| pop | O(1) amortized | O(1) |
| peek | O(1) | O(1) |
| Không gian | O(n) | O(n) |
Queue
| Thao tác | Mảng naïve | Linked list / deque |
|---|---|---|
| enqueue | O(1) amortized | O(1) |
| dequeue | ❌ O(n) | O(1) |
| front | O(1) | O(1) |
| Không gian | O(n) | O(n) |
Deque (Hàng Đợi Hai Đầu)
| Thao tác | Linked list / deque |
|---|---|
| addFront | O(1) |
| addRear | O(1) |
| removeFront | O(1) |
| removeRear | O(1) |
| Không gian | O(n) |
8. Bài Tập Luyện Tập
Làm các bài toán theo thứ tự. Mỗi bài củng cố các pattern từ bài đăng này.
Dễ
| Bài toán | Khái niệm chính | LeetCode |
|---|---|---|
| Valid Parentheses | Stack | #20 |
| Implement Stack using Queues | Hai Queue | #225 |
| Implement Queue using Stacks | Hai Stack | #232 |
| Min Stack | Stack phụ trợ | #155 |
| Next Greater Element I | Monotonic Stack | #496 |
Trung Bình
| Bài toán | Khái niệm chính | LeetCode |
|---|---|---|
| Daily Temperatures | Monotonic Stack | #739 |
| Evaluate Reverse Polish Notation | Stack | #150 |
| Decode String | Stack | #394 |
| Design Circular Queue | Circular Queue | #622 |
| Next Greater Element II | Monotonic Stack + Circular | #503 |
| Number of Visible People in a Queue | Monotonic Stack | #1944 |
Khó
| Bài toán | Khái niệm chính | LeetCode |
|---|---|---|
| Sliding Window Maximum | Monotonic Deque | #239 |
| Largest Rectangle in Histogram | Monotonic Stack | #84 |
| Trapping Rain Water | Monotonic Stack / Hai Con Trỏ | #42 |
Tóm Tắt và Điểm Mấu Chốt
Stack (LIFO):
✅ Dùng khi cần xử lý phần tử theo thứ tự ngược với khi nhận
✅ Hoàn hảo cho undo/redo, phân tích cú pháp, đánh giá biểu thức, DFS
✅ Monotonic stack giải các pattern "next greater/smaller" trong O(n)
Queue (FIFO):
✅ Dùng khi cần xử lý phần tử theo thứ tự nhận
✅ Thiết yếu cho BFS, lập lịch, buffering
✅ Luôn dùng deque trong Python — không bao giờ list.pop(0)
Deque:
✅ Tổng quát hóa cả stack và queue — thêm/xóa từ cả hai đầu
✅ Monotonic deque giải sliding window max/min trong O(n)
Các Pattern Phỏng Vấn Chính:
✅ Thấy ngoặc → nghĩ Stack
✅ Thấy "next greater element" → nghĩ Monotonic Stack
✅ Thấy BFS / theo mức → nghĩ Queue
✅ Thấy sliding window max/min → nghĩ Monotonic Deque
Tiếp Theo Là Gì?
Bạn đã hoàn thành Phase 2 của chuỗi DSA — Cấu Trúc Dữ Liệu Tuyến Tính. Tiếp theo là Phase 3: Cấu Trúc Dữ Liệu Phi Tuyến, bắt đầu với quan trọng nhất: Cây.
Tiếp tục chuỗi DSA:
- Tiếp theo: Trees & Binary Search Trees — Dữ liệu phân cấp, các thao tác BST, duyệt DFS/BFS
- Trước đó: Danh Sách Liên Kết — Dữ liệu tuần tự động, thao tác con trỏ
- Tổng quan chuỗi: Lộ Trình Data Structures & Algorithms
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.