Bài giảng Cấu trúc dữ liệu & Thuật toán [CO2003] - Chương 4: List (Danh sách)

Người đăng

Ẩn danh
89
1
0

Phí lưu trữ

30 Point

Tóm tắt

I. Tổng quan về cấu trúc dữ liệu và thuật toán List ch04

Trong lĩnh vực khoa học máy tính, cấu trúc dữ liệu và thuật toán là nền tảng cốt lõi, và chương 4 về List (danh sách) là một trong những chương quan trọng nhất. Một danh sách tuyến tính (Linear List) được định nghĩa là một cấu trúc dữ liệu trong đó mỗi phần tử có một phần tử kế thừa duy nhất. Theo tài liệu Data Structure and Algorithms [CO2003] của TS. Nguyễn Đức Dũng, đây là cấu trúc tuần tự hữu hạn các phần tử. Chủ đề "Data structure and algorithms cấu trúc dữ liệu và thuật toán ch04 list" tập trung vào hai phương pháp triển khai chính: danh sách mảng (Array List) và danh sách liên kết (Linked List). Mỗi phương pháp đều có những đặc điểm, ưu và nhược điểm riêng, ảnh hưởng trực tiếp đến hiệu suất của các thuật toán. Hiểu rõ bản chất của từng loại danh sách là bước đầu tiên để lựa chọn cấu trúc dữ liệu phù hợp, từ đó tối ưu hóa các thao tác cơ bản như thêm, xóa, tìm kiếm và duyệt phần tử. Việc nắm vững kiến thức này không chỉ giúp giải quyết các bài toán lập trình hiệu quả mà còn là tiền đề để tiếp cận các cấu trúc dữ liệu phức tạp hơn như Stack và Queue.

1.1. Định nghĩa danh sách tuyến tính và các thao tác cơ bản

Danh sách tuyến tính (Linear List) là một chuỗi các phần tử được sắp xếp theo một thứ tự xác định. Mỗi phần tử trong danh sách, ngoại trừ phần tử cuối cùng, đều có một phần tử kế tiếp duy nhất. Các thao tác cơ bản trên một danh sách bao gồm: tạo danh sách rỗng, thêm phần tử vào một vị trí cụ thể, xóa phần tử khỏi danh sách, tìm kiếm trong danh sách để xác định vị trí của một phần tử, và duyệt danh sách để xử lý từng phần tử. Các thao tác mở rộng hơn có thể kể đến như kiểm tra danh sách rỗng, lấy kích thước danh sách, hoặc gộp hai danh sách. Hiệu quả của các thao tác trên danh sách này phụ thuộc rất nhiều vào cách thức cài đặt cấu trúc dữ liệu bên dưới, có thể là mảng hoặc con trỏ.

1.2. So sánh sơ bộ giữa danh sách mảng và danh sách liên kết

Danh sách mảng (Array List) lưu trữ các phần tử trong một vùng nhớ liên tục, cho phép truy cập trực tiếp và nhanh chóng đến bất kỳ phần tử nào thông qua chỉ số. Ngược lại, danh sách liên kết (Linked List) lưu trữ các phần tử trong các vùng nhớ riêng biệt, được gọi là các node. Mỗi node chứa dữ liệu và một con trỏ (pointer) trỏ đến node tiếp theo. Sự khác biệt cơ bản này dẫn đến sự đánh đổi về hiệu năng: Array List mạnh về truy xuất nhưng yếu về chèn/xóa ở giữa, trong khi Linked List linh hoạt trong việc chèn/xóa nhưng lại chậm khi truy cập ngẫu nhiên. Việc lựa chọn giữa hai loại cấu trúc này là một quyết định quan trọng trong thiết kế thuật toán.

II. Thách thức về độ phức tạp khi chọn Array List và Linked List

