Menu
CoddyTech

Danh sách liên kết đôi

Cập nhật lần cuối

Danh sách liên kết đôi (doubly linked list) là danh sách liên kết trong đó mỗi nút giữ hai con trỏ: next (trỏ đến nút phía sau) và prev (trỏ đến nút phía trước). Liên kết ngược bổ sung này cho phép bạn duyệt theo cả hai chiều và xóa một nút trong O(1) khi đã có tham chiếu đến nó, vì bạn đến thẳng được nút liền trước thay vì phải đi từ đầu danh sách. Nhấn Phát ở trên để xem các nút được nối và nối lại theo cả hai chiều.

Cái giá là thêm một con trỏ cho mỗi nút và nhiều việc cập nhật hơn: mỗi lần chèn và xóa đều phải cập nhật cả next lẫn prev ở các nút lân cận. Đây là cấu trúc đứng sau nhiều deque và LRU cache, nơi việc gỡ bỏ nhanh ở cả hai đầu là quan trọng.

Độ phức tạp thời gian

Thao tácĐộ phức tạpGhi chú
Chèn vào đầu/cuốiO(1)Khi có con trỏ head và tail
Xóa một nút đã biếtO(1)Đến thẳng prev, không cần duyệt
Tìm kiếmO(n)Vẫn là duyệt tuần tự
Truy cập theo chỉ sốO(n)Không có truy cập ngẫu nhiên

Danh sách liên kết đơn và đôi

Khía cạnhLiên kết đơnLiên kết đôi
Số con trỏ mỗi nút1 (next)2 (next + prev)
Duyệt ngượcKhôngCó
Xóa một nút đã biếtO(n) (tìm prev)O(1)
Bộ nhớ phụThấp hơnCao hơn

Ví dụ minh họa

Tạo [10, 20, 30] bằng cách thêm vào cuối, rồi xóa nút 20:

BướcCấu trúcThao tác
Bắt đầunullDanh sách rỗng: head và tail đều là null
Thêm 1010Nút đầu tiên; head và tail đều trỏ đến nó, prev = next = null
Thêm 2010 <-> 20Đặt 10.next = 20 và 20.prev = 10; tail = 20
Thêm 3010 <-> 20 <-> 30Đặt 20.next = 30 và 30.prev = 20; tail = 30
Xóa 2010 <-> 30Dùng 20.prev và 20.next: đặt 10.next = 30 và 30.prev = 10 trong O(1)

Khi nào nên dùng danh sách liên kết đôi

Nên dùng khiTránh dùng khi
Bạn cần duyệt hoặc ghép nối theo cả hai chiềuBạn chỉ luôn đi về phía trước: danh sách liên kết đơn nhẹ hơn
Bạn xóa các nút bất kỳ mà bạn đã có tham chiếu (O(1))Bạn chủ yếu truy cập theo vị trí: hãy dùng mảng để truy cập O(1)
Bạn cài đặt deque, LRU cache hoặc lịch sử hoàn tác trên màn hìnhBộ nhớ eo hẹp: con trỏ prev bổ sung làm chi phí liên kết tăng gấp đôi
Việc chèn và gỡ bỏ diễn ra thường xuyên ở cả hai đầuDữ liệu nhỏ và hiếm khi thay đổi: sao chép mảng rẻ hơn

Code Doubly Linked List

Một cách cài đặt Doubly Linked List gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.

Code Doubly Linked List bằng Python

Python
1class Node:2    def __init__(self, value):3        self.value = value4        self.prev = None5        self.next = None6
7
8class DoublyLinkedList:9    def __init__(self):10        self.head = None11        self.tail = None12
13    def append(self, value):14        node = Node(value)15        if self.tail is None:16            self.head = self.tail = node17            return18        # Wire both directions: old tail <-> new node19        node.prev = self.tail20        self.tail.next = node21        self.tail = node22
23    def prepend(self, value):24        node = Node(value)25        if self.head is None:26            self.head = self.tail = node27            return28        node.next = self.head29        self.head.prev = node30        self.head = node31
32    def forward(self):33        values, current = [], self.head34        while current:35            values.append(current.value)36            current = current.next37        return values38
39    def backward(self):40        values, current = [], self.tail41        while current:42            values.append(current.value)43            current = current.prev44        return values45
46
47lst = DoublyLinkedList()48for value in [2, 3, 4]:49    lst.append(value)50lst.prepend(1)51
52print("Forward: ", lst.forward())53print("Backward:", lst.backward())
Chạy code này trong Playground Python

Câu hỏi thường gặp về danh sách liên kết đôi

Danh sách liên kết đơn và danh sách liên kết đôi khác nhau như thế nào?
Danh sách liên kết đơn có một con trỏ mỗi nút (next), nên bạn chỉ có thể đi về phía trước. Danh sách liên kết đôi thêm con trỏ prev, cho phép duyệt ngược và xóa một nút bạn đang giữ tham chiếu trong O(1), với cái giá là thêm một con trỏ mỗi nút và phải cập nhật hai liên kết mỗi khi thay đổi.
Khi nào danh sách liên kết đôi đáng với phần bộ nhớ bổ sung?
Khi bạn cần duyệt ngược hoặc xóa trong O(1) các nút bất kỳ mà bạn đã tham chiếu tới. Các trường hợp kinh điển là deque (hàng đợi hai đầu) và LRU cache, nơi các mục thường xuyên được di chuyển và loại bỏ ở cả hai đầu.
Vì sao danh sách liên kết đôi có thể xóa một nút trong O(1)?
Để gỡ một nút, bạn phải nối nút liền trước với nút liền sau của nó. Trong danh sách liên kết đơn, bạn phải đi từ đầu danh sách để tìm nút liền trước (O(n)); trong danh sách liên kết đôi, con trỏ prev của nút cho bạn nút liền trước ngay lập tức, nên việc nối lại là O(1).
Danh sách liên kết đôi hay mảng: nên dùng cái nào?
Dùng mảng khi bạn cần truy cập theo chỉ số O(1) và duyệt thân thiện với bộ nhớ đệm, và khi việc chèn chủ yếu diễn ra ở cuối. Dùng danh sách liên kết đôi khi bạn thường xuyên chèn hoặc gỡ ở giữa hoặc ở cả hai đầu khi đã có tham chiếu đến nút, vì thao tác đó là O(1) so với O(n) dịch chuyển của mảng. Mảng thắng về bộ nhớ và tính cục bộ; danh sách thắng ở các thao tác thay đổi cấu trúc.
Nút lính canh trong danh sách liên kết đôi là gì?
Nút lính canh (sentinel, hay nút giả) là một nút giữ chỗ nằm trước head và/hoặc sau tail để danh sách không bao giờ thực sự rỗng. Nó loại bỏ các bước kiểm tra null cho head, tail và các thao tác chèn/xóa ở biên, giúp mọi thao tác đi theo cùng một đoạn code nối lại. Nhiều bản cài đặt LRU cache trong môi trường production dùng hai nút lính canh để không phải xử lý riêng hai đầu.
Bug phổ biến nhất khi xóa khỏi danh sách liên kết đôi là gì?
Quên cập nhật head hoặc tail khi xóa nút đầu hoặc nút cuối, để lại một con trỏ treo trỏ đến nút đã bị giải phóng. Luôn kiểm tra xem node.prev có là null không (cập nhật head) hoặc node.next có là null không (cập nhật tail) trước khi nối lại các nút lân cận.
Hình minh họa các ngôn ngữ lập trình của Coddy

Thành thạo thuật toán cùng Coddy

Bắt đầu