Hai cấu trúc dữ liệu cơ bản với ứng dụng nâng cao. Monotonic Stack là pattern then chốt giải bài "next greater/smaller" trong O(n).
Python dùng list làm stack qua append và pop:
stack = []
stack.append(x) # push O(1) amortised
stack.pop() # pop top O(1)
stack[-1] # peek O(1)
len(stack) == 0 # check empty
stack[0] # SAI — đây là bottom, không phải top
stack.pop(0) # SAI — pop ở đầu là O(n), không phải O(1)
append, pop, [-1]).
Nếu cần thao tác cả hai đầu, dùng collections.deque để giữ O(1).
list.pop(0) là O(n) → không phù hợp cho queue. Dùng deque:
from collections import deque
q = deque()
q.append(x) # enqueue O(1)
q.popleft() # dequeue O(1)
q[0] # front O(1)
# deque cũng hỗ trợ thao tác hai đầu:
q.appendleft(x) # O(1)
q.pop() # O(1) — pop ở phải
Deque cài đặt bằng doubly linked list của block. Mọi thao tác ở hai đầu là O(1). Đây cũng là cấu trúc dùng cho Monotonic Deque (Phần 4).
| Thao tác | list (stack) | deque (queue/two-end) |
|---|---|---|
| append (cuối) | O(1) amortised | O(1) |
| pop (cuối) | O(1) | O(1) |
| appendleft (đầu) | — | O(1) |
| popleft (đầu) | O(n) — list.pop(0) | O(1) |
| Truy cập index giữa | O(1) | O(n) |
| Bộ nhớ tương đối | Thấp | Cao hơn ~30% |
Đa số bài thuật toán không quan tâm "truy cập index giữa" — nên deque ổn cho cả stack và queue.
Tuy nhiên, vì list nhẹ hơn, đa số code Python idiom dùng list cho stack
thuần và deque chỉ khi cần thao tác hai đầu.
Pattern này áp dụng cho: parentheses, brackets, HTML tags, function call simulation, undo/redo, expression evaluation.
Đề (LeetCode #20): Cho string chỉ gồm ()[]{}. Trả True nếu mọi ngoặc khớp đúng.
def isValid(s):
pair = {')': '(', ']': '[', '}': '{'}
stack = []
for ch in s:
if ch in '([{':
stack.append(ch)
else:
if not stack or stack[-1] != pair[ch]:
return False
stack.pop()
return len(stack) == 0
"({[]})"| Bước | ch | Hành động | Stack sau |
|---|---|---|---|
| 1 | ( | push | [(] |
| 2 | { | push | [(, {] |
| 3 | [ | push | [(, {, [] |
| 4 | ] | top = [, khớp → pop | [(, {] |
| 5 | } | top = {, khớp → pop | [(] |
| 6 | ) | top = (, khớp → pop | [] |
Stack rỗng → True. Time O(n), Space O(n).
Đề (LeetCode #155): Thiết kế stack hỗ trợ thêm getMin() trong O(1).
Stack thường có push/pop/top O(1), nhưng tìm min là O(n). Làm sao biến min cũng thành O(1)?
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = [] # đỉnh = min hiện tại
def push(self, x):
self.stack.append(x)
m = min(x, self.min_stack[-1] if self.min_stack else x)
self.min_stack.append(m)
def pop(self):
self.stack.pop()
self.min_stack.pop()
def top(self):
return self.stack[-1]
def getMin(self):
return self.min_stack[-1]
Mọi thao tác O(1). Space O(n).
Đề (LeetCode #150): Tính giá trị biểu thức RPN.
Ví dụ: ["2","1","+","3","*"] = (2+1) * 3 = 9.
def evalRPN(tokens):
stack = []
ops = {'+', '-', '*', '/'}
for t in tokens:
if t in ops:
b = stack.pop()
a = stack.pop()
if t == '+': stack.append(a + b)
elif t == '-': stack.append(a - b)
elif t == '*': stack.append(a * b)
else: stack.append(int(a / b)) # Python truncate khác C++
else:
stack.append(int(t))
return stack[0]
RPN là dạng "tự nhiên" cho stack: số → push, toán tử → pop 2 số, tính, push lại. Không có ngoặc, không có ưu tiên — đơn giản hoá hoàn toàn.
Time O(n), Space O(n).
// chia làm tròn xuống cho số âm
(−7 // 2 = −4 thay vì −3). Dùng int(a / b) để truncate đúng.
| Biến thể | Quy tắc | Dùng cho |
|---|---|---|
| Decreasing (đỉnh nhỏ nhất) | Pop khi top < x | Next Greater Element |
| Increasing (đỉnh lớn nhất) | Pop khi top > x | Next Smaller Element |
→ Tổng số thao tác qua toàn thuật toán ≤ 2n → O(n) (phân tích amortised).
Khi đọc đề, các "mùi" sau gợi ý mạnh:
| Cụm từ trong đề | Pattern |
|---|---|
| "next greater element" | Decreasing stack |
| "next smaller element" | Increasing stack |
| "days until warmer temperature" | Decreasing stack |
| "largest rectangle in histogram" | Increasing stack (boundary trick) |
| "trapping rain water" (O(1) variant) | Two pointer (hoặc Monotonic) |
| "sliding window max/min" | Monotonic Deque (Phần 4) |
Đề (LeetCode #739): Mảng temps. Trả mảng res với
res[i] = số ngày phải chờ đến ngày có nhiệt độ cao hơn. 0 nếu không có.
Ví dụ: [73,74,75,71,69,72,76,73] → [1,1,4,2,1,1,0,0].
Với mỗi i, scan j > i tìm temp lớn hơn → O(n²). Với n = 10⁵ → TLE.
def dailyTemperatures(temps):
res = [0] * len(temps)
stack = [] # lưu index
for i, t in enumerate(temps):
while stack and temps[stack[-1]] < t:
j = stack.pop()
res[j] = i - j # tìm thấy "next greater"
stack.append(i)
return res
Time O(n), Space O(n).
temps = [73, 74, 75, 71, 69, 72, 76, 73]
| i | t | Stack trước | Pop & gán | Stack sau push | res sau |
|---|---|---|---|---|---|
| 0 | 73 | [] | — | [0] | [0,0,0,0,0,0,0,0] |
| 1 | 74 | [0] | pop 0, res[0]=1 | [1] | [1,0,0,0,0,0,0,0] |
| 2 | 75 | [1] | pop 1, res[1]=1 | [2] | [1,1,0,0,0,0,0,0] |
| 3 | 71 | [2] | — | [2,3] | [1,1,0,0,0,0,0,0] |
| 4 | 69 | [2,3] | — | [2,3,4] | [1,1,0,0,0,0,0,0] |
| 5 | 72 | [2,3,4] | pop 4 (res[4]=1), pop 3 (res[3]=2) | [2,5] | [1,1,0,2,1,0,0,0] |
| 6 | 76 | [2,5] | pop 5 (res[5]=1), pop 2 (res[2]=4) | [6] | [1,1,4,2,1,1,0,0] |
| 7 | 73 | [6] | — | [6,7] | [1,1,4,2,1,1,0,0] |
Index còn trên stack ([6,7]) là không có "next greater" → res = 0.
Lo lắng: vòng while bên trong for có thể tốn nhiều bước → tổng có thể O(n²)?
Mỗi index i:
Tổng số thao tác push + pop qua n vòng for ≤ 2n. Do đó tổng thời gian là O(n)
bất kể vòng while có vẻ tốn nhiều.
Đề (LeetCode #503): Mảng circular (vòng tròn). Tìm next greater cho mỗi phần tử. Nếu không có, trả -1.
Ví dụ: [1,2,1] → [2,-1,2] (phần tử cuối quay lại từ đầu).
Để mô phỏng vòng tròn, duyệt index từ 0 đến 2n−1, dùng i % n để truy cập:
def nextGreaterElements(nums):
n = len(nums)
res = [-1] * n
stack = []
for i in range(2 * n):
x = nums[i % n]
while stack and nums[stack[-1]] < x:
res[stack.pop()] = x
if i < n: # chỉ push trong vòng đầu
stack.append(i)
return res
Time O(n), Space O(n).
Đề (LeetCode #84, Hard): Mảng heights các cột. Tìm hình chữ nhật diện tích lớn nhất chứa trong histogram.
Ví dụ: [2,1,5,6,2,3] → 10 (cột 5 và 6 cao 5 → 5×2 = 10).
Hình chữ nhật lớn nhất "đặt trên" cột i có:
heights[i]."Cột thấp hơn gần nhất" = bài Next Smaller. Dùng Monotonic Increasing Stack.
def largestRectangleArea(heights):
stack = [] # index, monotonic increasing
best = 0
heights.append(0) # sentinel để đẩy hết stack ở cuối
for i, h in enumerate(heights):
while stack and heights[stack[-1]] > h:
top = stack.pop()
left = stack[-1] if stack else -1
width = i - left - 1
best = max(best, heights[top] * width)
stack.append(i)
return best
heights.append(0)Sau vòng for thường, stack có thể còn phần tử (các cột chưa "gặp cột thấp hơn"). Append 0 ở cuối buộc mọi cột pop ra để xử lý. Không có sentinel sẽ cần xử lý sau vòng for.
width = i - left - 1?Khi pop top:
i là cột thấp hơn gần nhất bên phải của top.stack[-1] (nếu có) là cột thấp hơn gần nhất bên trái.top "thống trị" toàn bộ khoảng (left, i), tức i − left − 1 cột.[2,1,5,6,2,3,0] (đã append sentinel)| i | h | Pop nào? | width | area | best |
|---|---|---|---|---|---|
| 0 | 2 | — | — | — | 0 |
| 1 | 1 | pop 0 (h=2) | 1−(−1)−1=1 | 2×1=2 | 2 |
| 2 | 5 | — | — | — | 2 |
| 3 | 6 | — | — | — | 2 |
| 4 | 2 | pop 3 (h=6); pop 2 (h=5) | 1; 2 | 6; 10 | 10 |
| 5 | 3 | — | — | — | 10 |
| 6 | 0 | pop tất cả | ... | ... | 10 |
Tuần 2 đã giải #42 bằng Prefix Max O(n) time, O(n) space. Giờ giải bằng Monotonic Stack — đây là approach phổ biến trong phỏng vấn.
Duy trì decreasing stack các index. Khi gặp cột cao hơn đỉnh stack, phần "vũng" được tính:
def trap(height):
stack = []
water = 0
for i, h in enumerate(height):
while stack and height[stack[-1]] < h:
bottom = stack.pop()
if not stack: break
left = stack[-1]
width = i - left - 1
bounded_height = min(height[left], h) - height[bottom]
water += width * bounded_height
stack.append(i)
return water
Time O(n), Space O(n) — cùng O(n) như Prefix Max nhưng tinh thần khác.
Approach thứ ba, O(1) space, kết hợp tinh thần Container With Most Water (Tuần 2):
def trap(height):
left, right = 0, len(height) - 1
left_max = right_max = 0
water = 0
while left < right:
if height[left] < height[right]:
if height[left] >= left_max:
left_max = height[left]
else:
water += left_max - height[left]
left += 1
else:
if height[right] >= right_max:
right_max = height[right]
else:
water += right_max - height[right]
right -= 1
return water
Tại mỗi bước, dịch pointer của bên thấp hơn — vì bên đó "biết" nước có thể đọng đến đâu (giới hạn bởi
left_max hoặc right_max đã thấy). Bên cao hơn vẫn còn "chưa chắc" — không thể quyết định.
Cùng tinh thần "luôn dịch cột thấp hơn" như Container With Most Water.
heights.append(0)?Monotonic Stack chỉ thao tác một đầu (top). Nhưng có bài cần thao tác cả hai đầu:
Đây là khi collections.deque phát huy: O(1) cả hai đầu.
Bài này là "boss" của tuần — kết hợp 2 tuần kỹ thuật:
Đề (LeetCode #239, Hard): Mảng nums, số k. Trả mảng max của mọi cửa sổ size k.
Constraints: 1 ≤ n ≤ 10⁵ → cần O(n).
Với mỗi cửa sổ, tìm max → O(n × k). Với n,k = 10⁵ → TLE.
from collections import deque
def maxSlidingWindow(nums, k):
dq = deque() # lưu index, decreasing theo nums[index]
res = []
for i, x in enumerate(nums):
# 1. Bỏ index ngoài cửa sổ ở đầu trái
while dq and dq[0] < i - k + 1:
dq.popleft()
# 2. Duy trì monotonic decreasing — pop các phần tử nhỏ hơn x ở phải
while dq and nums[dq[-1]] < x:
dq.pop()
# 3. Thêm i ở phải
dq.append(i)
# 4. Đầu trái = max của cửa sổ hiện tại
if i >= k - 1:
res.append(nums[dq[0]])
return res
Time O(n), Space O(k).
nums = [1,3,-1,-3,5,3,6,7], k = 3. Kỳ vọng: [3,3,5,5,6,7].
| i | x | Bỏ ngoài cửa sổ? | Pop phải | dq sau | res |
|---|---|---|---|---|---|
| 0 | 1 | — | — | [0] | — |
| 1 | 3 | — | pop 0 (1<3) | [1] | — |
| 2 | -1 | — | — | [1,2] | 3 |
| 3 | -3 | — | — | [1,2,3] | 3 |
| 4 | 5 | popleft 1 (1<2) | pop 3 (-3<5), pop 2 (-1<5) | [4] | 5 |
| 5 | 3 | — | — | [4,5] | 5 |
| 6 | 6 | — | pop 5 (3<6), pop 4 (5<6) | [6] | 6 |
| 7 | 7 | — | pop 6 (6<7) | [7] | 7 |
Kết quả: [3,3,5,5,6,7].
from collections import deque
q = deque([start])
visited = {start}
while q:
node = q.popleft()
for neighbor in neighbors(node):
if neighbor not in visited:
visited.add(neighbor)
q.append(neighbor)
FIFO của queue đảm bảo BFS thăm nodes theo thứ tự khoảng cách từ start tăng dần. Đây là nền tảng của:
Tuần 10 sẽ đào sâu — tuần này chỉ giới thiệu để sinh viên không bỡ ngỡ với deque.
Đề (LeetCode #232): Cài queue chỉ dùng 2 stack. push, pop, peek, empty. pop và peek cần O(1) amortised.
class MyQueue:
def __init__(self):
self.in_stack = []
self.out_stack = []
def push(self, x):
self.in_stack.append(x)
def pop(self):
self._move()
return self.out_stack.pop()
def peek(self):
self._move()
return self.out_stack[-1]
def empty(self):
return not self.in_stack and not self.out_stack
def _move(self):
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
Mỗi phần tử được "di chuyển" giữa hai stack tối đa 1 lần (in → out). Push O(1) thường;
pop/peek O(1) amortised vì khi _move tốn O(n), nó chỉ xảy ra khi out_stack rỗng.
Binary Search — Trên Array và Trên Answer Space
Pattern cổ điển nhưng cực mạnh — biến O(n) thành O(log n). Học template chuẩn để tránh off-by-one, Binary Search trên rotated array (mẹo duyệt 2 lần áp dụng tiếp), và đặc biệt là kỹ thuật Binary Search trên answer space để giải Capacity to Ship Packages, Koko Eating Bananas, Median of Two Sorted Arrays. Đây là kỹ thuật nâng cao nhất của tuần và xuất hiện thường xuyên trong phỏng vấn senior.
Nhấn T hoặc Esc để đóng