Bài 34.4: Bài tập thực hành danh sách liên kết đôi trong C++

Chào mừng các bạn trở lại với chuỗi bài viết về C++ trên blog của FullhouseDev! Sau khi đã tìm hiểu về khái niệm cơ bản và ưu nhược điểm của danh sách liên kết đôi, hôm nay chúng ta sẽ xắn tay áo lên và đi vào phần thực hành đầy thú vị: triển khai cấu trúc dữ liệu này trong C++. Đây là cơ hội tuyệt vời để củng cố kiến thức lý thuyết bằng cách tự mình viết code và hiểu rõ cách các con trỏ hoạt động!

Danh sách liên kết đôi, hay Doubly Linked List, là một biến thể nâng cấp của danh sách liên kết đơn. Điểm khác biệt cốt lõi nằm ở việc mỗi nút (node) không chỉ trỏ đến nút tiếp theo (next) mà còn trỏ ngược lại đến nút trước đó (prev). Sự "hai chiều" này mang lại nhiều lợi ích, đặc biệt là khả năng duyệt danh sách theo cả hai hướng và thao tác xóa nút hiệu quả hơn đáng kể.

Trong bài thực hành này, chúng ta sẽ cùng nhau xây dựng một lớp (class) C++ đơn giản để quản lý danh sách liên kết đôi và triển khai một số thao tác cơ bản nhưng quan trọng nhất.

1. Cấu trúc của một Node trong Danh sách Liên kết Đôi

Trước hết, chúng ta cần định nghĩa cấu trúc của một nút (node). Mỗi nút sẽ chứa dữ liệu (data) và hai con trỏ: next (trỏ tới nút kế tiếp) và prev (trỏ tới nút đứng trước).

#include <iostream> // Cần cho việc nhập xuất

// Định nghĩa cấu trúc của một Node
struct Node {
    int data;   // Dữ liệu lưu trữ trong node
    Node* next; // Con trỏ tới node tiếp theo
    Node* prev; // Con trỏ tới node trước đó

    // Constructor tiện lợi để khởi tạo node mới
    Node(int val) : data(val), next(nullptr), prev(nullptr) {}
};

Giải thích:

  • struct Node: Chúng ta sử dụng struct để tạo một kiểu dữ liệu tùy chỉnh cho các nút.
  • int data;: Thành phần này lưu trữ giá trị thực tế của nút. Bạn có thể thay int bằng bất kỳ kiểu dữ liệu nào khác tùy theo nhu cầu.
  • Node* next;: Đây là con trỏ kiểu Node*, trỏ đến nút tiếp theo trong danh sách. Nếu nút này là nút cuối cùng, next sẽ là nullptr.
  • Node* prev;: Đây cũng là con trỏ kiểu Node*, trỏ đến nút trước đó trong danh sách. Nếu nút này là nút đầu tiên, prev sẽ là nullptr.
  • Node(int val) : data(val), next(nullptr), prev(nullptr) {}: Đây là một constructor. Khi bạn tạo một Node mới với cú pháp Node newNode(someValue);, constructor này sẽ được gọi để khởi tạo data bằng val, và quan trọng là thiết lập nextprev ban đầu là nullptr.

2. Xây dựng Lớp Quản lý Danh sách Liên kết Đôi

Để quản lý toàn bộ danh sách (như biết nút đầu tiên ở đâu, nút cuối cùng ở đâu), chúng ta sẽ tạo một lớp DoublyLinkedList. Lớp này sẽ chứa các con trỏ trỏ đến nút đầu (head) và nút cuối (tail) của danh sách.

#include <iostream>

// Định nghĩa cấu trúc của một Node (như trên)
struct Node {
    int data;
    Node* next;
    Node* prev;
    Node(int val) : data(val), next(nullptr), prev(nullptr) {}
};

// Định nghĩa lớp DoublyLinkedList để quản lý danh sách
class DoublyLinkedList {
private:
    Node* head; // Con trỏ tới nút đầu tiên
    Node* tail; // Con trỏ tới nút cuối cùng

public:
    // Constructor: Khởi tạo danh sách rỗng
    DoublyLinkedList() : head(nullptr), tail(nullptr) {}

    // Destructor: Giải phóng bộ nhớ khi đối tượng DoublyLinkedList bị hủy
    ~DoublyLinkedList() {
        Node* current = head;
        while (current != nullptr) {
            Node* next = current->next;
            delete current; // Giải phóng bộ nhớ của node hiện tại
            current = next;  // Di chuyển tới node tiếp theo
        }
        head = nullptr; // Đảm bảo head và tail là nullptr sau khi giải phóng
        tail = nullptr;
        cout << "\nDa giai phong bo nho danh sach." << endl;
    }

