24
More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007 1 Tóm tt: Tài liu này gii thiu vnhng cu trúc và kĩ thuật cơ bản để xây dng mt danh sách liên kết vi nhiu gii thích, hình v, mã ngun mu và bài tp. Tài liu này thì rt cn thiết nếu bn mun hiu vdanh sách liên kết hay mun xem nhng ví dsdng con trmt cách sâu sc. Mt tài liệu khác đi kèm là, nhng vấn đề vdanh sách liên kết sbao hàm độ khó rộng hơn. Danh sách liên kết thì cn phi hc vì hai lý do. Rõ ràng nht, danh sách liên kết đơn là một cu trúc dliu mà bn smun dùng trong chương trình thc tế. Vic biết được sc mạnh và điểm yếu ca danh sách liên kết smang đến cho bn sđánh giá đúng hơn về không gian, thi gian và nhng vấn đề vmã nguồn để suy nghĩ về bt ckiu dliu nào nói chung. Thhai, danh sách liên kết là mt cách tuyt vời để hc vcon tr. Trong thc t ế, bn có thskhông dùng mt danh sách liên kết trong chương trình thc tế, nhưng bạn chc chn sdùng nhiu con tr. Các vấn đề vdanh sách liên kết là mt skết hp tuyệt đẹp gia thut toán và các thao tác con tr. Theo truyn thng, danh sách liên kết là mt lĩnh vực mà các lp trình viên mi bắt đầu thc tập để hiu vcon tr. Độc gi: Tài liu này tha nhn các vấn đề cơ bản ca lp trình và con tr. Các bài viết dùng cú pháp C cho ví dnhưng giải thích tránh sdng C nhiu nht có t- tht sthì bài viết hướng ti nhng khái nim quan trong ca các thao tác con trthut toán danh sách liên kết. Ni dung tài liu: Phn 1 –Cu trúc danh sách liên kết cơ bản Phn 2 – Xây dng danh sách liên kết cơ bản Phn 3 – Các kthut code danh sách liên kết Phn 4 – Các mã ngun ví dBài viết được nhóm itspirit dch ttài liu vlinked list của trường Stanford và có mt sthông tin thoangtran.wordpress.com. Tài liu dch v“nhng vấn đề vdanh sách liên kết” sđược cung cp cho các bn trong thi gian sm nhất. Xin cám ơn các bạn .

[Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

Embed Size (px)

Citation preview

Page 1: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

1

Tóm tắt:

Tài liệu này giới thiệu về những cấu trúc và kĩ thuật cơ bản để xây dựng một danh sách liên kết với nhiều giải thích, hình vẽ, mã nguồn mẫu và bài tập. Tài liệu này thì rất cần thiết nếu bạn muốn hiểu về danh sách liên kết hay muốn xem những ví dụ sử dụng con trỏ một cách sâu sắc. Một tài liệu khác đi kèm là, những vấn đề về danh sách liên kết sẽ bao hàm độ khó rộng hơn.

Danh sách liên kết thì cần phải học vì hai lý do. Rõ ràng nhất, danh sách liên kết đơn là một cấu trúc dữ liệu mà bạn sẽ muốn dùng trong chương trình thực tế. Việc biết được sức mạnh và điểm yếu của danh sách liên kết sẽ mang đến cho bạn sự đánh giá đúng hơn về không gian, thời gian và những vấn đề về mã nguồn để suy nghĩ về bất cứ kiểu dữ liệu nào nói chung. Thứ hai, danh sách liên kết là một cách tuyệt vời để học về con trỏ. Trong thực tế, bạn có thể sẽ không dùng một danh sách liên kết trong chương trình thực tế, nhưng bạn chắc chắn sẽ dùng nhiều con trỏ. Các vấn đề về danh sách liên kết là một sự kết hợp tuyệt đẹp giữa thuật toán và các thao tác con trỏ. Theo truyền thống, danh sách liên kết là một lĩnh vực mà các lập trình viên mới bắt đầu thực tập để hiểu về con trỏ.

Độc giả:

Tài liệu này thừa nhận các vấn đề cơ bản của lập trình và con trỏ. Các bài viết dùng cú pháp C cho ví dụ nhưng giải thích tránh sử dụng C nhiều nhất có tể - thật sự thì bài viết hướng tới những khái niệm quan trong của các thao tác con trỏ và thuật toán danh sách liên kết.

Nội dung tài liệu:

Phần 1 –Cấu trúc danh sách liên kết cơ bản

Phần 2 – Xây dựng danh sách liên kết cơ bản

Phần 3 – Các kỹ thuật code danh sách liên kết

Phần 4 – Các mã nguồn ví dụ

Bài viết được nhóm itspirit dịch từ tài liệu về linked list của trường Stanford và có một số thông tin từ hoangtran.wordpress.com. Tài liệu dịch về “những vấn đề về danh sách liên kết” sẽ được cung cấp cho các bạn trong thời gian sớm nhất. Xin cám ơn các bạn .

Page 2: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

2

Phần 1 –Cấu trúc danh sách liên kết cơ bản Tại sao lại sử dụng danh sách liên kết?

Danh sách liên kết và mảng thì tương tự nhau khi chúng đều chứa tập hợp các dữ liệu. Mảng và các danh sách liên kết đều chứa các phần tử (elements). Trong danh sách liên kết, loại dữ liệu cụ thể của phần tử thì không quan trọng vì về cơ bản cấu trúc dữ liệu này có thể chứa bất cứ loại phần tử nào. Một cách để nghĩ về danh sách liên kết là xem mảng hoạt động như thế nào và nghĩ về cách tiếp cận tương tự.

Ôn lại mảng. Mảng là cấu trúc dữ liệu thường dùng nhất để lưu tập hợp các phần tử. Trong nhiều ngôn ngữ, mảng thì thuận tiện để khai báo và với việc dùng cú pháp [ ] để truy xuất các phần tử qua số chỉ thị. Ví dụ sau sẽ cho thấy vào đoạn mã nguồn tiêu biểu của mảng và chỉ ra cách nhìn mảng trong bộ nhớ. Mã nguồn cấp phát cho mảng int giá trị [100], và đặt 3 phần tử đầu là 1 2 3 và để phần còn lại của mảng không được khởi gán.

void ArrayTest() {

int scores[100];

// operate on the elements of the scores array...

scores[0] = 1;

scores[1] = 2;

scores[2] = 3;

}

Sau đây là bản vẽ thể hiện các giá trị của mảng được lưu thế nào trong bộ nhớ. Vấn đề chính là toàn bộ mảng được khai báo trong một khối của bộ nhớ. Mỗi phần tử của mảng có ô nhớ riêng của chúng trong mảng. Bất cứ phần tử nào trong mảng cũng có thể được truy xuất trực tiếp thông qua sử dụng cú pháp [ ].

Bất lợi của mảng là …

1) Kích thước của một mảng thì cố định – trong trường hợp này là 100 phần tử. Hầu như kích thước này được xác định lúc biên dịch với khai báo như trên. Với một chút cố gắng, kích thước của mảng có thể được hoãn lại cho tới khi mảng được tạo lúc chạy, như sau đó vẫn cố định kích thước