Việc lựa chọn giữa danh sách mảngdanh sách liên kết không chỉ dựa trên nhu cầu lưu trữ mà còn phải cân nhắc kỹ lưỡng về độ phức tạp thời gian (time complexity) của các thuật toán. Đây là thách thức lớn nhất khi làm việc với chủ đề "Data structure and algorithms cấu trúc dữ liệu và thuật toán ch04 list". Phân tích thuật toán bằng ký hiệu Big O Notation cho thấy sự khác biệt rõ rệt về hiệu suất. Ví dụ, việc chèn một phần tử vào đầu một Array List đòi hỏi phải dịch chuyển tất cả các phần tử hiện có, dẫn đến độ phức tạp O(n). Trong khi đó, thao tác tương tự trên Linked List chỉ cần thay đổi một vài con trỏ, với độ phức tạp là O(1). Ngược lại, truy cập một phần tử ở vị trí thứ i trong mảng là O(1), nhưng với danh sách liên kết, ta phải duyệt từ đầu, mất thời gian O(n). Sự đánh đổi này buộc các lập trình viên phải phân tích kỹ lưỡng tần suất của các thao tác truy cập, chèn, xóa để đưa ra quyết định tối ưu nhất cho ứng dụng cụ thể.

2.1. Phân tích Time Complexity của các thao tác trên Array List

Với Array List, hay còn gọi là triển khai liên tục (Contiguous Implementation), độ phức tạp thời gian của các thao tác rất khác nhau. Truy cập phần tử tại một chỉ số bất kỳ (get) có độ phức tạp là O(1) vì các phần tử được lưu trong bộ nhớ liền kề. Tuy nhiên, các thao tác thêm phần tử (insert) và xóa phần tử (remove) ở giữa danh sách yêu cầu dịch chuyển các phần tử phía sau, dẫn đến time complexity là O(n) trong trường hợp xấu nhất. Thao tác thêm vào cuối danh sách, nếu mảng chưa đầy, thường là O(1). Nhưng khi mảng đầy và cần cấp phát lại bộ nhớ, nó có thể trở thành O(n). Phân tích này cho thấy Array List phù hợp cho các ứng dụng yêu cầu truy cập ngẫu nhiên thường xuyên và ít thay đổi kích thước.

2.2. Đánh giá Big O Notation cho các hoạt động của Linked List

Đối với Linked List, Big O Notation lại cho thấy một bức tranh khác. Việc thêm phần tử hoặc xóa phần tử ở đầu danh sách (thay đổi con trỏ head) là một thao tác cực kỳ hiệu quả với độ phức tạp O(1). Tương tự, nếu có một con trỏ trỏ đến node ngay trước vị trí cần chèn/xóa, thao tác cũng chỉ mất O(1). Tuy nhiên, điểm yếu lớn nhất của danh sách liên kếttìm kiếm trong danh sách và truy cập phần tử theo vị trí. Cả hai thao tác này đều đòi hỏi phải duyệt danh sách từ đầu, dẫn đến time complexity là O(n). Do đó, Linked List là lựa chọn lý tưởng cho các ứng dụng có tần suất chèn/xóa cao ở các vị trí đầu hoặc cuối danh sách, chẳng hạn như khi cài đặt Stack và Queue.

III. Hướng dẫn cài đặt danh sách liên kết đơn Singly Linked List

Một trong những nội dung cốt lõi của "Data structure and algorithms cấu trúc dữ liệu và thuật toán ch04 list" là cài đặt danh sách liên kết. Danh sách liên kết đơn (Singly Linked List) là dạng cơ bản và phổ biến nhất. Cấu trúc này bao gồm một chuỗi các node, mỗi node chứa hai thành phần chính: dữ liệu (data) và một con trỏ (link hoặc next) trỏ đến node kế tiếp trong danh sách. Node đầu tiên được gọi là head, và con trỏ của node cuối cùng trỏ đến NULL để đánh dấu kết thúc danh sách. Việc cài đặt (linked list implementation) đòi hỏi phải định nghĩa cấu trúc của node và lớp quản lý danh sách. Lớp này sẽ chứa một con trỏ head và các phương thức để thực hiện các thao tác trên danh sách như thêm, xóa, tìm kiếm. Hiểu rõ cách các con trỏ được cập nhật trong mỗi thao tác là chìa khóa để làm chủ cấu trúc dữ liệu này. Ví dụ, khi thêm phần tử vào đầu, con trỏ head phải được cập nhật để trỏ đến node mới.