    // --- Các phương thức thao tác sẽ được thêm vào đây ---

    // Phương thức hiển thị danh sách (duyệt xuôi)
    void displayForward() const {
        Node* current = head;
        cout << "Danh sach (xuoi): ";
        while (current != nullptr) {
            cout << current->data;
            if (current->next != nullptr) {
                cout << " <-> ";
            }
            current = current->next;
        }
        cout << endl;
    }

    // Phương thức hiển thị danh sách (duyệt ngược)
    void displayBackward() const {
        Node* current = tail; // Bắt đầu từ tail
        cout << "Danh sach (nguoc): ";
        while (current != nullptr) {
            cout << current->data;
            if (current->prev != nullptr) { // Sử dụng prev để di chuyển ngược
                cout << " <-> ";
            }
            current = current->prev;
        }
        cout << endl;
    }

    // --- Các phương thức chèn và xóa sẽ được thêm vào sau ---
};

Giải thích:

  • Node* head;, Node* tail;: Hai con trỏ thành viên private này giúp chúng ta luôn biết được điểm bắt đầu và kết thúc của danh sách.
  • DoublyLinkedList() : head(nullptr), tail(nullptr) {}: Constructor khởi tạo headtailnullptr, biểu thị một danh sách rỗng.
  • ~DoublyLinkedList(): Đây là destructor. Nó được gọi tự động khi một đối tượng DoublyLinkedList bị hủy (ví dụ: khi nó ra khỏi phạm vi). Nhiệm vụ của nó là giải phóng bộ nhớ mà chúng ta đã cấp phát động cho các nút bằng new Node(...). Nếu không có destructor này, sẽ xảy ra hiện tượng rò rỉ bộ nhớ (memory leak). Vòng lặp duyệt qua từng nút và dùng delete để giải phóng bộ nhớ.
  • displayForward(): Duyệt danh sách từ head đến tail bằng cách đi theo con trỏ next.
  • displayBackward(): Duyệt danh sách từ tail đến head bằng cách đi theo con trỏ prev. Đây là ưu điểm nổi bật của danh sách liên kết đôi so với danh sách liên kết đơn!

3. Các Thao Tác Chèn Nút

Bây giờ, chúng ta sẽ thêm các phương thức để thêm nút mới vào danh sách.

3.1. Chèn vào đầu danh sách (insertAtHead)

Thao tác này thêm một nút mới làm nút đầu tiên của danh sách.

// Thêm vào trong class DoublyLinkedList:
void insertAtHead(int val) {
    Node* newNode = new Node(val); // 1. Tạo nút mới

    if (head == nullptr) { // Trường hợp danh sách rỗng
        head = newNode;
        tail = newNode; // Nút đầu tiên cũng là nút cuối cùng
    } else { // Trường hợp danh sách không rỗng
        newNode->next = head; // 2. Nút mới trỏ tới head hiện tại
        head->prev = newNode; // 3. Head hiện tại trỏ ngược về nút mới
        head = newNode;       // 4. Cập nhật head là nút mới
    }
    cout << "Da chen " << val << " vao dau danh sach." << endl;
}

Giải thích:

  1. Tạo nút mới: Node* newNode = new Node(val); cấp phát bộ nhớ và tạo một nút mới với dữ liệu val. nextprev của nó ban đầu là nullptr nhờ constructor.
  2. Kiểm tra danh sách rỗng: Nếu headnullptr, danh sách đang trống. Nút mới là nút duy nhất, nên nó vừa là head vừa là tail.
  3. Danh sách không rỗng:
    • newNode->next = head;: Nút mới sẽ đứng trước head cũ, nên con trỏ next của nó phải trỏ đến head cũ.
    • head->prev = newNode;: Nút head cũ bây giờ có một nút đứng trước nó là newNode, nên con trỏ prev của head cũ phải trỏ đến newNode.
    • head = newNode;: Cuối cùng, cập nhật head của danh sách là newNode.
3.2. Chèn vào cuối danh sách (insertAtTail)

Thao tác này thêm một nút mới làm nút cuối cùng của danh sách.

