Back to blog

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

algorithmsdata-structureslinked-listsinterview-prepdsa
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


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:

  1. Data — giá trị được lưu
  2. 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ácArrayLinked List
Truy cập theo indexO(1)O(n)
Chèn vào đầuO(n)O(1)
Chèn vào cuốiO(1)*O(n)† / O(1)‡
Chèn vào giữaO(n)O(1)§
Xóa ở đầuO(n)O(1)
Xóa ở giữaO(n)O(1)§
Tìm kiếmO(n)O(n)
Bộ nhớLiên tục, nhỏ gọnRả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 = next

Triể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 -> None

Minh 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 = None

Triể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 <-> None

Singly vs Doubly Linked List

Tính năngSinglyDoubly
Bộ nhớ mỗi nodeData + 1 con trỏData + 2 con trỏ
Duyệt tiếnCóCó
Duyệt lùiKhôngCó
Xóa với tham chiếu nodeO(n)*O(1)
Chèn trước nodeO(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ơnPhứ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 == head

5. 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.next

Mẹ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 stack

Mẹ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ó cycle

Tì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:

  1. Tìm giữa (fast & slow pointers)
  2. Đảo ngược nửa sau
  3. 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 CaseVí dụTại sao Quan trọng
List rỗnghead = nullNull pointer exception
Một node[1] -> nullKhông có next node để xử lý
Hai nodes[1] -> [2] -> nullMinimum 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 ở headXóa head nodePhải cập nhật head pointer
Target ở tailXóa node cuốiPhả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ậtKhi nào DùngÝ tưởng Chính
Dummy headHead có thể thay đổiThêm node giả trước head
Fast & slow pointersPhát hiện cycle, tìm giữaHai tốc độ khác nhau
Đảo ngược linked listNhiều bài cần nóDùng prev, current, next pointers
Khoảng cách hai con trỏNode thứ N từ cuốiGiữ khoảng cách cố định giữa hai con trỏ
Merge hai listsKết hợp dữ liệu đã sắp xếpDùng dummy head, so sánh giá trị

Tóm tắt Độ phức tạp

Thao tácSinglyDoubly
Truy cập (theo index)O(n)O(n)
PrependO(1)O(1)
Append (có tail)O(1)O(1)
Chèn tại vị tríO(n)O(n)*
Xóa headO(1)O(1)
Xóa tailO(n)O(1)
Tìm kiếmO(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 cycleFast & slow pointers
Tìm giữaFast & slow pointers
Merge sorted listsDummy head + so sánh
Đảo ngược listBa con trỏ lặp
Node thứ N từ cuốiKhoảng cách hai con trỏ
Kiểm tra palindromeTìm giữa + đảo ngược + so sánh
Sắp xếp lại/tái cấu trúcTìm giữa + đảo ngược + merge
Head có thể thay đổiDummy head

Bài tập Thực hành

Dễ

  1. LeetCode 206: Reverse Linked List
  2. LeetCode 21: Merge Two Sorted Lists
  3. LeetCode 141: Linked List Cycle
  4. LeetCode 876: Middle of the Linked List
  5. LeetCode 83: Remove Duplicates from Sorted List

Trung bình

  1. LeetCode 19: Remove Nth Node From End
  2. LeetCode 142: Linked List Cycle II
  3. LeetCode 143: Reorder List
  4. LeetCode 234: Palindrome Linked List
  5. LeetCode 148: Sort List

Khó

  1. LeetCode 25: Reverse Nodes in k-Group
  2. LeetCode 23: Merge k Sorted Lists

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

📬 Đă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.