3.1. Cấu trúc của một Node và con trỏ Head Tail trong danh sách

Một node trong danh sách liên kết đơn là một thực thể cơ bản. Trong C++, nó thường được định nghĩa bằng struct hoặc class, chứa ít nhất hai trường: một trường để lưu dữ liệu và một trường là con trỏ (Node* link) để lưu địa chỉ của node tiếp theo. Toàn bộ danh sách được quản lý thông qua một vài con trỏ đặc biệt. Con trỏ head luôn trỏ đến node đầu tiên của danh sách. Nếu head là NULL, danh sách được coi là rỗng. Trong một số cách cài đặt, người ta còn sử dụng thêm con trỏ tail, trỏ đến node cuối cùng. Việc có con trỏ head và tail giúp tối ưu hóa một số thao tác, ví dụ như thêm một phần tử vào cuối danh sách chỉ mất O(1) thay vì phải duyệt toàn bộ danh sách để tìm vị trí cuối cùng.

3.2. Quy trình thêm và xóa một phần tử khỏi Singly Linked List

Thao tác thêm phần tử vào Singly Linked List gồm các bước: tạo một node mới, gán dữ liệu cho nó, và sau đó cập nhật các con trỏ để liên kết node này vào danh sách. Vị trí chèn (đầu, giữa, cuối) sẽ quyết định các con trỏ nào cần được thay đổi. Ví dụ, để chèn vào giữa, cần cập nhật con trỏ của node phía trước để trỏ đến node mới, và con trỏ của node mới trỏ đến node phía sau. Tương tự, xóa phần tử đòi hỏi phải tìm node cần xóa, sau đó điều chỉnh con trỏ của node đứng trước nó để "nhảy cóc" qua node bị xóa và trỏ thẳng đến node kế tiếp. Cuối cùng, giải phóng bộ nhớ của node đã bị xóa để tránh rò rỉ bộ nhớ. Cả hai quy trình này đều minh họa sự linh hoạt của danh sách liên kết.

IV. Khám phá các biến thể của cấu trúc dữ liệu Linked List

Ngoài danh sách liên kết đơn, chủ đề "Data structure and algorithms cấu trúc dữ liệu và thuật toán ch04 list" còn giới thiệu các biến thể nâng cao hơn để giải quyết những hạn chế của cấu trúc cơ bản. Các biến thể này cung cấp thêm tính năng và hiệu quả cho những trường hợp sử dụng cụ thể. Hai biến thể nổi bật nhất là danh sách liên kết đôi (Doubly Linked List) và danh sách liên kết vòng (Circular Linked List). Doubly Linked List cho phép duyệt theo cả hai chiều (tiến và lùi) bằng cách thêm một con trỏ trỏ đến node phía trước. Trong khi đó, Circular Linked List tạo ra một vòng lặp bằng cách cho con trỏ của node cuối cùng trỏ ngược lại node đầu tiên. Mỗi biến thể này mở ra những khả năng mới trong việc thiết kế thuật toán, ví dụ như dễ dàng xóa một node bất kỳ trong danh sách đôi mà không cần con trỏ đến node phía trước, hay duyệt vòng tròn trong các ứng dụng như quản lý tiến trình của hệ điều hành.

4.1. Cách hoạt động của danh sách liên kết đôi Doubly Linked List

Danh sách liên kết đôi (Doubly Linked List) cải tiến Singly Linked List bằng cách thêm vào mỗi node một con trỏ thứ hai, thường gọi là prev (previous), trỏ đến node đứng ngay trước nó. Nhờ vậy, mỗi node đều biết được cả node kế tiếp và node phía trước. Lợi ích lớn nhất của cấu trúc này là khả năng duyệt danh sách theo hai chiều. Thao tác xóa phần tử cũng trở nên hiệu quả hơn, vì khi có con trỏ đến một node bất kỳ, ta có thể dễ dàng tìm thấy node trước và sau nó để cập nhật liên kết mà không cần duyệt từ đầu. Tuy nhiên, đánh đổi lại là việc tốn thêm bộ nhớ cho mỗi node và các thao tác chèn/xóa trở nên phức tạp hơn một chút do phải cập nhật nhiều con trỏ hơn.