// Thêm vào trong class DoublyLinkedList:
void insertAtTail(int val) {
    Node* newNode = new Node(val); // 1. Tạo nút mới

    if (tail == nullptr) { // Trường hợp danh sách rỗng (giống insertAtHead)
        head = newNode;
        tail = newNode;
    } else { // Trường hợp danh sách không rỗng
        newNode->prev = tail; // 2. Nút mới trỏ ngược về tail hiện tại
        tail->next = newNode; // 3. Tail hiện tại trỏ tới nút mới
        tail = newNode;       // 4. Cập nhật tail là nút mới
    }
    cout << "Da chen " << val << " vao cuoi danh sach." << endl;
}

Giải thích:

  1. Tạo nút mới: Tương tự insertAtHead.
  2. Kiểm tra danh sách rỗng: Tương tự insertAtHead.
  3. Danh sách không rỗng:
    • newNode->prev = tail;: Nút mới sẽ đứng sau tail cũ, nên con trỏ prev của nó phải trỏ đến tail cũ.
    • tail->next = newNode;: Nút tail cũ bây giờ có một nút đứng sau nó là newNode, nên con trỏ next của tail cũ phải trỏ đến newNode.
    • tail = newNode;: Cuối cùng, cập nhật tail của danh sách là newNode.
3.3. Chèn sau một nút cho trước (insertAfterNode)

Thao tác này yêu cầu tìm hoặc được cung cấp một con trỏ đến một nút hiện có, sau đó chèn nút mới vào sau nút đó. Thao tác này đặc biệt hữu ích và minh họa rõ ràng việc cập nhật cả hai con trỏ nextprev.

// Thêm vào trong class DoublyLinkedList:
void insertAfterNode(Node* prevNode, int val) {
    // 1. Kiểm tra nếu prevNode không hợp lệ (nullptr)
    if (prevNode == nullptr) {
        cout << "Loi: prevNode khong duoc la nullptr." << endl;
        return;
    }

    // 2. Tạo nút mới
    Node* newNode = new Node(val);

    // 3. Thiết lập con trỏ next và prev cho nút mới
    newNode->next = prevNode->next; // Nút mới trỏ tới nút mà prevNode đang trỏ tới
    newNode->prev = prevNode;       // Nút mới trỏ ngược về prevNode

    // 4. Cập nhật con trỏ next của prevNode
    prevNode->next = newNode;

    // 5. Cập nhật con trỏ prev của nút đứng SAU nút mới (nếu có)
    if (newNode->next != nullptr) {
        newNode->next->prev = newNode;
    } else {
        // Nếu newNode->next là nullptr, tức là newNode được chèn vào cuối danh sách
        tail = newNode; // Cập nhật tail là nút mới
    }
    cout << "Da chen " << val << " sau node co data = " << prevNode->data << "." << endl;
}

// Để sử dụng insertAfterNode, bạn cần tìm được con trỏ đến prevNode.
// Cần thêm phương thức tìm kiếm hoặc truy cập node.
// Tạm thời, ta có thể tìm thủ công hoặc thêm phương thức search.
// Ví dụ đơn giản phương thức search để minh họa:
Node* search(int val) const {
    Node* current = head;
    while (current != nullptr) {
        if (current->data == val) {
            return current; // Trả về con trỏ tới node nếu tìm thấy
        }
        current = current->next;
    }
    return nullptr; // Trả về nullptr nếu không tìm thấy
}

Giải thích:

  1. Kiểm tra tính hợp lệ của prevNode.
  2. Tạo nút mới newNode.
  3. Thiết lập liên kết cho newNode:
    • newNode->next = prevNode->next;: Con trỏ next của newNode sẽ trỏ tới nút mà prevNode đang trỏ tới (nút sau prevNode).
    • newNode->prev = prevNode;: Con trỏ prev của newNode trỏ ngược lại prevNode.
  4. Cập nhật liên kết của prevNode:
    • prevNode->next = newNode;: Con trỏ next của prevNode bây giờ trỏ tới newNode.
  5. Cập nhật liên kết của nút sau newNode (nếu có):
    • if (newNode->next != nullptr): Kiểm tra xem nút sau newNode có tồn tại không.
    • newNode->next->prev = newNode;: Nếu có, con trỏ prev của nút đó (là newNode->next) phải được cập nhật để trỏ ngược về newNode.
    • Nếu newNode->nextnullptr, tức là newNode được chèn vào vị trí cuối cùng, chúng ta cần cập nhật tail của danh sách.