Page 3: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

3

2) Bởi vì điều (1), cách thuận tiện nhất cho người lập trình là khai báo một mảng mà “vừa đủ lớn”. Mặc dù thuận tiện như vậy, chiến lược này vẫn có 2 bất lợi (a) phần lớn thời gian thì chỉ dùng 20 hoặc 30 phần tử của dãy và 70% bộ nhớ của mảng thì bị lãng phí. (b) Nếu chương trình cần truy xuất hơn 100 dữ liệu, mã nguồn sẽ bị hỏng. Điều bất ngờ là một lượng lớn các mã nguồn thương mại thì dùng kiểu khai báo khờ khạo này, cái mà lãng phí bộ nhớ ở hầu hết trường hợp và bị hỏng khi ở trường hợp đậc biệt.

3) Việc chèn những phần tử mới vào trước thì khá là tốn kém vì những phần tử đã có cần phải dịch chuyển qua để tạo chỗ trống.

Danh sách liên kết thì cũng có điểm mạnh và điểm yếu của nó nhưng chúng thường tốt hơn ở những chỗ mà mảng yếu. Các đặc điểm của mảng sinh ra từ chiến lược cấp phát bộ nhớ cho những phần tử của nó trong một khối bộ nhớ. Danh sách liên kết dùng một chiến lược hoàn toàn khác. Như chúng ta sẽ thấy, danh sách liên kết cấp phát bộ nhớ chỗ mỗi phần tử một cách riêng biệt và chỉ khi chúng cần thiết.

Nhắc lại về con trỏ Sau đây chúng ta sẽ ôn lại nhanh về các thuật ngữ và luật của con trỏ. Danh sách liên kết đơn cũng theo sau bởi những điều luật này.

Pointer/Pointee: Một con trỏ “pointer” sẽ lưu một reference đến một biến khác được biết như là pointee của nó. Con trỏ có thể được thiết lập giá trị NULL có nghĩa là nó refer đến một pointee nào. (Trong C và C++, giá trị NULL có thể được sử dụng như là giá trị boolean false).

Dereference: Toán tử dereference trên con trỏ cho phép truy nhập vào pointee của nó. Một pointer chỉ có thể bị dereference sau khi nó được thiết lập trỏ đến một pointee cụ thể. Một pointer mà không có pointee thì là bad pointer và không thể bị dereference.

Bad pointer: Một pointer mà không được trỏ vào một pointee thì là “bad” và không thể dereference. Trong C và C++, việc dereference một bad pointer đôi khi gây xung đột ngay lập tức và làm hỏng bộ nhớ của chương trình đang chạy, gây nên “không biết đường nào mà lần”. Kiểu lỗi này rất khó để theo dõi. Trong C và C++, tất cả các pointer bắt đầu bằng bad values (những giá trị ngẫu nhiên), do đó rất dễ tình cờ sử dụng bad pointer. Những đoạn mã đúng sẽ thiết lập mỗi pointer có một good value trước khi sử dụng chúng. Chính vì vậy sử dụng bad pointer là một lỗi rất phổ biến trong C/C++. Với Java và các ngôn ngữ khác, các pointers được tự động bắt đầu với giá trị NULL, do đó quá trình dereference sẽ được dễ dàng detect nên các chương trình Java dễ gỡ lỗi này hơn nhiều.

Pointer assignment: Một phép gán giữa hai con trỏ như p = q; sẽ làm cho hai pointer trỏ vào cùng một pointee. Nó sẽ không copy vùng nhớ của pointee. Sau phép gán thì cả hai pointer sẽ chỉ vào cùng một vùng nhớ của pointee.

malloc(): malloc() là một hàm hệ thống mà cấp pháp một vùng nhớ trong “heap” và trả về con trỏ tới vùng nhớ mới đó. Prototype của malloc() và các hàm khác ở trong stdlib.h. Tham số của malloc() là một số nguyên là kích thước của vùng nhớ cần cấp phát tính theo bytes. Không giống như các biến cục bộ (“stack”), vùng nhớ heap không tự động giải phóng khi hàm tạo thoát ra. malloc() sẽ trả về NULL nếu nó không thế đáp ứng được yêu cầu cấp phát. Bạn nên kiểm tra trường hợp NULL với assert() nếu bạn mong nó an toàn. Hầu hết các hệ điều hành tiên tiến sẽ ném ra một exception hoặc làm việc bắt lỗi tự động trong việc cấp phát bộ nhớ của chúng, do đó không nhất thiết là trong đoạn mã của bạn phải kiểm tra việc cấp phát bộ nhớ thất bại.

free(): free() thì ngược với malloc(). Gọi hàm free() trên vùng nhớ trên heap để chỉ ra rằng hệ thống đã thực hiện xong và giải phóng vùng nhớ đó. Tham số của free là một con trỏ tới vùng nhớ trên heap – con trỏ mà chúng ta đã có được thông qua lời gọi tới hàm malloc().

Một danh sách liên kết là như thế nào ? Một mảng thì cấp phát bộ nhớ để cho tất cả các phần tử của nó thì tập trung lại như một khối ở trong bộ nhớ.

Ngược lại, một danh sách liên kết thì cấp phát vùng nhớ cho mỗi phần tử riêng biệt trong những khối bộ nhớ riêng gọi là

Page 4: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

4

“các phần tử của danh sách liên kết” hay “node”. Danh sách thì có một cấu trúc tổng quát bằng các sử dụng các con trỏ để kết nối các node lại với nhau như các dây xích.

