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

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ụngstruct
để 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ể thayint
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ểuNode*
, 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ểuNode*
, 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ộtNode
mới với cú phápNode newNode(someValue);
, constructor này sẽ được gọi để khởi tạodata
bằngval
, và quan trọng là thiết lậpnext
vàprev
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ênprivate
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ạohead
vàtail
lànullptr
, 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ượngDoublyLinkedList
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ằngnew 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ùngdelete
để giải phóng bộ nhớ.displayForward()
: Duyệt danh sách từhead
đếntail
bằng cách đi theo con trỏnext
.displayBackward()
: Duyệt danh sách từtail
đếnhead
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:
- 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ệuval
.next
vàprev
của nó ban đầu lànullptr
nhờ constructor. - Kiểm tra danh sách rỗng: Nếu
head
lànullptr
, 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
. - Danh sách không rỗng:
newNode->next = head;
: Nút mới sẽ đứng trướchead
cũ, nên con trỏnext
của nó phải trỏ đếnhead
cũ.head->prev = newNode;
: Núthead
cũ bây giờ có một nút đứng trước nó lànewNode
, nên con trỏprev
củahead
cũ phải trỏ đếnnewNode
.head = newNode;
: Cuối cùng, cập nhậthead
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:
- Tạo nút mới: Tương tự
insertAtHead
. - Kiểm tra danh sách rỗng: Tương tự
insertAtHead
. - Danh sách không rỗng:
newNode->prev = tail;
: Nút mới sẽ đứng sautail
cũ, nên con trỏprev
của nó phải trỏ đếntail
cũ.tail->next = newNode;
: Núttail
cũ bây giờ có một nút đứng sau nó lànewNode
, nên con trỏnext
củatail
cũ phải trỏ đếnnewNode
.tail = newNode;
: Cuối cùng, cập nhậttail
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ỏ next
và prev
.
// 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:
- Kiểm tra tính hợp lệ của
prevNode
. - Tạo nút mới
newNode
. - Thiết lập liên kết cho
newNode
:newNode->next = prevNode->next;
: Con trỏnext
củanewNode
sẽ trỏ tới nút màprevNode
đang trỏ tới (nút sauprevNode
).newNode->prev = prevNode;
: Con trỏprev
củanewNode
trỏ ngược lạiprevNode
.
- Cập nhật liên kết của
prevNode
:prevNode->next = newNode;
: Con trỏnext
củaprevNode
bây giờ trỏ tớinewNode
.
- 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 saunewNode
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->next
lànullptr
, tức lànewNode
được chèn vào vị trí cuối cùng, chúng ta cần cập nhậttail
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:
- 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
. - 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ảinullptr
), thì con trỏnext
của nút đứng trước nó (nodeToDelete->prev
) phải được bỏ quanodeToDelete
và trỏ thẳng tới nút đứng saunodeToDelete
(nodeToDelete->next
). NếunodeToDelete
làhead
, chúng ta chỉ cần cập nhậthead
lên nút tiếp theo. - 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ảinullptr
), thì con trỏprev
của nút đứng sau nó (nodeToDelete->next
) phải được bỏ quanodeToDelete
và trỏ ngược về nút đứng trướcnodeToDelete
(nodeToDelete->prev
). NếunodeToDelete
làtail
, chúng ta chỉ cần cập nhậttail
lùi về nút trước đó. - 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 đó. - Phương thức
deleteByValue
minh họa cách bạn có thể kết hợpsearch
vàdeleteNode
để 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ỏ head
và tail
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ả head
và tail
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êndll
. - 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ọiinsertAfterNode
để 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ượngdll
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