Linked Lists: Cấu trúc Dữ liệu Tuần tự Động

Giới thiệu
Linked lists là một trong những cấu trúc dữ liệu quan trọng nhất trong khoa học máy tính. Khác với arrays lưu trữ phần tử liên tục trong bộ nhớ, linked lists sử dụng các node kết nối bằng con trỏ, giúp chèn và xóa hiệu quả.
Hiểu linked lists là rất quan trọng cho phỏng vấn coding — chúng kiểm tra khả năng thao tác con trỏ, xử lý edge cases, và tư duy về bộ nhớ. Nhiều bài toán phỏng vấn kinh điển (đảo ngược list, phát hiện cycle, merge sorted lists) đều dựa vào linked list.
Bạn Sẽ Học Được
✅ Hiểu cách linked lists hoạt động bên trong
✅ Triển khai singly và doubly linked lists từ đầu
✅ Nắm vững kỹ thuật thao tác con trỏ
✅ Áp dụng mẫu Fast & Slow pointer
✅ Đảo ngược linked lists theo cách lặp và đệ quy
✅ Giải các bài toán phỏng vấn kinh điển từng bước
Điều kiện tiên quyết
- Hiểu về Big O Notation & Phân tích Độ phức tạp
- Quen thuộc với Mẫu Giải quyết Vấn đề
- Hoàn thành Arrays & Strings
- Kiến thức lập trình cơ bản trong TypeScript hoặc Python
1. Linked List là gì?
Linked list là cấu trúc dữ liệu tuyến tính, mỗi phần tử (gọi là node) chứa:
- Data — giá trị được lưu
- Con trỏ — tham chiếu đến node tiếp theo (và có thể node trước)
Khác với arrays, các node của linked list nằm rải rác trong bộ nhớ và kết nối qua con trỏ.
Arrays vs Linked Lists
Array (bộ nhớ liên tục):
Linked List (bộ nhớ rải rác):
So sánh Chính
| Thao tác | Array | Linked List |
|---|---|---|
| Truy cập theo index | O(1) | O(n) |
| Chèn vào đầu | O(n) | O(1) |
| Chèn vào cuối | O(1)* | O(n)† / O(1)‡ |
| Chèn vào giữa | O(n) | O(1)§ |
| Xóa ở đầu | O(n) | O(1) |
| Xóa ở giữa | O(n) | O(1)§ |
| Tìm kiếm | O(n) | O(n) |
| Bộ nhớ | Liên tục, nhỏ gọn | Rải rác, thêm overhead con trỏ |
*Amortized cho dynamic arrays | †Không có tail pointer | ‡Có tail pointer | §Nếu đã có tham chiếu node
Khi nào dùng linked lists thay arrays:
- Chèn/xóa thường xuyên ở đầu danh sách
- Kích thước không xác định (không cần resize)
- Không cần truy cập ngẫu nhiên theo index
- Triển khai stacks, queues, hoặc hash table chaining
2. Singly Linked List
Dạng đơn giản nhất — mỗi node trỏ đến node tiếp theo, node cuối trỏ đến null.
Cấu trúc Node
// TypeScript - Singly Linked List Node
class ListNode<T> {
val: T;
next: ListNode<T> | null;
constructor(val: T, next: ListNode<T> | null = null) {
this.val = val;
this.next = next;
}
}# Python - Singly Linked List Node
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = nextTriển khai Đầy đủ
// TypeScript - Singly Linked List
class SinglyLinkedList<T> {
head: ListNode<T> | null = null;
size: number = 0;
// Chèn vào đầu - O(1)
prepend(val: T): void {
const newNode = new ListNode(val, this.head);
this.head = newNode;
this.size++;
}
// Chèn vào cuối - O(n)
append(val: T): void {
const newNode = new ListNode(val);
if (!this.head) {
this.head = newNode;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
this.size++;
}
// Chèn vào vị trí cụ thể - O(n)
insertAt(val: T, index: number): void {
if (index < 0 || index > this.size) {
throw new Error("Index out of bounds");
}
if (index === 0) {
this.prepend(val);
return;
}
const newNode = new ListNode(val);
let current = this.head;
for (let i = 0; i < index - 1; i++) {
current = current!.next;
}
newNode.next = current!.next;
current!.next = newNode;
this.size++;
}
// Xóa theo giá trị - O(n)
delete(val: T): boolean {
if (!this.head) return false;
// Nếu node head chứa giá trị
if (this.head.val === val) {
this.head = this.head.next;
this.size--;
return true;
}
let current = this.head;
while (current.next) {
if (current.next.val === val) {
current.next = current.next.next;
this.size--;
return true;
}
current = current.next;
}
return false;
}
// Xóa theo index - O(n)
deleteAt(index: number): T {
if (index < 0 || index >= this.size) {
throw new Error("Index out of bounds");
}
let removedVal: T;
if (index === 0) {
removedVal = this.head!.val;
this.head = this.head!.next;
} else {
let current = this.head;
for (let i = 0; i < index - 1; i++) {
current = current!.next;
}
removedVal = current!.next!.val;
current!.next = current!.next!.next;
}
this.size--;
return removedVal;
}
// Tìm kiếm - O(n)
search(val: T): number {
let current = this.head;
let index = 0;
while (current) {
if (current.val === val) return index;
current = current.next;
index++;
}
return -1; // Không tìm thấy
}
// Lấy giá trị theo index - O(n)
get(index: number): T {
if (index < 0 || index >= this.size) {
throw new Error("Index out of bounds");
}
let current = this.head;
for (let i = 0; i < index; i++) {
current = current!.next;
}
return current!.val;
}
// In danh sách
print(): string {
const values: T[] = [];
let current = this.head;
while (current) {
values.push(current.val);
current = current.next;
}
return values.join(" -> ") + " -> null";
}
}
// Sử dụng
const list = new SinglyLinkedList<number>();
list.append(10);
list.append(20);
list.append(30);
list.prepend(5);
console.log(list.print()); // 5 -> 10 -> 20 -> 30 -> null
list.insertAt(15, 2);
console.log(list.print()); // 5 -> 10 -> 15 -> 20 -> 30 -> null
list.delete(15);
console.log(list.print()); // 5 -> 10 -> 20 -> 30 -> null# Python - Singly Linked List
class SinglyLinkedList:
def __init__(self):
self.head = None
self.size = 0
# Chèn vào đầu - O(1)
def prepend(self, val):
new_node = ListNode(val, self.head)
self.head = new_node
self.size += 1
# Chèn vào cuối - O(n)
def append(self, val):
new_node = ListNode(val)
if not self.head:
self.head = new_node
else:
current = self.head
while current.next:
current = current.next
current.next = new_node
self.size += 1
# Chèn vào vị trí cụ thể - O(n)
def insert_at(self, val, index):
if index < 0 or index > self.size:
raise IndexError("Index out of bounds")
if index == 0:
self.prepend(val)
return
new_node = ListNode(val)
current = self.head
for _ in range(index - 1):
current = current.next
new_node.next = current.next
current.next = new_node
self.size += 1
# Xóa theo giá trị - O(n)
def delete(self, val):
if not self.head:
return False
if self.head.val == val:
self.head = self.head.next
self.size -= 1
return True
current = self.head
while current.next:
if current.next.val == val:
current.next = current.next.next
self.size -= 1
return True
current = current.next
return False
# Tìm kiếm - O(n)
def search(self, val):
current = self.head
index = 0
while current:
if current.val == val:
return index
current = current.next
index += 1
return -1
# In danh sách
def print_list(self):
values = []
current = self.head
while current:
values.append(str(current.val))
current = current.next
return " -> ".join(values) + " -> None"
# Sử dụng
lst = SinglyLinkedList()
lst.append(10)
lst.append(20)
lst.append(30)
lst.prepend(5)
print(lst.print_list()) # 5 -> 10 -> 20 -> 30 -> None
lst.insert_at(15, 2)
print(lst.print_list()) # 5 -> 10 -> 15 -> 20 -> 30 -> None
lst.delete(15)
print(lst.print_list()) # 5 -> 10 -> 20 -> 30 -> NoneMinh họa Chèn & Xóa
Chèn vào đầu (prepend):
Trước:
Sau prepend(5):
Xóa node có giá trị 20:
Trước:
Sau delete(20):
3. Doubly Linked List
Mỗi node có con trỏ đến cả node tiếp theo và node trước, cho phép duyệt hai chiều.
Cấu trúc Node
// TypeScript - Doubly Linked List Node
class DoublyNode<T> {
val: T;
next: DoublyNode<T> | null;
prev: DoublyNode<T> | null;
constructor(val: T) {
this.val = val;
this.next = null;
this.prev = null;
}
}# Python - Doubly Linked List Node
class DoublyNode:
def __init__(self, val=0):
self.val = val
self.next = None
self.prev = NoneTriển khai Đầy đủ
// TypeScript - Doubly Linked List
class DoublyLinkedList<T> {
head: DoublyNode<T> | null = null;
tail: DoublyNode<T> | null = null;
size: number = 0;
// Chèn vào đầu - O(1)
prepend(val: T): void {
const newNode = new DoublyNode(val);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.next = this.head;
this.head.prev = newNode;
this.head = newNode;
}
this.size++;
}
// Chèn vào cuối - O(1) với tail pointer!
append(val: T): void {
const newNode = new DoublyNode(val);
if (!this.tail) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode;
}
this.size++;
}
// Xóa node - O(1) khi có tham chiếu node
deleteNode(node: DoublyNode<T>): void {
if (node.prev) {
node.prev.next = node.next;
} else {
this.head = node.next; // Đang xóa head
}
if (node.next) {
node.next.prev = node.prev;
} else {
this.tail = node.prev; // Đang xóa tail
}
this.size--;
}
// Xóa từ đầu - O(1)
deleteFirst(): T | null {
if (!this.head) return null;
const val = this.head.val;
this.deleteNode(this.head);
return val;
}
// Xóa từ cuối - O(1) với tail pointer!
deleteLast(): T | null {
if (!this.tail) return null;
const val = this.tail.val;
this.deleteNode(this.tail);
return val;
}
// In tiến
printForward(): string {
const values: T[] = [];
let current = this.head;
while (current) {
values.push(current.val);
current = current.next;
}
return "null <-> " + values.join(" <-> ") + " <-> null";
}
// In lùi
printBackward(): string {
const values: T[] = [];
let current = this.tail;
while (current) {
values.push(current.val);
current = current.prev;
}
return "null <-> " + values.join(" <-> ") + " <-> null";
}
}
// Sử dụng
const dll = new DoublyLinkedList<number>();
dll.append(10);
dll.append(20);
dll.append(30);
dll.prepend(5);
console.log(dll.printForward()); // null <-> 5 <-> 10 <-> 20 <-> 30 <-> null
console.log(dll.printBackward()); // null <-> 30 <-> 20 <-> 10 <-> 5 <-> null
dll.deleteLast();
console.log(dll.printForward()); // null <-> 5 <-> 10 <-> 20 <-> null# Python - Doubly Linked List
class DoublyLinkedList:
def __init__(self):
self.head = None
self.tail = None
self.size = 0
def prepend(self, val):
new_node = DoublyNode(val)
if not self.head:
self.head = new_node
self.tail = new_node
else:
new_node.next = self.head
self.head.prev = new_node
self.head = new_node
self.size += 1
def append(self, val):
new_node = DoublyNode(val)
if not self.tail:
self.head = new_node
self.tail = new_node
else:
new_node.prev = self.tail
self.tail.next = new_node
self.tail = new_node
self.size += 1
def delete_node(self, node):
if node.prev:
node.prev.next = node.next
else:
self.head = node.next
if node.next:
node.next.prev = node.prev
else:
self.tail = node.prev
self.size -= 1
def delete_first(self):
if not self.head:
return None
val = self.head.val
self.delete_node(self.head)
return val
def delete_last(self):
if not self.tail:
return None
val = self.tail.val
self.delete_node(self.tail)
return val
def print_forward(self):
values = []
current = self.head
while current:
values.append(str(current.val))
current = current.next
return "None <-> " + " <-> ".join(values) + " <-> None"
# Sử dụng
dll = DoublyLinkedList()
dll.append(10)
dll.append(20)
dll.append(30)
dll.prepend(5)
print(dll.print_forward()) # None <-> 5 <-> 10 <-> 20 <-> 30 <-> None
dll.delete_last()
print(dll.print_forward()) # None <-> 5 <-> 10 <-> 20 <-> NoneSingly vs Doubly Linked List
| Tính năng | Singly | Doubly |
|---|---|---|
| Bộ nhớ mỗi node | Data + 1 con trỏ | Data + 2 con trỏ |
| Duyệt tiến | Có | Có |
| Duyệt lùi | Không | Có |
| Xóa với tham chiếu node | O(n)* | O(1) |
| Chèn trước node | O(n)* | O(1) |
| Append (có tail) | O(1) | O(1) |
| Xóa cuối (có tail) | O(n)† | O(1) |
| Độ phức tạp | Đơn giản hơn | Phức tạp hơn |
*Cần tìm node trước | †Cần duyệt để tìm node kế cuối
4. Circular Linked List
Trong circular linked list, node cuối trỏ ngược lại node đầu thay vì null, tạo thành vòng.
Singly Circular:
Doubly Circular:
Khi nào dùng Circular Lists
- Lập lịch round-robin — luân phiên giữa các tasks/players
- Playlist nhạc — lặp lại từ bài đầu tiên
- Quản lý buffer — circular buffers
- Bài toán Josephus — bài toán phỏng vấn kinh điển
// TypeScript - Kiểm tra Circular Linked List
function isCircular<T>(head: ListNode<T> | null): boolean {
if (!head) return false;
let current = head.next;
while (current && current !== head) {
current = current.next;
}
return current === head;
}# Python - Kiểm tra Circular Linked List
def is_circular(head):
if not head:
return False
current = head.next
while current and current != head:
current = current.next
return current == head5. Kỹ thuật Dummy Head
Nhiều bài toán linked list cần xử lý đặc biệt cho node head. Kỹ thuật dummy head (sentinel node) loại bỏ các edge cases bằng cách thêm node giả trước head thật.
// Không dùng dummy head - phải xử lý head riêng
function deleteValue<T>(head: ListNode<T> | null, val: T): ListNode<T> | null {
// Trường hợp đặc biệt: head chứa giá trị
while (head && head.val === val) {
head = head.next;
}
let current = head;
while (current && current.next) {
if (current.next.val === val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return head;
}
// Với dummy head - logic sạch, thống nhất
function deleteValueClean<T>(head: ListNode<T> | null, val: T): ListNode<T> | null {
const dummy = new ListNode(0 as T, head); // Node giả trước head
let current = dummy;
while (current.next) {
if (current.next.val === val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return dummy.next; // Head thật
}# Python - Kỹ thuật Dummy head
def delete_value_clean(head, val):
dummy = ListNode(0, head)
current = dummy
while current.next:
if current.next.val == val:
current.next = current.next.next
else:
current = current.next
return dummy.nextMẹo phỏng vấn: Dùng dummy head bất cứ khi nào head có thể thay đổi (xóa, chèn ở đầu, merge). Nó đơn giản hóa code và giảm bugs.
6. Đảo ngược Linked List
Đảo ngược linked list là một trong những câu hỏi phỏng vấn được hỏi nhiều nhất. Có hai cách tiếp cận: lặp và đệ quy.
Cách Lặp (Khuyên dùng)
Dùng ba con trỏ: prev, current, và next.
Bước 1 — Đảo con trỏ đầu tiên:
Bước 2 — Tiếp tục đảo:
Xong — Tất cả con trỏ đã đảo:
// TypeScript - Đảo ngược Linked List (Lặp)
function reverseList(head: ListNode<number> | null): ListNode<number> | null {
let prev: ListNode<number> | null = null;
let current = head;
while (current) {
const next = current.next; // Lưu next
current.next = prev; // Đảo con trỏ
prev = current; // Tiến prev
current = next; // Tiến current
}
return prev; // Head mới
}
// Time: O(n) | Space: O(1)# Python - Đảo ngược Linked List (Lặp)
def reverse_list(head):
prev = None
current = head
while current:
next_node = current.next # Lưu next
current.next = prev # Đảo con trỏ
prev = current # Tiến prev
current = next_node # Tiến current
return prev # Head mới
# Time: O(n) | Space: O(1)Cách Đệ quy
// TypeScript - Đảo ngược Linked List (Đệ quy)
function reverseListRecursive(head: ListNode<number> | null): ListNode<number> | null {
// Trường hợp cơ sở: list rỗng hoặc một node
if (!head || !head.next) {
return head;
}
// Đệ quy đảo ngược phần còn lại
const newHead = reverseListRecursive(head.next);
// Đảo con trỏ
head.next.next = head; // Node sau head trỏ ngược lại head
head.next = null; // Head bây giờ trỏ đến null
return newHead;
}
// Time: O(n) | Space: O(n) do call stack# Python - Đảo ngược Linked List (Đệ quy)
def reverse_list_recursive(head):
# Trường hợp cơ sở
if not head or not head.next:
return head
# Đệ quy đảo ngược phần còn lại
new_head = reverse_list_recursive(head.next)
# Đảo con trỏ
head.next.next = head
head.next = None
return new_head
# Time: O(n) | Space: O(n) do call stackMẹo phỏng vấn: Người phỏng vấn thường hỏi cả hai cách. Bắt đầu với lặp (O(1) space), sau đó trình bày đệ quy để thể hiện kỹ năng recursion.
7. Fast & Slow Pointers (Thuật toán Floyd)
Kỹ thuật fast and slow pointer (còn gọi là Floyd's Tortoise and Hare) dùng hai con trỏ di chuyển với tốc độ khác nhau. Mẫu này giải quyết nhiều bài toán linked list một cách tinh tế.
Phát hiện Cycle trong Linked List
Ý tưởng: Nếu có cycle, con trỏ fast (2 bước) cuối cùng sẽ bắt kịp con trỏ slow (1 bước).
// TypeScript - Phát hiện Cycle (LeetCode 141)
function hasCycle(head: ListNode<number> | null): boolean {
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow!.next; // 1 bước
fast = fast.next.next; // 2 bước
if (slow === fast) {
return true; // Phát hiện cycle
}
}
return false; // Không có cycle (fast đến cuối)
}
// Time: O(n) | Space: O(1)# Python - Phát hiện Cycle (LeetCode 141)
def has_cycle(head):
slow = head
fast = head
while fast and fast.next:
slow = slow.next # 1 bước
fast = fast.next.next # 2 bước
if slow == fast:
return True # Phát hiện cycle
return False # Không có cycle
# Time: O(n) | Space: O(1)Tìm Điểm Bắt đầu Cycle (LeetCode 142)
Sau khi phát hiện cycle, reset một con trỏ về head và di chuyển cả hai cùng tốc độ — chúng sẽ gặp nhau tại điểm bắt đầu cycle.
Tại sao điều này hoạt động? Nếu khoảng cách từ head đến điểm bắt đầu cycle là a, và điểm gặp nhau cách b bước vào cycle có độ dài c, thì:
- Slow đi:
a + b - Fast đi:
a + b + c(đi quanh cycle một vòng) - Vì fast = 2 × slow:
a + b + c = 2(a + b)→c = a + b→a = c - b
Vậy bắt đầu từ head và từ điểm gặp nhau, cả hai con trỏ cần a bước để đến điểm bắt đầu cycle.
// TypeScript - Tìm Điểm Bắt đầu Cycle (LeetCode 142)
function detectCycle(head: ListNode<number> | null): ListNode<number> | null {
let slow = head;
let fast = head;
// Bước 1: Phát hiện cycle
while (fast && fast.next) {
slow = slow!.next;
fast = fast.next.next;
if (slow === fast) {
// Bước 2: Tìm điểm bắt đầu cycle
let pointer = head;
while (pointer !== slow) {
pointer = pointer!.next;
slow = slow!.next;
}
return pointer; // Điểm bắt đầu cycle
}
}
return null; // Không có cycle
}# Python - Tìm Điểm Bắt đầu Cycle (LeetCode 142)
def detect_cycle(head):
slow = head
fast = head
# Bước 1: Phát hiện cycle
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
# Bước 2: Tìm điểm bắt đầu cycle
pointer = head
while pointer != slow:
pointer = pointer.next
slow = slow.next
return pointer # Điểm bắt đầu cycle
return None # Không có cycleTìm Node Giữa Linked List (LeetCode 876)
Khi slow đến giữa, fast đã đến cuối.
// TypeScript - Tìm Node Giữa
function middleNode(head: ListNode<number> | null): ListNode<number> | null {
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow!.next; // 1 bước
fast = fast.next.next; // 2 bước
}
return slow; // Node giữa
}
// Với [1, 2, 3, 4, 5] → trả về node 3
// Với [1, 2, 3, 4, 5, 6] → trả về node 4 (node giữa thứ hai)# Python - Tìm Node Giữa
def middle_node(head):
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
# Time: O(n) | Space: O(1)8. Bài toán Phỏng vấn Kinh điển
Bài 1: Merge Two Sorted Lists (LeetCode 21)
Cho hai linked lists đã sắp xếp, merge chúng thành một list đã sắp xếp.
// TypeScript - Merge Two Sorted Lists
function mergeTwoLists(
l1: ListNode<number> | null,
l2: ListNode<number> | null
): ListNode<number> | null {
const dummy = new ListNode(0);
let current = dummy;
while (l1 && l2) {
if (l1.val <= l2.val) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
// Gắn các nodes còn lại
current.next = l1 || l2;
return dummy.next;
}
// Time: O(n + m) | Space: O(1) — tái sử dụng nodes hiện có# Python - Merge Two Sorted Lists
def merge_two_lists(l1, l2):
dummy = ListNode(0)
current = dummy
while l1 and l2:
if l1.val <= l2.val:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
# Gắn các nodes còn lại
current.next = l1 if l1 else l2
return dummy.next
# Time: O(n + m) | Space: O(1)Bài 2: Remove Nth Node From End (LeetCode 19)
Xóa node thứ n từ cuối list trong một lần duyệt.
Mẹo: Dùng hai con trỏ cách nhau n nodes. Khi con trỏ fast đến cuối, con trỏ slow ở ngay trước target.
// TypeScript - Xóa Node thứ N từ Cuối
function removeNthFromEnd(
head: ListNode<number> | null,
n: number
): ListNode<number> | null {
const dummy = new ListNode(0, head);
let fast: ListNode<number> | null = dummy;
let slow: ListNode<number> | null = dummy;
// Di chuyển con trỏ fast trước n+1 bước
for (let i = 0; i <= n; i++) {
fast = fast!.next;
}
// Di chuyển cả hai cho đến khi fast đến cuối
while (fast) {
fast = fast.next;
slow = slow!.next;
}
// Bỏ qua node target
slow!.next = slow!.next!.next;
return dummy.next;
}
// Time: O(n) | Space: O(1)# Python - Xóa Node thứ N từ Cuối
def remove_nth_from_end(head, n):
dummy = ListNode(0, head)
fast = dummy
slow = dummy
# Di chuyển con trỏ fast trước n+1 bước
for _ in range(n + 1):
fast = fast.next
# Di chuyển cả hai cho đến khi fast đến cuối
while fast:
fast = fast.next
slow = slow.next
# Bỏ qua node target
slow.next = slow.next.next
return dummy.next
# Time: O(n) | Space: O(1)Bài 3: Palindrome Linked List (LeetCode 234)
Kiểm tra linked list có phải palindrome không với O(1) space.
Chiến lược:
- Tìm giữa (fast & slow pointers)
- Đảo ngược nửa sau
- So sánh hai nửa
// TypeScript - Palindrome Linked List
function isPalindrome(head: ListNode<number> | null): boolean {
if (!head || !head.next) return true;
// Bước 1: Tìm giữa
let slow: ListNode<number> | null = head;
let fast: ListNode<number> | null = head;
while (fast && fast.next) {
slow = slow!.next;
fast = fast.next.next;
}
// Bước 2: Đảo ngược nửa sau
let prev: ListNode<number> | null = null;
let current = slow;
while (current) {
const next = current.next;
current.next = prev;
prev = current;
current = next;
}
// Bước 3: So sánh hai nửa
let left: ListNode<number> | null = head;
let right: ListNode<number> | null = prev;
while (right) {
if (left!.val !== right.val) return false;
left = left!.next;
right = right.next;
}
return true;
}
// Time: O(n) | Space: O(1)# Python - Palindrome Linked List
def is_palindrome(head):
if not head or not head.next:
return True
# Bước 1: Tìm giữa
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Bước 2: Đảo ngược nửa sau
prev = None
current = slow
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
# Bước 3: So sánh hai nửa
left = head
right = prev
while right:
if left.val != right.val:
return False
left = left.next
right = right.next
return True
# Time: O(n) | Space: O(1)Bài 4: Intersection of Two Linked Lists (LeetCode 160)
Tìm node nơi hai singly linked lists giao nhau.
Mẹo: Nếu list A có độ dài a và list B có độ dài b, duyệt cả hai. Khi một con trỏ đến cuối, chuyển hướng đến head của list kia. Chúng sẽ gặp nhau tại điểm giao sau a + b bước.
// TypeScript - Giao điểm hai Linked Lists
function getIntersectionNode(
headA: ListNode<number> | null,
headB: ListNode<number> | null
): ListNode<number> | null {
let pA = headA;
let pB = headB;
while (pA !== pB) {
pA = pA ? pA.next : headB;
pB = pB ? pB.next : headA;
}
return pA; // Node giao hoặc null
}
// Time: O(n + m) | Space: O(1)# Python - Giao điểm hai Linked Lists
def get_intersection_node(head_a, head_b):
p_a = head_a
p_b = head_b
while p_a != p_b:
p_a = p_a.next if p_a else head_b
p_b = p_b.next if p_b else head_a
return p_a # Node giao hoặc None
# Time: O(n + m) | Space: O(1)Bài 5: Reorder List (LeetCode 143)
Sắp xếp lại list từ L0 → L1 → ... → Ln thành L0 → Ln → L1 → Ln-1 → ...
Bài này kết hợp ba kỹ thuật: tìm giữa, đảo ngược, và merge.
// TypeScript - Reorder List
function reorderList(head: ListNode<number> | null): void {
if (!head || !head.next) return;
// Bước 1: Tìm giữa
let slow: ListNode<number> | null = head;
let fast: ListNode<number> | null = head;
while (fast && fast.next) {
slow = slow!.next;
fast = fast.next.next;
}
// Bước 2: Đảo ngược nửa sau
let prev: ListNode<number> | null = null;
let current = slow!.next;
slow!.next = null; // Tách list
while (current) {
const next = current.next;
current.next = prev;
prev = current;
current = next;
}
// Bước 3: Merge hai nửa xen kẽ
let first: ListNode<number> | null = head;
let second: ListNode<number> | null = prev;
while (second) {
const tmp1 = first!.next;
const tmp2 = second.next;
first!.next = second;
second.next = tmp1;
first = tmp1;
second = tmp2;
}
}
// Time: O(n) | Space: O(1)# Python - Reorder List
def reorder_list(head):
if not head or not head.next:
return
# Bước 1: Tìm giữa
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Bước 2: Đảo ngược nửa sau
prev = None
current = slow.next
slow.next = None # Tách list
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
# Bước 3: Merge hai nửa xen kẽ
first = head
second = prev
while second:
tmp1 = first.next
tmp2 = second.next
first.next = second
second.next = tmp1
first = tmp1
second = tmp2
# Time: O(n) | Space: O(1)9. Edge Cases Phổ biến
Edge cases là nơi phần lớn bugs linked list ẩn náu. Luôn test solution với các trường hợp này:
| Edge Case | Ví dụ | Tại sao Quan trọng |
|---|---|---|
| List rỗng | head = null | Null pointer exception |
| Một node | [1] -> null | Không có next node để xử lý |
| Hai nodes | [1] -> [2] -> null | Minimum cho hầu hết thao tác |
| Giá trị trùng | [1] -> [1] -> [2] | Đảm bảo xử lý tất cả occurrences |
| Target ở head | Xóa head node | Phải cập nhật head pointer |
| Target ở tail | Xóa node cuối | Phải tìm node trước |
| Tất cả cùng giá trị | [5] -> [5] -> [5] | Edge case xóa hàng loạt |
// TypeScript - Luôn kiểm tra các edge cases này
function safeOperation(head: ListNode<number> | null): ListNode<number> | null {
// Edge case 1: List rỗng
if (!head) return null;
// Edge case 2: Một node
if (!head.next) return head;
// Edge case 3: Hai nodes (edge case phổ biến cho đảo ngược, etc.)
// ... logic của bạn ở đây
return head;
}10. Linked Lists trong Ứng dụng Thực tế
Linked lists không chỉ là bài tập phỏng vấn — chúng vận hành các hệ thống quan trọng:
Lịch sử Trình duyệt (Doubly Linked List)
// Lịch sử trình duyệt đơn giản hóa dùng doubly linked list
class BrowserHistory {
private current: DoublyNode<string>;
constructor(homepage: string) {
this.current = new DoublyNode(homepage);
}
visit(url: string): void {
const newPage = new DoublyNode(url);
this.current.next = newPage;
newPage.prev = this.current;
this.current = newPage;
}
back(steps: number): string {
while (steps > 0 && this.current.prev) {
this.current = this.current.prev;
steps--;
}
return this.current.val;
}
forward(steps: number): string {
while (steps > 0 && this.current.next) {
this.current = this.current.next;
steps--;
}
return this.current.val;
}
}LRU Cache (Hash Map + Doubly Linked List)
LRU Cache (Least Recently Used) là bài toán system design kinh điển kết hợp doubly linked list với hash map cho O(1) operations.
# Python - LRU Cache (LeetCode 146)
class LRUNode:
def __init__(self, key=0, val=0):
self.key = key
self.val = val
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = {} # key -> node
# Dummy head và tail
self.head = LRUNode()
self.tail = LRUNode()
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _add_to_front(self, node):
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key):
if key in self.cache:
node = self.cache[key]
self._remove(node)
self._add_to_front(node)
return node.val
return -1
def put(self, key, value):
if key in self.cache:
self._remove(self.cache[key])
node = LRUNode(key, value)
self._add_to_front(node)
self.cache[key] = node
if len(self.cache) > self.capacity:
# Xóa least recently used (từ tail)
lru = self.tail.prev
self._remove(lru)
del self.cache[lru.key]
# Sử dụng
cache = LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
print(cache.get(1)) # 1
cache.put(3, 3) # Loại bỏ key 2
print(cache.get(2)) # -1 (không tìm thấy)Các Ứng dụng Thực tế Khác
- Playlist nhạc — circular linked list cho chế độ lặp
- Hệ thống Undo/Redo — doubly linked list lưu các trạng thái
- Cấp phát bộ nhớ — free lists trong hệ điều hành
- Blockchain — mỗi block trỏ đến block trước
- Hash table chaining — xử lý va chạm bằng linked lists
- Bộ lập lịch hệ điều hành — circular linked list
Tóm tắt và Điểm Chính
Khái niệm Cốt lõi
- Singly linked list: nodes với data + next pointer — đơn giản, chỉ duyệt tiến
- Doubly linked list: nodes với prev + next — hai chiều, O(1) xóa với tham chiếu node
- Circular linked list: node cuối kết nối lại node đầu — hữu ích cho các quy trình tuần hoàn
Kỹ thuật Thiết yếu
| Kỹ thuật | Khi nào Dùng | Ý tưởng Chính |
|---|---|---|
| Dummy head | Head có thể thay đổi | Thêm node giả trước head |
| Fast & slow pointers | Phát hiện cycle, tìm giữa | Hai tốc độ khác nhau |
| Đảo ngược linked list | Nhiều bài cần nó | Dùng prev, current, next pointers |
| Khoảng cách hai con trỏ | Node thứ N từ cuối | Giữ khoảng cách cố định giữa hai con trỏ |
| Merge hai lists | Kết hợp dữ liệu đã sắp xếp | Dùng dummy head, so sánh giá trị |
Tóm tắt Độ phức tạp
| Thao tác | Singly | Doubly |
|---|---|---|
| Truy cập (theo index) | O(n) | O(n) |
| Prepend | O(1) | O(1) |
| Append (có tail) | O(1) | O(1) |
| Chèn tại vị trí | O(n) | O(n)* |
| Xóa head | O(1) | O(1) |
| Xóa tail | O(n) | O(1) |
| Tìm kiếm | O(n) | O(n) |
*O(1) nếu đã có tham chiếu node trước
Nhận diện Mẫu
| Nếu bạn thấy... | Cân nhắc... |
|---|---|
| Phát hiện cycle | Fast & slow pointers |
| Tìm giữa | Fast & slow pointers |
| Merge sorted lists | Dummy head + so sánh |
| Đảo ngược list | Ba con trỏ lặp |
| Node thứ N từ cuối | Khoảng cách hai con trỏ |
| Kiểm tra palindrome | Tìm giữa + đảo ngược + so sánh |
| Sắp xếp lại/tái cấu trúc | Tìm giữa + đảo ngược + merge |
| Head có thể thay đổi | Dummy head |
Bài tập Thực hành
Dễ
- LeetCode 206: Reverse Linked List
- LeetCode 21: Merge Two Sorted Lists
- LeetCode 141: Linked List Cycle
- LeetCode 876: Middle of the Linked List
- LeetCode 83: Remove Duplicates from Sorted List
Trung bình
- LeetCode 19: Remove Nth Node From End
- LeetCode 142: Linked List Cycle II
- LeetCode 143: Reorder List
- LeetCode 234: Palindrome Linked List
- LeetCode 148: Sort List
Khó
Tiếp theo
Bây giờ bạn đã nắm vững linked lists, bạn sẵn sàng học về Stacks và Queues trong bài viết tiếp theo. Bạn sẽ khám phá:
- Nguyên lý LIFO (Stack) và FIFO (Queue)
- Triển khai stacks và queues từ đầu
- Bài toán kinh điển: valid parentheses, next greater element
- Ứng dụng thực tế: function call stack, task scheduling
Tiếp tục hành trình DSA: Stacks & Queues Complete Guide
Tài nguyên Bổ sung
Các Bài Viết Trước trong Series
- DSA Roadmap - Lộ trình học tập hoàn chỉnh
- Big O Notation & Phân tích Độ phức tạp - Hiểu độ phức tạp thời gian và không gian
- Mẫu Giải quyết Vấn đề - Các mẫu thiết yếu cho phỏng vấn coding
- Arrays & Strings Complete Guide - Cấu trúc dữ liệu nền tảng
📬 Đă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.