Mỗi node thì chứa hai trường giá trị : một trường “data” để chứa dữ liệu và một trường “next” để chứa con trỏ dùng để nối một node với node khác. Mỗi node thì được cấp phát ở trong heap với một hàm malloc(), nên bộ nhớ node vẫn luôn tồn tại cho tới khi nó bị giải phóng bằng một hàm free(). Trước một danh sách là một con trỏ trỏ tới phần tử node đầu tiên. Sau đây là mô tả về một danh sách chứa các số 1, 2, 3 :

Hình vẽ này thể hiện một danh sách được tạo trong bộ nhớ bằng hàm BuildOneTwoThree() (mã nguồn sẽ được đưa ra ở dưới). Phần khởi đầu của danh sách liên kết thì được chứa trong một con trỏ “head” – cái mà trỏ tới node đầu tiên. Node đầu tiên này chứa một con trỏ để trỏ tới node thứ hai. Node thứ hai chứa một con trỏ tới node thứ ba…và cứ như thế. Con trỏ cuối cùng có trường “next” của nó được gán bằng NULL để đánh dấu kết thúc của danh sách. Mã nguồn có thể truy cập và bất cứ node nào trong danh sách bắt đầu tử head thông qua con trỏ next. Các thao tác tới phần đầu của danh sách thì diễn ra nhanh, trong khi truy xuất tới các phần tử ở phía sau sẽ tốn nhiều thời gian hơn. Kiểu truy xuất dữ liệu này thì tốn kém hơn kiểu truy xuất bằng [ ] của mảng. Trong khía cạnh này, danh sách liên kết thì kém hiệu quả hơn mảng. Việc vẽ ra bảng trên thì khá cần thiết trong việc suy nghĩ về mã nguồn con trỏ, nên phần lớn ví dụ trong tài liệu này sẽ kết hợp code với việc mô tả bằng hình vẽ. Trong trường hợp này, con trỏ head thì là một biến cục bộ bình thường, nên nó được vẽ tách biệt bên trái để thể hiện nó được lưu trong stack. Còn các node thì được vẽ bên phải để thể hiện chúng được cấp phát ở trong heap.

Danh sách rỗng – NULL Danh sách ở trên là một danh sách có “độ dài ba” vì nó được tạo bởi ba node với trường .next của node cuối gán bằng NULL. Ta cũng cần có một cách biểu diễn một danh sách rỗng – danh sách có 0 node. Các biểu diễn thông dụng nhất được chọn cho danh sách rỗng là một con trỏ head NULL. Tất cả các mã nguồn được thể hiện trong tài liệu này thì vẫn hoạt động đúng cho danh sách rỗng. Khi làm việc với mã nguồn danh sách liên kết, việc nhớ để kiểm tra danh sách rỗng là một thói quen tốt. Thỉnh thoảng, trường hợp danh sách rỗng hoạt động giống như các trường hợp khác. Nhưng đôi khi nó cần một trường hợp đặc biệt. Dù sao đi nữa thì tốt nhất là cứ nghĩ tới nó.

Page 5: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

5

Kiểu của danh sách liên kết: Node và Pointer Trước khi viết mã nguồn để xây dựng danh sách ở trên, chúng ta cần hai kiểu dữ liệu

NODE: Kiểu dữ liệu cho nodes sẽ tạo ra phần thân cho danh sách. Chúng được cấp phát ở trong heap. Mỗi node chứa một phần tử dữ liệu và một con trỏ tới node tiếp theo. Hãy ghi : struct node{ }

struct node {

int data;

struct node* next;

};

Node Pointer : Kiểu dữ liệu cho con trỏ tới các nodes. Đây sẽ là kiểu dữ liệu của con trỏ head và các trường .next trong mỗi node. Trong C và C++, không cần phải khai báo một loại riêng, đơn giản là kiểu của node + “*”. Hãy ghi: struct node *

BuildOneTwoThree() Function Đây là một hàm đơn giản mà dùng phép toán con trỏ để tạo ra một danh sách {1,2,3}. Hàm này giải thích cách gọi

malloc() và phép gán con trỏ để xây dựng một cấu trúc con trỏ trong heap.

Page 6: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

6

Bài tập:

Hỏi: Viết một đoạn code với số lượng phép gán (= ) ít nhất mà sẽ xây dựng cấu trúc bộ nhớ trên

Trả lời: Cần 3 lần gán để malloc(). 3 phép gán int để tạo các biến int. 4 phép gán con trỏ để tạo head và ba trường .next còn lại. Với một ít thông minh và sự hiểu biết về ngôn ngữ C, bạn có thể hoàn thành chúng với 7 phép gán (=).

Hàm Length() lấy số phần tử của danh sách Hàm Length() sẽ nhận một danh sách liên kết và tính toán số phần tử của danh sách. Hàm Length() là một hàm

danh sách đơn giản nhưng nó giải thích nhiều khái niệm mà sẽ dùng sau này , trong những hàm danh sách phức tạp hơn.

Có hai đặc trưng của danh sách liên kết được mô tả trong hàm Length () :

1) Truyền một danh sách bằng cách truyền con trỏ Head Một danh sách liên kết được truyền vào trong hàm Length () thông qua một con trỏ Head. Con trỏ được truyền tham trị vào hàm Length(). Việc sao chép con trỏ này thì không nhân đôi cả danh sách. Nó chỉ sao chép con trỏ để hàm gọi và hàm Length() sẽ có những con trỏ trỏ tới cùng một cấu trúc dữ liệu.

2) Lặp đi lặp lại danh sách với một con trỏ cục bộ Mã nguồn trên lặp đi lặp lại qua tất cả các phần tử.

struct node* current = head; while (current != NULL) { // do something with *current node current = current->next; }

Điểm đáng chú ý của đoạn mã trên là:

1) Con trỏ cục bộ - current trong trường hợp này – bắt đầu với việc trỏ tới cùng node như con trỏ head với current = head . Khi hàm này được thoát ra, con trỏ current sẽ tự động giải phóng do nó chỉ là cục bộ nhưng các nodes ở trong heap thì vẫn giữ nguyên

/* Given a linked list head pointer, compute and return the number of nodes in the list. */ int Length(struct node* head) {

struct node* current = head; int count = 0; while (current != NULL) {

count++; current = current->next;

} return count;

}

Page 7: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

7