Lưu ý: Để sử dụng insertAfterNode một cách thực tế, bạn sẽ cần một cách để lấy con trỏ đến nút prevNode mong muốn, ví dụ như bằng cách duyệt hoặc dùng một phương thức search. Tôi đã thêm một phương thức search đơn giản để bạn dễ hình dung cách sử dụng.

4. Thao Tác Xóa Nút

Xóa một nút trong danh sách liên kết đôi dễ dàng hơn nhiều so với danh sách liên kết đơn, đặc biệt là khi xóa một nút ở giữa hoặc cuối danh sách, vì chúng ta có con trỏ prev để truy cập nút đứng trước nó mà không cần duyệt lại từ đầu.

Chúng ta sẽ viết một phương thức để xóa một nút cụ thể dựa vào con trỏ tới nút đó.

// Thêm vào trong class DoublyLinkedList:
void deleteNode(Node* nodeToDelete) {
    // 1. Kiểm tra nếu nodeToDelete không hợp lệ (nullptr) hoặc danh sách rỗng
    if (head == nullptr || nodeToDelete == nullptr) {
        cout << "Khong the xoa. Danh sach rỗng hoặc node xoa la nullptr." << endl;
        return;
    }

    // 2. Xử lý các con trỏ của nút đứng trước và nút đứng sau nodeToDelete

    // Nếu có nút trước nodeToDelete
    if (nodeToDelete->prev != nullptr) {
        nodeToDelete->prev->next = nodeToDelete->next;
    } else {
        // Nếu nodeToDelete là head, cập nhật head
        head = nodeToDelete->next;
    }

    // Nếu có nút sau nodeToDelete
    if (nodeToDelete->next != nullptr) {
        nodeToDelete->next->prev = nodeToDelete->prev;
    } else {
        // Nếu nodeToDelete là tail, cập nhật tail
        tail = nodeToDelete->prev;
    }

    // 3. Giải phóng bộ nhớ của nodeToDelete
    cout << "Da xoa node co data = " << nodeToDelete->data << "." << endl;
    delete nodeToDelete;
}

// Ví dụ về cách xóa theo giá trị (cần sử dụng search trước)
void deleteByValue(int val) {
    Node* nodeToDelete = search(val); // Tìm node cần xóa
    if (nodeToDelete != nullptr) {
        deleteNode(nodeToDelete); // Gọi phương thức xóa node dựa trên con trỏ
    } else {
        cout << "Khong tim thay node co data = " << val << " de xoa." << endl;
    }
}

Giải thích:

  1. Kiểm tra các trường hợp lỗi: danh sách rỗng hoặc con trỏ truyền vào là nullptr.
  2. Cập nhật con trỏ của nút trước: Nếu nodeToDelete không phải là head (tức là nodeToDelete->prev không phải nullptr), thì con trỏ next của nút đứng trước nó (nodeToDelete->prev) phải được bỏ qua nodeToDelete và trỏ thẳng tới nút đứng sau nodeToDelete (nodeToDelete->next). Nếu nodeToDeletehead, chúng ta chỉ cần cập nhật head lên nút tiếp theo.
  3. Cập nhật con trỏ của nút sau: Tương tự, nếu nodeToDelete không phải là tail (tức là nodeToDelete->next không phải nullptr), thì con trỏ prev của nút đứng sau nó (nodeToDelete->next) phải được bỏ qua nodeToDelete và trỏ ngược về nút đứng trước nodeToDelete (nodeToDelete->prev). Nếu nodeToDeletetail, chúng ta chỉ cần cập nhật tail lùi về nút trước đó.
  4. Giải phóng bộ nhớ: Sau khi các liên kết đã được sửa, chúng ta sử dụng delete nodeToDelete để giải phóng vùng nhớ đã cấp phát cho nút đó.
  5. Phương thức deleteByValue minh họa cách bạn có thể kết hợp searchdeleteNode để xóa một nút dựa trên giá trị của nó.

Lưu ý: Việc xử lý các trường hợp đặc biệt (xóa head, xóa tail, xóa nút duy nhất trong danh sách) được tự động xử lý bởi logic cập nhật con trỏ headtail trong phương thức deleteNode. Nếu nút bị xóa là head, head được cập nhật. Nếu là tail, tail được cập nhật. Nếu danh sách chỉ còn một nút và nó bị xóa, cả headtail sẽ trở thành nullptr.

5. Ví dụ Minh Họa Toàn Bộ

Bây giờ, chúng ta hãy ghép tất cả lại trong một hàm main để thấy cách các phương thức hoạt động cùng nhau.

#include <iostream>