4.2. Tìm hiểu về danh sách liên kết vòng Circular Linked List

Một danh sách liên kết vòng (Circular Linked List) là một biến thể trong đó con trỏ next của node cuối cùng không trỏ đến NULL, mà trỏ ngược lại node đầu tiên (head). Điều này tạo ra một vòng khép kín. Cấu trúc này rất hữu ích trong các ứng dụng cần duyệt qua danh sách nhiều lần một cách liên tục, ví dụ như playlist nhạc lặp lại hoặc phân chia thời gian cho các tiến trình trong hệ điều hành (round-robin scheduling). Với danh sách liên kết vòng, bất kỳ node nào cũng có thể được coi là điểm bắt đầu. Thường thì người ta chỉ cần một con trỏ duy nhất trỏ đến node cuối cùng, vì từ đó có thể dễ dàng truy cập node đầu tiên (last->next). Cấu trúc này có thể được áp dụng cho cả danh sách liên kết đơn và đôi.

V. Ứng dụng thực tiễn của List để cài đặt Stack và Queue

Kiến thức từ "Data structure and algorithms cấu trúc dữ liệu và thuật toán ch04 list" không chỉ mang tính lý thuyết mà còn có ứng dụng thực tiễn vô cùng quan trọng. Một trong những ứng dụng phổ biến nhất của danh sách là dùng để cài đặt hai cấu trúc dữ liệu trừu tượng khác: Stack và Queue. Stack (ngăn xếp) hoạt động theo nguyên tắc LIFO (Last-In, First-Out), tức là phần tử vào sau cùng sẽ ra trước tiên. Queue (hàng đợi) thì hoạt động theo nguyên tắc FIFO (First-In, First-Out), phần tử nào vào trước sẽ ra trước. Sử dụng danh sách liên kết để cài đặt hai cấu trúc này mang lại hiệu quả cao. Cụ thể, các thao tác pushpop của Stack, hay enqueuedequeue của Queue, có thể được thực hiện với độ phức tạp thời gian O(1) khi sử dụng linked list, vượt trội so với việc dùng mảng có kích thước cố định.

5.1. Phương pháp dùng Linked List để triển khai cấu trúc Stack

Để cài đặt Stack bằng danh sách liên kết, các thao tác chính được ánh xạ như sau: thao tác push (thêm một phần tử vào đỉnh stack) tương đương với việc thêm phần tử vào đầu danh sách liên kết. Thao tác pop (lấy và xóa phần tử ở đỉnh stack) tương ứng với việc xóa phần tử ở đầu danh sách. Cả hai thao tác này, khi thực hiện trên linked list, chỉ cần thay đổi con trỏ head và một vài liên kết, do đó đạt được time complexity là O(1). Cách cài đặt này rất linh hoạt vì kích thước của Stack có thể tăng giảm động mà không bị giới hạn bởi kích thước mảng ban đầu.

5.2. Kỹ thuật cài đặt Queue hiệu quả với danh sách liên kết

Tương tự, Queue có thể được cài đặt hiệu quả bằng danh sách liên kết, đặc biệt là khi sử dụng cả con trỏ head và tail. Thao tác enqueue (thêm một phần tử vào cuối hàng đợi) sẽ được thực hiện bằng cách thêm một node mới vào cuối danh sách và cập nhật con trỏ tail. Thao tác dequeue (lấy và xóa phần tử ở đầu hàng đợi) được thực hiện bằng cách xóa node đầu tiên và cập nhật con trỏ head. Với việc sử dụng cả headtail, cả hai thao tác cơ bản này của Queue đều có độ phức tạp thời gian là O(1), đảm bảo hiệu suất cao cho các ứng dụng xử lý hàng đợi.

16/07/2025