2) Vòng lặp while kiểm tra liệu đã tới cuối danh sách hay chưa (current != NULL). Cách kiểm tra này cũng diễn ra tốt cho trường hợp danh sách rỗng – current sẽ là NULL trong vòng lặp đầu tiên và vòng lặp while sẽ thoát ra.

3) Ở cuối cùng của vòng lặp while: current = current->next; sẽ tăng con trỏ cục bộ tới node tiếp theo trong danh sách. Khi mà không còn liên kết nào nữa, con trỏ khi đó bằng NULL.

Gọi hàm Length() Dưới đây là một đoạn code tiêu biểu để gọi hàm Length(). Đầu tiên nó sẽ gọi hàm BuildOneTwoThree() để tạo ra

một danh sách và lưu con trỏ head trong một biến cục bộ. Sau đó sẽ gọi hàm Length() với danh sách và lưu giá trị trả về qua một biến int.

Vẽ mô phỏng bộ nhớ

Các tốt nhất để thiết kế và suy nghĩ về một danh sách liên kết là dùng một hình vẽ để thấy cách con trỏ hoạt động trong bộ nhớ. Những hình vẽ dưới đây sẽ thể hiện trạng thái của bộ nhớ trước và trong quá trình gọi hàm Length() – hãy tận dụng cơ hội này để tập xem hình vẽ bộ nhớ và dùng chúng để suy nghĩ về các mã nguồn dùng con trỏ. Bạn sẽ có khả năng hiểu nhiều hơn sau này.

Bắt đầu với mã nguồn Length() và LengthTest() và một trang giấy trắng. Hãy theo dấu các hoạt động của code và cập nhật bức ảnh của bạn để xem trạng thái bộ nhớ cho mỗi bước. Các hình vẽ bộ nhớ sẽ phân biệc rõ vùng nhớ heap và stack. Hãy lưu ý là: malloc() cấp phát vùng nhớ trong heap và chỉ bị giải phóng bằng cách gọi hàm free(). Ngược lại, các biến cục bộ stack cho mỗi hàm thì tự động cấp phát khi vào hàm và giải phóng khi ra khỏi hàm.

void LengthTest() { struct node* myList = BuildOneTwoThree(); int len = Length(myList); // results in len == 3

}

Page 8: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

8

Hình 1: trước khi gọi hàm Length()

Ở dưới là trạng thái của bộ nhớ trước khi gọi hàm Length() trong hàm LengthTest() ở trên. Hàm BuildOneTwoThree() xây dựng một danh sách {1 , 2, 3} trong heap và trả về một con trỏ head. Con trỏ head được lưu trong một biến cục bộ myList. Biến cục bộ len có giá trị ngẫu nhiên – nó chỉ được gán bằng 3 khi hàm Length() trả về.

Hình 2: Ở giữa hàm Length()

Đây là trạng thái của bộ nhớ ở giữa quá trình chạy hàm Length(). Biến head và current cục bộ của hàm Length() tự động được cấp phát. Con trỏ curretn bắt đầu chỉ vào node đầu tiên và sau đó vòng lặp đầu tiên của while đã tăng nó để trỏ tới node thứ hai.

Chú ý các biến cục bộ trong hàm Length( head và current) thì được tách biệt ra so với các biến cục bộ trong hàm LengthTest() ( myList và len). Các biến cục bộ của hàm Length sẽ được giải phóng tự động khi ra khỏi hàm. Danh sách liên kết được cấp phát trên heap vẫn sẽ tồn tại mặc dù con trỏ trỏ tới nó bị xóa.

Page 9: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

9

Bài tập:

Hỏi: Chuyện gì nếu chúng ta gọi lệnh head = NULL ở cuối hàm Length() – Liệu nó sẽ làm danh sách myList của chúng ta rối loạn lên ?

Trả lời: Không, head là một biến cục bộ của hàm Length() nên thay đổi nó sẽ không làm ảnh hưởng tới biến thật sự trong hàm gọi.

Hỏi: Nếu chúng ta truyền vào danh sách không có phần tử nào, liệu Length() có thể xử lý đúng được không?

Trả lời: Có. Đại diện của một danh sách rỗng là một con trỏ head NULL. Hãy xem lại hàm Length() để xem cách hàm xử lý trường hợp này.

Phần 2 –Xây dựng danh sách liên kết BuildOneTwoThree() là một ví dụ tốt về các thao tác con trỏ nhưng nó không phải là kỹ thuật thông thường để

xay dựng danh sách. Giải pháp tốt nhất sẽ là một hàm tự do để thêm một node mới vào bất kì danh sách nào. Chúng ta có thể gọi hàm đó bao nhiêu lần tùy thích để xây dựng một danh sách. Trước khi đi vào mã nguồn cụ thể, chúng ta có thể nhận thấy 3 bước để thêm một node vào trước một danh sách liên kết. Ba bước đó là:

1) Cấp phát (Allocate): Cấp phát một node mới trong heap và gán giá trị cho phần tử .data.

2) Tạo liên kết next: Gán con trỏ .next của node mới để trỏ tới node đầu tiên của danh sách. Đây thực tế chỉ là

một phép gán con trỏ - nhớ rằng : “ gán một con trỏ tới một cái khác chỉ làm chúng trỏ tới cùng một nơi”

3) Tạo liên kết Head: Thay đổi con trỏ head để trỏ tới node mới để nó bây giờ trở node đầu tiên của danh sách

Ba bước trên trong mã nguồn:

struct node* newNode; newNode = malloc(sizeof(struct node)); newNode->data = data_client_wants_stored;

newNode->next = head;

head = newNode;