// --- Cấu trúc Node ---
struct Node {
    int data;
    Node* next;
    Node* prev;
    Node(int val) : data(val), next(nullptr), prev(nullptr) {}
};

// --- Lớp DoublyLinkedList ---
class DoublyLinkedList {
private:
    Node* head;
    Node* tail;

public:
    DoublyLinkedList() : head(nullptr), tail(nullptr) {}

    ~DoublyLinkedList() {
        Node* current = head;
        while (current != nullptr) {
            Node* next = current->next;
            delete current;
            current = next;
        }
        head = nullptr;
        tail = nullptr;
        cout << "\nDa giai phong bo nho danh sach." << endl;
    }

    // Phương thức chèn vào đầu
    void insertAtHead(int val) {
        Node* newNode = new Node(val);
        if (head == nullptr) {
            head = newNode;
            tail = newNode;
        } else {
            newNode->next = head;
            head->prev = newNode;
            head = newNode;
        }
        cout << "Da chen " << val << " vao dau danh sach." << endl;
    }

    // Phương thức chèn vào cuối
    void insertAtTail(int val) {
        Node* newNode = new Node(val);
        if (tail == nullptr) {
            head = newNode;
            tail = newNode;
        } else {
            newNode->prev = tail;
            tail->next = newNode;
            tail = newNode;
        }
        cout << "Da chen " << val << " vao cuoi danh sach." << endl;
    }

     // Phương thức tìm kiếm node theo giá trị
    Node* search(int val) const {
        Node* current = head;
        while (current != nullptr) {
            if (current->data == val) {
                return current;
            }
            current = current->next;
        }
        return nullptr;
    }

    // Phương thức chèn sau một node cho trước
    void insertAfterNode(Node* prevNode, int val) {
        if (prevNode == nullptr) {
            cout << "Loi: prevNode khong duoc la nullptr." << endl;
            return;
        }
        Node* newNode = new Node(val);
        newNode->next = prevNode->next;
        newNode->prev = prevNode;
        prevNode->next = newNode;
        if (newNode->next != nullptr) {
            newNode->next->prev = newNode;
        } else {
            tail = newNode;
        }
        cout << "Da chen " << val << " sau node co data = " << prevNode->data << "." << endl;
    }

    // Phương thức xóa node (dựa trên con trỏ)
    void deleteNode(Node* nodeToDelete) {
        if (head == nullptr || nodeToDelete == nullptr) {
            cout << "Khong the xoa. Danh sach rỗng hoặc node xoa la nullptr." << endl;
            return;
        }

        if (nodeToDelete->prev != nullptr) {
            nodeToDelete->prev->next = nodeToDelete->next;
        } else { // Xoa head
            head = nodeToDelete->next;
        }

        if (nodeToDelete->next != nullptr) {
            nodeToDelete->next->prev = nodeToDelete->prev;
        } else { // Xoa tail
            tail = nodeToDelete->prev;
        }

        cout << "Da xoa node co data = " << nodeToDelete->data << "." << endl;
        delete nodeToDelete;

        // Cập nhật head/tail nếu danh sách trở nên rỗng sau khi xóa
        if (head == nullptr) {
             tail = nullptr;
        }
    }

    // Phương thức xóa node theo giá trị
    void deleteByValue(int val) {
        Node* nodeToDelete = search(val);
        if (nodeToDelete != nullptr) {
            deleteNode(nodeToDelete);
        } else {
            cout << "Khong tim thay node co data = " << val << " de xoa." << endl;
        }
    }


    // Phương thức hiển thị danh sách (duyệt xuôi)
    void displayForward() const {
        Node* current = head;
        cout << "Danh sach (xuoi): ";
        if (current == nullptr) {
             cout << "rong";
        }
        while (current != nullptr) {
            cout << current->data;
            if (current->next != nullptr) {
                cout << " <-> ";
            }
            current = current->next;
        }
        cout << endl;
    }

    // Phương thức hiển thị danh sách (duyệt ngược)
    void displayBackward() const {
        Node* current = tail;
        cout << "Danh sach (nguoc): ";
         if (current == nullptr) {
             cout << "rong";
        }
        while (current != nullptr) {
            cout << current->data;
            if (current->prev != nullptr) {
                cout << " <-> ";
            }
            current = current->prev;
        }
        cout << endl;
    }

}; // Kết thúc class DoublyLinkedList

