Cấu trúc tuần tự không có index ngẫu nhiên. Học cách di chuyển con trỏ một cách cẩn thận: Fast & Slow để dò chu kỳ, đảo ngược 3-pointer, và merge với dummy node.
| Thao tác | Array | Linked List |
|---|---|---|
| Truy cập index thứ i | O(1) | O(i) |
| Thêm vào đầu | O(n) | O(1) |
| Xoá ở giữa (đã có ref) | O(n) | O(1) |
| Tìm phần tử x | O(n) | O(n) |
| Bộ nhớ phụ trên mỗi phần tử | 0 | 1 con trỏ (8 byte) |
Linked List có ưu thế khi cần thêm/xoá ở vị trí đã biết con trỏ; thua khi cần truy cập ngẫu nhiên. Trên LeetCode, đa số bài linked list là về thao tác con trỏ chứ không phải về performance.
Định nghĩa chuẩn dùng ở mọi bài LeetCode:
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Tạo list 1 → 2 → 3
head = ListNode(1, ListNode(2, ListNode(3)))
# Duyệt
cur = head
while cur:
print(cur.val)
cur = cur.next
head — đầu danh sách (không thay đổi).cur hoặc node — con trỏ đang chạy.prev, next_node — khi cần ba con trỏ.dummy — node giả đứng trước head (xem 1.3).Tạo một node "giả" đứng trước head để loại bỏ trường hợp đặc biệt khi xử lý đầu danh sách.
# Xoá mọi node có val == target
def removeElements(head, target):
while head and head.val == target: # case 1: xoá ở head
head = head.next
cur = head
while cur and cur.next: # case 2: xoá ở giữa
if cur.next.val == target:
cur.next = cur.next.next
else:
cur = cur.next
return head
def removeElements(head, target):
dummy = ListNode(0, head)
cur = dummy
while cur.next:
if cur.next.val == target:
cur.next = cur.next.next
else:
cur = cur.next
return dummy.next
Code ngắn hơn, ít bug hơn. Quy tắc: bài nào có thể thay đổi head → dùng dummy.
cur.next = new_node # nếu chưa lưu cur.next cũ trước, mất phần đuôi list!
# Đúng:
tmp = cur.next
cur.next = new_node
new_node.next = tmpcur and cur.next trước khi truy cập cur.next.val.node.next = head là tạo chu kỳ.dummy.next chứ không phải dummy hay head (head có thể đã bị xoá).slow — đi 1 bước mỗi lần.fast — đi 2 bước mỗi lần.fast tới None → slow đang ở giữa.Đây là phiên bản "trên linked list" của detect cycle đã giới thiệu ở Tuần 3 (Happy Number).
Đề (LeetCode #876): Cho head của linked list. Trả về node ở giữa. Nếu chẵn, trả node giữa thứ hai.
Pass 1: đếm n. Pass 2: đi n/2 bước. → 2 lần duyệt.
def middleNode(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
1 → 2 → 3 → 4 → 5| Bước | slow | fast |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 2 | 3 |
| 2 | 3 | 5 |
| 3 | — | fast.next = None → dừng |
Trả về node 3. Time O(n), Space O(1).
fast and fast.next?Đề (LeetCode #141): Cho head. Trả True nếu danh sách có chu kỳ.
def hasCycle(head):
seen = set()
cur = head
while cur:
if cur in seen: return True
seen.add(cur)
cur = cur.next
return False
# Time O(n), Space O(n)
def hasCycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
# Time O(n), Space O(1)
Tưởng tượng hai vận động viên chạy trên đường đua tròn. Một người chạy 1 ô/giây, một người 2 ô/giây. Người chạy nhanh sẽ vượt người chạy chậm tròn vòng. Khi vượt, hai người gặp nhau.
Trong chu kỳ, khoảng cách fast − slow tăng đúng 1 mỗi bước (mod cycle length). Sau hữu hạn bước, khoảng cách = cycle length ≡ 0 → fast bắt kịp slow.
Đề (LeetCode #142): Trả về node bắt đầu chu kỳ. None nếu không có.
def detectCycle(head):
slow = fast = head
# Pha 1: phát hiện chu kỳ
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None # không gặp nhau → không có chu kỳ
if not (fast and fast.next):
return None
# Pha 2: tìm node bắt đầu
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
Time O(n), Space O(1).
Đây là chứng minh kinh điển — sinh viên cần hiểu để không phải học vẹt.
L1 = quãng đường từ head đến node bắt đầu chu kỳ.L2 = quãng đường từ node bắt đầu đến nơi slow gặp fast.C = chiều dài chu kỳ.Khi gặp nhau, slow đã đi L1 + L2 bước. fast đã đi gấp đôi:
2(L1 + L2). Đồng thời fast đi nhiều hơn slow đúng k vòng chu kỳ:
Tức: từ điểm gặp nhau, đi thêm L1 bước (= kC − L2 bước) sẽ tới node bắt đầu chu kỳ.
Đó là lý do: sau pha 1, đặt slow về head, cả hai đi cùng tốc độ 1 bước/lần — sẽ gặp ở node bắt đầu.
Tuần 3 đã làm bằng set với O(log n) space. Giờ áp dụng Fast & Slow để giảm xuống O(1) space.
Quan sát: hàm "tổng bình phương chữ số" tạo ra một chuỗi giá trị như linked list ảo. Phát hiện chu kỳ trên chuỗi này tương đương với phát hiện chu kỳ trên linked list.
def isHappy(n):
def step(x):
s = 0
while x:
x, d = divmod(x, 10)
s += d * d
return s
slow = n
fast = step(n)
while fast != 1 and slow != fast:
slow = step(slow)
fast = step(step(fast))
return fast == 1
Time O(log n), Space O(1).
while fast and fast.next kiểm tra gì?L1 = kC - L2 để đưa hai pointer đến cycle start cùng lúc.Đảo linked list = lật ngược hướng của mọi next pointer. Cần 3 con trỏ cùng lúc:
prev — node phía sau (theo hướng đảo).cur — node đang xử lý.next_node — lưu node kế trước khi cắt.next_node = cur.next # lưu kẻo mất
cur.next = prev # lật ngược
prev = cur # tiến prev
cur = next_node # tiến cur
Đề (LeetCode #206): Đảo ngược linked list, trả về head mới.
def reverseList(head):
prev = None
cur = head
while cur:
next_node = cur.next
cur.next = prev
prev = cur
cur = next_node
return prev
1 → 2 → 3 → None| Bước | prev | cur | next_node | Trạng thái sau |
|---|---|---|---|---|
| 0 | None | 1 | — | 1 → 2 → 3 → None |
| 1 | 1 | 2 | 2 | None ← 1, 2 → 3 → None |
| 2 | 2 | 3 | 3 | None ← 1 ← 2, 3 → None |
| 3 | 3 | None | None | None ← 1 ← 2 ← 3 |
Trả về prev = 3. Time O(n), Space O(1).
def reverseList(head):
if not head or not head.next:
return head
new_head = reverseList(head.next)
head.next.next = head # lật pointer
head.next = None # cắt nối cũ
return new_head
Ý tưởng: giả sử đã đảo được phần đuôi head.next → ... → None thành ... → head.next.
Bây giờ chỉ cần nối head.next.next = head và cắt head.next = None.
| Aspect | Iterative | Recursive |
|---|---|---|
| Time | O(n) | O(n) |
| Space | O(1) | O(n) call stack |
| Đọc dễ? | Trung bình | Khó hơn — cần hiểu đệ quy |
| Phỏng vấn ưu tiên | Thường ưu tiên iterative cho space | Đôi khi yêu cầu cả hai |
Đề (LeetCode #92): Đảo ngược các node từ vị trí left đến right (1-indexed). Một lần duyệt.
def reverseBetween(head, left, right):
dummy = ListNode(0, head)
prev = dummy
# Bước 1: di chuyển prev tới node đứng trước vị trí left
for _ in range(left - 1):
prev = prev.next
# Bước 2: đảo (right - left) liên kết bằng "head insertion"
cur = prev.next
for _ in range(right - left):
next_node = cur.next
cur.next = next_node.next
next_node.next = prev.next
prev.next = next_node
return dummy.next
Time O(n), Space O(1).
Đề (LeetCode #234): Trả True nếu linked list là palindrome. Yêu cầu O(1) bộ nhớ phụ.
def isPalindrome(head):
# 1. Tìm middle
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 2. Đảo nửa sau (bắt đầu từ slow)
prev = None
cur = slow
while cur:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
# 3. So sánh
left, right = head, prev
while right:
if left.val != right.val: return False
left = left.next
right = right.next
return True
Time O(n), Space O(1).
next_node?cur.next vào biến tạm trước khi gán. Quên là lỗi phổ biến nhất khi viết reverse.dummy = ListNode(0)
tail = dummy
while <còn nguồn>:
chosen = pick_smallest(...)
tail.next = chosen
tail = tail.next
advance source pointer
tail.next = remaining # nối phần dư
return dummy.next
Ý tưởng: dummy là điểm tựa, tail luôn trỏ đến node cuối của output. Chỉ cần "móc" node mới vào tail.next rồi tiến tail.
dummy.next.Đề (LeetCode #21): Cho hai linked list đã sort. Merge thành một list sort, dùng các node có sẵn (không tạo node mới).
def mergeTwoLists(l1, l2):
dummy = ListNode(0)
tail = dummy
while l1 and l2:
if l1.val <= l2.val:
tail.next = l1
l1 = l1.next
else:
tail.next = l2
l2 = l2.next
tail = tail.next
tail.next = l1 if l1 else l2 # nối phần dư
return dummy.next
l1: 1→3→5, l2: 2→4| Bước | l1.val | l2.val | chosen | output sau |
|---|---|---|---|---|
| 1 | 1 | 2 | l1=1 | dummy→1 |
| 2 | 3 | 2 | l2=2 | dummy→1→2 |
| 3 | 3 | 4 | l1=3 | dummy→1→2→3 |
| 4 | 5 | 4 | l2=4 | dummy→1→2→3→4 |
| 5 | 5 | None | nối phần dư l1 | dummy→1→2→3→4→5 |
Time O(n+m), Space O(1).
Đề (LeetCode #19): Xoá node thứ n tính từ cuối. Một lần duyệt.
Đây là biến thể của Fast & Slow — nhưng cùng tốc độ với khoảng cách cố định n. Khi fast tới cuối, slow đang ở vị trí cần xoá.
def removeNthFromEnd(head, n):
dummy = ListNode(0, head)
fast = slow = dummy
# Đẩy fast đi n+1 bước
for _ in range(n + 1):
fast = fast.next
# Cùng đi đến khi fast = None
while fast:
fast = fast.next
slow = slow.next
# slow.next là node cần xoá
slow.next = slow.next.next
return dummy.next
Để slow dừng ở node trước node cần xoá (cần ref để cắt). Khoảng cách fast − slow = n+1 → khi fast = None, slow ở vị trí n+1 từ cuối → tức trước node thứ n.
Time O(n) một lần duyệt, Space O(1).
Đề (LeetCode #143): Cho L: L₀ → L₁ → ... → Lₙ₋₁ → Lₙ. Đổi thành L₀ → Lₙ → L₁ → Lₙ₋₁ → L₂ → ... Tại chỗ.
Ví dụ: 1→2→3→4→5 → 1→5→2→4→3.
def reorderList(head):
if not head or not head.next: return
# PHA 1: tìm middle
slow = fast = head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
# PHA 2: đảo nửa sau
prev, cur = None, slow.next
slow.next = None # cắt list thành 2 nửa
while cur:
nxt = cur.next; cur.next = prev
prev = cur; cur = nxt
# PHA 3: đan xen 2 list
first, second = head, prev
while second:
tmp1, tmp2 = first.next, second.next
first.next = second
second.next = tmp1
first, second = tmp1, tmp2
Time O(n), Space O(1).
| Tín hiệu trong đề | Pattern phù hợp |
|---|---|
| "Tìm middle", "tìm node thứ n từ cuối" | Fast & Slow / Two pointer with gap |
| "Có chu kỳ không?", "tìm cycle start" | Floyd's Algorithm |
| "Đảo ngược toàn bộ", "đảo từng cụm k node" | 3-pointer Reverse |
| "Đảo một đoạn từ left đến right" | Dummy + Head Insertion |
| "Merge sorted lists" | Dummy Node + Merge |
| "Xoá theo điều kiện" | Dummy Node + duyệt prev/cur |
| "Palindrome", "đan xen" | Combo: fast/slow + reverse + merge |
| "Sort linked list" | Merge sort linked list (Tuần 8) |
cur.next.next không? → Cần check cur.next.Stack & Queue — Monotonic Stack và BFS Preparation
Hai cấu trúc dữ liệu cơ bản nhưng có ứng dụng sâu sắc. Học pattern Monotonic Stack để giải Daily Temperatures, Next Greater Element, Trapping Rain Water (lời giải O(1) space). Queue + deque sẽ chuẩn bị cho BFS ở Tuần 10. Sliding Window Maximum (#239) là bài "boss" kết hợp deque monotonic với sliding window đã học.
Nhấn T hoặc Esc để đóng