void LinkTest() { struct node* head = BuildTwoThree(); // suppose this builds the {2, 3} list struct node* newNode; newNode= malloc(sizeof(struct node)); // allocate newNode->data = 1; newNode->next = head; // link next head = newNode; // link head // now head points to the list {1, 2, 3} }

Page 10: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

10

Ba bước trong hình vẽ:

Dưới đây là hình vẽ minh họa 3 bước trên ( con trỏ bị ghi đè được thể hiện màu xám)

Hàm Push() Có vẻ từ 3 bước trên thì việc viết hàm Push() để thêm một node vào đầu danh sách rất dễ dàng. Hãy xem hàm sau và tìm chỗ sai trước khi xem lời giải.

WrongPush()

Không may rằng, hàm Push() trong C gặp phải một vấn đề cơ bản: đối số cho hàm Push() là gì?. Đây là một vấn đề rắc rối trong C. Dưới đây là một cách dễ dàng và rõ ràng để viết hàm Push() , nó nhìn khá đúng nhưng thật sự thì sai. Việc tìm ra chỗ sai sẽ giúp ích trong việc thực hành vẽ bộ nhớ và giúp bạn trở thành lập trình viên tốt hơn.

void WrongPush(struct node* head, int data) { struct node* newNode = malloc(sizeof(struct node)); newNode->data = data; newNode->next = head; head = newNode; // NO this line does not work! }

void WrongPushTest() { List head = BuildTwoThree(); WrongPush(head, 1); // try to push a 1 on front -- doesn't work }

Page 11: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

11

WrongPush() thì gần đúng. Nó theo đúng như 3 bước trên. Vấn đề là ở dòng cuối cùng của hàm. Nó ra lệnh để chúng ta thay đổi con trỏ head để trỏ tới một node mới. Thật sự thì dòng head = newNode; làm được gì trong WrongPush() ? Nó gán giá trị tới con trỏ head, nhưng không phải là con trỏ head đúng. Nó gán giá trị cho một biến cục bộ head ở trong hàm WrongPush(). Điều đó không thay đổi biến head mà chúng ta thật sự quan tâm ở trong hàm gọi.

Bài tập:

Hãy vẽ mô phỏng bộ nhớ trong WrongPushTest() để thấy lí do nó không hoạt động. Hãy nhớ rằng các biến cục bộ của hàm WrongPushTest() và hàm WrongPush() thì được tách riêng ra trong bộ nhớ.

Truyền tham chiếu trong C

Chúng ta đang gặp phải một vấn đề cơ bản trong ngôn ngữ C rằng: sự thay đổi với biến cục bộ thì không được truyền ngược về hàm gọi. Chúng tôi sẽ đưa ra giải pháp truyền thống cho vấn đề này, nhưng bạn có thể muốn tham khảo các tài liệu C khác để biết sâu hơn.

Chúng ta cần Push() có khả năng thay đổi giá trị trong bộ nhớ của hàm gọi - ở đây là biến head. Cách truyền thống để cho phép điều này là truyền một con trỏ tới hàm Push() thay vì một bản sao. Trong trường hợp này, giá trị mà chúng ta muốn thay đổi là struct node*, chúng ta sẽ thay bằng struct node **. Hai dấu sao ( ** ) thì hơi đáng sợ nhưng thật sự nó chỉ là một cách áp dụng trực tiếp của nguyên tắc trên. Do chúng ta giá trị chúng ta truyền vào đã có sẵn một dấu sao ( * ) nên đối số phải có 2 dấu *. Thay vì khai báo WrongPush(struct node* head, int data); chúng ta khai báo Push(struct node** headRef, int data);. Hãy nhớ nguyên tắc là: để thay đổi giá trị bộ nhớ của hàm gọi, hãy truyền một con trỏ trỏ tới bộ nhớ đó. Đối số có một từ Ref để nhắc nhỏ rằng đây là một con trỏ tham chiếu tới con trỏ head ban đầu.

Mã nguồn Push() chính xác

Dưới đây là hàm Push() và PushTest() hoạt động đúng. Danh sách được truyền vào thông qua một con trỏ tới con trỏ head. Trong mã nguồn này, chính là việc dùng “&” ở trong hàm gọi và cách dùng “*” ở trong hàm con.

/* Takes a list and a data value. Creates a new link with the given data and pushes it onto the front of the list. The list is not passed in by its head pointer. Instead the list is passed in as a "reference" pointer to the head pointer -- this allows us to modify the caller's memory. */ void Push(struct node** headRef, int data) { struct node* newNode = malloc(sizeof(struct node)); newNode->data = data; newNode->next = *headRef; // The '*' to dereferences back to the real head *headRef = newNode; // ditto } void PushTest() { struct node* head = BuildTwoThree();// suppose this returns the list {2, 3} Push(&head, 1); // note the & Push(&head, 13); // head is now the list {13, 1, 2, 3} }

Page 12: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

12

Hình vẽ mô phỏng bộ nhớ chính xác:

Dưới đây là hình vẽ mô phỏng bộ nhớ ngay trước khi hàm Push() thoát ra. Giá trị gốc của con trỏ head thì có màu xám. Chú ý cách mà đối số headRef trong Push() trỏ ngược lại con trỏ thật ở trong PushTest(). Push() dùng *headRef để truy xuất và thay đổi giá trị của con trỏ thật.

Bài tập:

Hình vẽ trên mô phỏng bộ nhớ trong lần gọi đầu tiên của hàm Push() trong PushTest(). Hãy mở rộng hình vẽ để theo dấu tới lần gọi thứ hai của hàm Push(). Kết quả của danh sách khi đó sẽ phải là {13, 1, 2 ,3 }.

Bài tập:

Hàm sau xây dựng một danh sách ba phần tử chỉ dùng hàm Push(). Hãy vẽ hình vẽ mô phỏng bộ nhớ để theo vết các hoạt động của nó và chỉ ra trạng thái cuối cùng của danh sách. Điều này cũng chứng minh rằng Push() hoạt động tốt với danh sách rỗng.

void PushTest2() { struct node* head = NULL; // make a list with no elements Push(&head, 1); Push(&head, 2); Push(&head, 3); // head now points to the list {3, 2, 1} }

Page 13: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

13

Trong C++ thì sao? Với C++ thì có hỗ trợ truyền tham chiếu (reference &) vào hàm, nên hàm Push có thể viết như sau với C++

/* Push() in C++ -- we just add a '&' to the right hand side of the head parameter type, and the compiler makes that parameter work by reference. So this code changes the caller's memory, but no extra uses of '*' are necessary -- we just access "head" directly, and the compiler makes that change reference back to the caller. */ void Push(struct node*& head, int data) { struct node* newNode = malloc(sizeof(struct node)); newNode->data = data; newNode->next = head; // No extra use of * necessary on head -- the compiler head = newNode; // just takes care of it behind the scenes. } void PushTest() { struct node* head = BuildTwoThree();// suppose this returns the list {2, 3} Push(head, 1); // No extra use & necessary -- the compiler takes Push(head, 13); // care of it here too. Head is being changed by

// these calls. // head is now the list {13, 1, 2, 3}

}

Page 14: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

14

Phần 3: Các kỹ thuật trong lập trình Phần này sẽ tóm tắt các kỹ thuật chính cho danh sách liên kết. Các kỹ thuật này sẽ được minh họa trong các ví dụ cụ thể ở phần tiếp theo.

Đây là một kỹ thuật rất thường xuyên sử dụng trong danh sách liên kết để duyệt qua các node trong danh sách thông qua con trỏ. Theo truyền thống, cách này được viết như một vòng lặp while. Con trỏ head thì được sao chép trong một biến cục bộ current để duyệt xuống danh sách. Kiểm tra hết danh sách với current != NULL. Tiến con trỏ tới node tiếp theo bằng current = current ->next.

Có thể thay vòng lặp bằng

for (current = head; current != NULL; current = current->next) {

Nhiều hàm cần phải thay đổi con trỏ head mặc dù nó được truyền vào hàm số theo kiểu pass-by-value. Để làm điều này trong C thì hãy truyền một pointer đến head pointer thay vì chỉ truyền head pointer. Kiểu pointer chỉ đến một pointer khác thường được gọi là reference pointer.

Hãy xem hàm ChangeToNull để cho con trỏ head trỏ vào NULL

// Trả về số lượng node của danh sách int Length(struct node* head) { int count = 0; struct node* current = head; while (current != NULL) { count++; current = current->next } return(count); }

// Change the passed in head pointer to be NULL // Uses a reference pointer to access the caller's memory void ChangeToNull(struct node** headRef) { // Takes a pointer to the value of interest *headRef = NULL; // use '*' to access the value of interest }

Page 15: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

15

Đây là hình minh họa cách con trỏ headRef trong ChangeToNull() trỏ ngược về biến trong hàm gọi

Xem cách dùng hàm Push() ở trên như một ví dụ cho kỹ thuật này.

Cách dễ nhất để xây dưng một danh sách là thêm nodes vào đầu với Push(). Đoạn mã nguồn thì ngắn và chạy nhanh. Bất lợi là các phần tử sẽ xuất hiện trong danh sách với thứ tự ngược lại khi bạn thêm vào. Nếu bạn không quan tâm thứ tự, cách thêm vào đầu là cách tốt nhất.

Nếu ta muốn thêm nodes vào cuối danh sách thì phải làm thế nào? Thêm node vào cuối danh sách thường bao gồm việc xác định vị trí node cuối trong danh sách và thay đổi giá trị .next của nó từ NULL sang trỏ tới node mới

Hình sau mô tả cách thêm node 3 vào cuối danh sách {1, 2}...

void ChangeCaller() { struct node* head1; struct node* head2; ChangeToNull(&head1); // use '&' to compute and pass a pointer to ChangeToNull(&head2); // the value of interest // head1 and head2 are NULL at this point }

struct node* AddAtHead() { struct node* head = NULL; int i; for (i=1; i<6; i++) { Push(&head, i); } // head == {5, 4, 3, 2, 1}; return(head); }

Page 16: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

16

Tuy nhiên thì có một trường hợp đặc biệt khi phần tử mới thêm vào danh sách là phần tử đầu tiên của list. Hãy xem xét tiếp các kỹ thuật sau.

Xem xét vấn đề trong khi xây dựng một danh sách {1, 2 ,3, 4 ,5} bằng việc thêm các node vào cuối. Khó khăn là node đầu tiên phải được thêm vào con trỏ head nhưng các node khác thì được thêm vào sau bằng cách sử dụng con trỏ tail. Cách đơn giản để giải quyết cả 2 trường hợp là tách riêng 2 trường hợp trong mã nguồn. Đoạn mã nguồn đặc biệt đầu tiên thêm node head {1}. Sau đó làm một vòng lặp riêng dùng con trỏ tail để thêm tất cả các node còn lại. Con trỏ tail vẫn tiếp tục trỏ tới node cuối cùng và mỗi node thì được thêm vào ở tail->next. Vấn đề duy nhất của giải pháp này là việc viết riêng biệt mã nguồn cho node đầu tiên. Dù sao đi nữa, cách tiếp cận này là một cách cơ sở cho việc tạo mã nguồn – nó nhanh và đơn giản.

newNode->next = NULL; tail->next = newNode; tail = tail->next;

struct node* BuildWithSpecialCase() { struct node* head = NULL; struct node* tail; int i; // Deal with the head node here, and set the tail pointer Push(&head, 1); tail = head; // Do all the other nodes using 'tail' for (i=2; i<6; i++) { Push(&(tail->next), i); // add node at tail->next tail = tail->next; // advance tail to point to last node } return(head); // head == {1, 2, 3, 4, 5}; }

Page 17: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

17

Một giải pháp khác là dùng một node dummy tạm ở đầu của danh sách suốt quá trình tính toán. Dummy node sẽ đóng vai trò là phần tử đầu tiên và tất cả các node “thực sự” sẽ được thêm vào sau dummy node.

Mội vài cách thể hiện danh sách liên kết giữ dummy node như là một phần vĩnh cửu của danh sách. Đối với chiến lược “dummy vĩnh cửu” này, một danh sách rỗng thì không phải thể hiện bởi NULL mà thay vào đó là danh sách có một dummy node ở đầu. Các thuật toán sẽ thông qua dummy node cho tất cả các phép tính. Nhưng tôi không khuyến khích sử dụng kỹ thuật này.

Chiến lược “dummy-in-the stack” ở trên thì có một chút khác biệt, nhưng nó tránh được việc tạo ra một thành phần dummy vĩnh cữu trong danh sách. Vài ví dụ trong tài liệu này sẽ dùng cách “dummy tạm thời”này. Mã nguồn của chiến lược “dummy vĩnh cữu” thì cực kì đơn giản nhưng nó không được đưa ra ở đây.

Cuối cùng đây là một giải pháp rất “mẹo mực” mà không phải sự dụng dummy node. Đó là sử dụng một local “reference pointer” mà luôn trỏ vào pointer cuối cùng của list chứ không phải node cuối cùng. Tất cả việc thêm vào danh sách thì được làm thông qua một con trỏ tham chiếu. Con trỏ tham chiếu bằng đầu bằng việc trỏ vào con trỏ head. Sau đó, nó trỏ tới trường .next bên trong node cuối cùng của danh sách. (giải thích chi tiết ở sau)

struct node* BuildWithDummyNode() { struct node dummy; // Dummy node is temporarily the first node struct node* tail = &dummy; // Start the tail at the dummy. // Build the list on dummy.next (aka tail->next) int i; dummy.next = NULL; for (i=1; i<6; i++) { Push(&(tail->next), i); tail = tail->next; } // The real result list is now in dummy.next // dummy.next == {1, 2, 3, 4, 5}; return(dummy.next); }

Page 18: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

18

Kĩ thuật này thì ngắn nhưng vòng lặp bên trong thì đáng sợ. Kĩ thuật này hiếm khi được sử dụng. Dưới đây là cách nó hoạt động:

1) Ở đầu của vòng lặp, lastPtrRef trỏ tới con trỏ cuối cùng của danh sách. Ban đầu, nó trỏ tới con trỏ head. Sau đó nó trỏ tới trường .next trong node cuối cùng của danh sách.

2) Push(lastPtrRef, i); thêm một node mới tại con trỏ cuối. Node mới trở thành node cuối của danh sách. 3) lastPtrRef= &((*lastPtrRef)->next); đẩy lastPtrRef để trỏ tới trường .next của node cuối mới.

Dưới đây là hình vẽ mô tả trạng thái của bộ nhớ cho đoạn mã nguồn trên trước khi node thứ ba được thêm vào. Giá trị cũ của lastPtrRef thì có màu xám…

Cả hai giải pháp sử dụng temporary-dummy và reference pointer có hơi chút “không bình thường” nhưng nó rất tốt để chắc chắn rằng chúng ta hiểu về pointer bởi vì chúng sử dụng pointer theo cách “không bình thường”.

struct node* BuildWithLocalRef() { struct node* head = NULL; struct node** lastPtrRef= &head; // Start out pointing to the head pointer int i; for (i=1; i<6; i++) { Push(lastPtrRef, i); // Add node at the last pointer in the list lastPtrRef= &((*lastPtrRef)->next); // Advance to point to the // new last pointer } // head == {1, 2, 3, 4, 5}; return(head); }

Page 19: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

19

Phần 4: Các ví dụ tiêu biểu Phần này giới thiệu các ví dụ để minh họa cho các kĩ thuật trên. Để biết thêm nhiều ví dụ hơn, các bạn hãy xem phần tiếp theo của tài liệu này trong “ Các vấn đề về danh sách liên kết” – nhóm sẽ đưa tới các bạn trong thời gian sớm nhất.

AppendNode()

Hãy nghĩ một hàm AppendNode() thì giống như Push(), ngoại trừ nó thêm vào node mới ở cuối danh sách thay vì đầu danh sách. Nếu nó là một danh sách rỗng, nó sẽ dùng con trỏ tham chiếu để thay đổi con trỏ head. Mặc dù nó dùng một vòng lặp để xác định vị trí của node cuối cùng trong danh sách. Phiên bản này không sử dụng Push(). Nó xây dựng node mới một cách trực tiếp

AppendNode() với Push()

Phiên bản này thì rất đơn giản, nhưng nó phụ thuộc vào Push() để xây dựng một node mới. Để hiểu phiên bản này, cần phải hiểu được về con trỏ tham chiếu

struct node* AppendNode(struct node** headRef, int num) { struct node* current = *headRef; struct node* newNode; newNode = malloc(sizeof(struct node)); newNode->data = num; newNode->next = NULL; // special case for length 0 if (current == NULL) { *headRef = newNode; } else { // Locate the last node while (current->next != NULL) { current = current->next; } current->next = newNode; } }

Page 20: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

20

Hãy nghĩ một hàm CopyList() là nhận một danh sách và trả về một bản copy của danh sách đó. Một con trỏ có thể chạy qua suốt danh sách gốc bằng cách thông thường. Hai con trỏ khác sẽ theo vết của danh sách mới: một con trỏ head và một con trỏ tail - cái mà luôn luôn trỏ tới node cuối của danh sách mới. Node đầu tiên thì được hoàn thành như một trường hợp đặc biệt và sau đó con trỏ tail dùng các cách thông thường cho các node còn lại.

struct node* AppendNode(struct node** headRef, int num) { struct node* current = *headRef; // special case for the empty list if (current == NULL) { Push(headRef, num); } else { // Locate the last node while (current->next != NULL) { current = current->next; } // Build the node after the last node Push(&(current->next), num); } }

struct node* CopyList(struct node* head) { struct node* current = head; // used to iterate over the original list struct node* newList = NULL; // head of the new list struct node* tail = NULL; // kept pointing to the last node in the new list while (current != NULL) { if (newList == NULL) { // special case for the first new node newList = malloc(sizeof(struct node)); newList->data = current->data; newList->next = NULL; tail = newList; } else { tail->next = malloc(sizeof(struct node)); tail = tail->next; tail->data = current->data; tail->next = NULL; } current = current->next; } return(newList); }

Page 21: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

21

Hình vẽ mô phỏng hàm CopyList()

Dưới đây là trạng thái của bộ nhớ khi CopyList() hoàn thành việc sao chép danh sách {1,2}

Bài tập CopyList() với Push()

Cách biểu diễn ở trên thì không thỏa mãn vì 3 bước tạo node bị lặp lại trong hàm. Viết một CopyList2() dùng Push() để xử lý việc cấp phát và thêm node mới và tránh lặp lại mã nguồn.

Đáp án:

// Variant of CopyList() that uses Push() struct node* CopyList2(struct node* head) { struct node* current = head; // used to iterate over the original list struct node* newList = NULL; // head of the new list struct node* tail = NULL; // kept pointing to the last node in the new list while (current != NULL) { if (newList == NULL) { // special case for the first new node Push(&newList, current->data); tail = newList; } else { Push(&(tail->next), current->data); // add each node at the tail tail = tail->next; // advance the tail to the new last node } current = current->next; } return(newList); }

Page 22: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

22

CopyList() với Dummy Node

Một chiến lược khác cho CopyList() là dùng một dummy node tạm thời để xử lý trường hợp node đầu tiên. Dummy node là node đầu tiên tạm thời của danh sách và con trỏ tail bắt đầu bằng việc trỏ tới nó. Tất cả các node được thêm vào con trỏ tail

CopyList() với sử dụng tham chiếu cục bộ (local references)

Phiên bản cuối cùng và ít sử dụng nhất của tham chiếu cục bộ thay cho con trỏ tail. Chiến lược là dùng một con trỏ lastPtr để trỏ tới con trỏ cuối cùng của danh sách. Tất cả các việc thêm node thì hoàn thành qua lastPtr và nó luôn trỏ tới con trỏ cuối cùng của danh sách. Khi danh sách rỗng, nó trỏ tới con trỏ head. Sau đó nó trỏ tới trường .next trong node cuối cùng.

// Dummy node variant struct node* CopyList(struct node* head) { struct node* current = head; // used to iterate over the original list struct node* tail; // kept pointing to the last node in the new list struct node dummy; // build the new list off this dummy node dummy.next = NULL; tail = &dummy; // start the tail pointing at the dummy while (current != NULL) { Push(&(tail->next), current->data); // add each node at the tail tail = tail->next; // advance the tail to the new last node } current = current->next; } return(dummy.next); }

// Local reference variant struct node* CopyList(struct node* head) { struct node* current = head; // used to iterate over the original list struct node* newList = NULL; struct node** lastPtr; lastPtr = &newList; // start off pointing to the head itself while (current != NULL) { Push(lastPtr, current->data); // add each node at the lastPtr lastPtr = &((*lastPtr)->next); // advance lastPtr current = current->next; } return(newList); }

Page 23: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

23

Đệ quy với CopyList()

Và cuối cùng, là một phiên bản đệ quy của CopyList(). Nó có tính chất ngắn gọn dễ chịu mà các mã nguồn đệ quy thường có. Tuy nhiên nó thì không tốt cho việc chế tạo mã nguồn vì nó dùng không gian stack tỉ lệ với chiều dài của danh sách.

Có rất nhiều biến thể của danh sách liên kết cơ bản mà có thế mạnh riêng đối với danh sách cơ bản. Tốt nhất là nên có một sự thấu hiểu nhất định với danh sách liên kết cơ bản và mã nguồn của nó trước khi lo lắng về các biến thể quá nhiều.

Dummy Header : Bỏ qua trường hợp mà con trỏ head là NULL. Thay vào đó, sử dụng một node “dummy” mà có trường .data không sử dụng. Thuận lợi của kĩ thuật này là trường hợp dùng pointer-to-pointer (đối số tham chiếu) không xảy ra trong tính toán như Push(). Và một vài phép toán khác sẽ đơn giản hơn do chúng ta thừa nhận sự có mặt của node dummy ở đầu. Bất lợi là việc cấp phát một danh sách rỗng thì lãng phí bộ nhớ. Vài thuật toán thì bị xấu đi do nó phải bỏ qua trường hợp dummy node ở đầu. Cách dùng dummy header là để cho lập trình viên tránh sử dụng đối số tham chiếu như trong hàm Push(). Các ngôn ngữ mà không cho phép đối số tham chiếu nhưu Java thì cần dùng dummy header để làm việc.

Circular ( Vòng): Thay vì thiết lập trường .next của node cuối thành NULL, thì gán nó để trỏ tới node đầu tiên. Thay vì cần một đầu head cố định, bất cứ con trỏ nào trong danh sách đểu có thể làm việc đó.

Con trỏ Tail : Danh sách thì không chỉ thể hiện bởi một con trỏ head duy nhất. Thay vào đó, danh sách có con trỏ head trỏ tới node đầu tiên và con trỏ tail trỏ tới node cuối cùng. Con trỏ tail cho phép các phép toán ở cuối danh sách như thêm phần tử cuối và ghép hai danh sách.

Head struct : Một biến thể có một struct header đặc biệt cái mà chứa con trỏ head, con trỏ tail và có thể là độ dài để giúp các thao tác trở nên dễ dàng. Nhiều vấn đề của đối số tham chiếu được loại bỏ đi do hầu hết các hàm có thể xử lý với con trỏ tới một struct head. Đây là một cách tiếp cận tốt nhất để dùng trong các ngôn ngữ không sử dụng đối số tham chiếu

Liên kết kép: Thay vì chỉ có một trường .next, mỗi node có cả hai con trỏ .next và .previous. Việc thêm hay xóa bây giờ cần nhiều thao tác hơn nhưng những thao tác thì đơn giản hơn. Đưa một con trỏ tới node, việc thêm và xóa có thể được thực hiện trực tiếp. Trong khi trong liên kết đơn, việc lặp lại cần thực hiện để xác định vị trí trước vị trí cần thay đổi trong danh sách để con trỏ .next có thể nối ra sau.

// Recursive variant struct node* CopyList(struct node* head) { if (head == NULL) return NULL; else { struct node* newList = malloc(sizeof(struct node)); // make the one node newList->data = current->data; newList->next = CopyList(current->next); // recur for the rest return(newList); } }

Page 24: [Huong Dan Tieng Viet] Co Ban Ve Danh Sach Lien Ket

More information: www.itspiritclub.net Linked list basics Translater: huahongquan2007

24

Danh sách “khúc” (Chunk list) : thay vì lưu trữ các phần tử đơn trong mỗi node, lưu tữ một mảng kích cỡ xác định cho mỗi phần tử trong một node. Thay đổi số phần tử của mỗi node có thể cung cấp một đặc trưng khác: nhiều phần tử/ node có đặc tính giống như mảng, ít phần tử/node thì đặc tính giống như danh sách liên kết. Chunk List là một cách tốt để xây dựng danh sách liên kết với hiệu suất cao.

Mảng động: Thay vì dùng danh sáhc liên kết, các phần tử có thể chứa trong một khối mảng (array block) được cấp phát ở trên heap . Nó có thể lớn lên hay giảm kích thước của khối khi cần thiết với hàm realloc(). Quản lý khối heap theo cách này thì khá là phức tạp, nhưng có thể là một cách hiệu quả nhất cho việc lưu trữ và truy xuất. Ngược lại, danh sách liên kết có thể có một ít bất tiện, do nó có khuynh hướng lặp lại qua các vòng nhớ mà không liền kề

Phần tiếp theo :”Các vấn đề với danh sách liên kết” sẽ được đưa tới các bạn trong thời gian sớm nhất.

Nếu các bạn có ý kiến đóng góp hoặc muốn tham gia dịch tài liệu với chúng tôi, các bạn hãy liên hệ nhóm itspirit qua ym: whereareyou_sweetie