// --- Hàm main để chạy thử ---
int main() {
    // 1. Khởi tạo danh sách
    DoublyLinkedList dll;
    cout << "Khoi tao danh sach rong." << endl;
    dll.displayForward();

    // 2. Chen vao dau
    cout << "\n--- Chen vao dau ---" << endl;
    dll.insertAtHead(10); // DSLK: 10
    dll.insertAtHead(20); // DSLK: 20 <-> 10
    dll.insertAtHead(5);  // DSLK: 5 <-> 20 <-> 10
    dll.displayForward();
    dll.displayBackward();

    // 3. Chen vao cuoi
    cout << "\n--- Chen vao cuoi ---" << endl;
    dll.insertAtTail(30); // DSLK: 5 <-> 20 <-> 10 <-> 30
    dll.insertAtTail(40); // DSLK: 5 <-> 20 <-> 10 <-> 30 <-> 40
    dll.displayForward();
    dll.displayBackward();

    // 4. Chen sau mot node cho truoc (sau node 20)
    cout << "\n--- Chen sau node 20 ---" << endl;
    Node* node20 = dll.search(20);
    if (node20 != nullptr) {
        dll.insertAfterNode(node20, 25); // DSLK: 5 <-> 20 <-> 25 <-> 10 <-> 30 <-> 40
        dll.displayForward();
        dll.displayBackward();
    } else {
         cout << "Khong tim thay node 20 de chen sau." << endl;
    }

    // 5. Xoa node theo gia tri
    cout << "\n--- Xoa node theo gia tri ---" << endl;
    dll.deleteByValue(10); // Xoa node 10 (o giua)
    dll.displayForward();
    dll.deleteByValue(5);  // Xoa node 5 (head)
    dll.displayForward();
    dll.deleteByValue(40); // Xoa node 40 (tail)
    dll.displayForward();
    dll.deleteByValue(100); // Xoa node khong ton tai
    dll.displayForward();

    // DSLK hien tai: 20 <-> 25 <-> 30

    // Chen them de test xoa cac truong hop khac
    cout << "\n--- Chen lai de test xoa ---" << endl;
    dll.insertAtHead(1); // DSLK: 1 <-> 20 <-> 25 <-> 30
    dll.insertAtTail(50); // DSLK: 1 <-> 20 <-> 25 <-> 30 <-> 50
    dll.displayForward();

    cout << "\n--- Xoa het danh sach ---" << endl;
    dll.deleteByValue(25); // Xoa node 25 (o giua)
    dll.displayForward(); // DSLK: 1 <-> 20 <-> 30 <-> 50
    dll.deleteByValue(1); // Xoa head
    dll.displayForward(); // DSLK: 20 <-> 30 <-> 50
    dll.deleteByValue(50); // Xoa tail
    dll.displayForward(); // DSLK: 20 <-> 30
    dll.deleteByValue(20); // Xoa head moi
    dll.displayForward(); // DSLK: 30
    dll.deleteByValue(30); // Xoa node cuoi cung
    dll.displayForward(); // DSLK: rong


    // Khi doi tuong dll ra khoi pham vi cua main, destructor ~DoublyLinkedList() se tu dong duoc goi
    // de giai phong bo nho.

    return 0;
}

Giải thích hàm main:

  • Chúng ta tạo một đối tượng DoublyLinkedList có tên dll.
  • Lần lượt gọi các phương thức insertAtHead, insertAtTail để thêm các phần tử và hiển thị kết quả sau mỗi lần chèn.
  • Sử dụng phương thức search để tìm con trỏ đến nút có giá trị 20, sau đó gọi insertAfterNode để chèn 25 vào sau nó. Điều này minh họa cách bạn cần có con trỏ tới nút muốn chèn sau.
  • Thực hiện các thao tác xóa khác nhau bằng cách gọi deleteByValue, lần lượt xóa một nút ở giữa, nút đầu, nút cuối. Sau mỗi lần xóa, chúng ta hiển thị danh sách để kiểm tra.
  • Cuối cùng, chúng ta xóa từng nút còn lại cho đến khi danh sách rỗng để kiểm tra đầy đủ các trường hợp xóa.
  • Khi hàm main kết thúc, đối tượng dll sẽ bị hủy, và destructor ~DoublyLinkedList() mà chúng ta đã viết sẽ tự động được gọi để giải phóng tất cả bộ nhớ đã cấp phát cho các nút còn lại, tránh rò rỉ bộ nhớ. Thông báo "Da giai phong bo nho danh sach." sẽ xuất hiện ở cuối output.

Comments

There are no comments at the moment.