You are on page 1of 30

More information: www.itspiritclub.

net Translater: huahongquan2007

Đối với vài vấn đề chúng tôi đưa ra nhiều giải pháp, như là vòng lặp và đệ quy, dummy node và tham chiếu cục
bộ. Những vấn đề được trình bày theo thứ tự từ dễ tới khó: Count, GetNth, DeleteList, Pop, InsertNth, SortedInsert,
InsertSort, Append, FrontBackSplit, RemoveDuplicates, MoveNode, AlternatingSplit, ShuffleMerge, SortedMerge,
SortedIntersect, Reverse, and RecursiveReverse.

Nội dung:

Phần 1 – Ôn lại về danh sách liên kết

Phần 2 – 18 vấn đề theo thứ tự từ dễ tới khó

Phần 3 – Giải pháp tới tất cả vấn đề

1
More information: www.itspiritclub.net Translater: huahongquan2007

Phần 1 – Xem lại các kĩ thuật cơ bản về DSLK


Phần này sẽ ôn lại nhanh về các khái niệm được dùng trong danh sách liên kết. Để biết chi tiết hơn, hãy xem tài
liệu “Cơ bản về danh sách liên kết” ( http://itspiritclub.net ).

Các nguyên tắc cơ bản của danh sách liên kết

Tất cả các mã nguồn về danh sách liên kết trong tài liệu này dùng cấu trúc danh sách liên kết đơn thuần túy: chỉ
dùng một con trỏ head để trỏ tới node đầu tiên của danh sách. Mỗi node chứa một con trỏ .next để trỏ tới node tiếp theo.
Con trỏ .next cuối cùng là NULL. Danh sách rỗng thì biểu diễn bằng một con trỏ head NULL. Tất cả các node thì được
cấp phát ở trên heap.

struct node {

int data;

struct node* next;

};

Các hàm tiện ích cơ bản

Ở vài chỗ, chúng ta thừa nhận sự có mặt của các hàm tiện ích sau:

 int Length(struct node* head);


Trả về số lượng node của danh sách
 struct node* BuildOneTwoThree();
Cấp phát và trả về một danh sách {1, 2, 3}.
 void Push(struct node** headRef, int newData);
Đưa một số int và một tham chiếu tới con trỏ head, thêm một node ở đầu của danh sách với 3 bước cơ
bản.

Cách dùng các hàm tiện ích cơ bản:

Đoạn mã nguồn dưới đây mô tả cách sử dụng hàm tiện ích cơ bản.

void BasicsCaller() {
struct node* head;
int len;
head = BuildOneTwoThree(); // Start with {1, 2, 3}
Push(&head, 13); // Push 13 on the front, yielding {13, 1, 2, 3}
// (The '&' is because head is passed
// as a reference pointer.)
Push(&(head->next), 42); // Push 42 into the second position
// yielding {13, 42, 1, 2, 3}
// Demonstrates a use of '&' on
// the .next field of a node.
// (See technique #2 below.)
len = Length(head); // Computes that the length is 5.
}

2
More information: www.itspiritclub.net Translater: huahongquan2007

# Các phần còn lại của phần 1 giống hoàn toàn tài liệu trước

3
More information: www.itspiritclub.net Translater: huahongquan2007

Phần 2 – Các vấn đề với DSLK


Đây là 18 vấn đề về danh sách liên kết được sắp xếp theo thứ tự độ khó tăng dần. Vài vấn đề đầu thì khá là cơ
bản, nhưng vấn đề cuối thì nâng cao hơn. Mỗi vấn đề bắt đầu với một định nghĩa cơ bản về điều gì cần phải hoàn thành.
Nhiều vấn đề cũng bao hàm các gợi ý hoặc hình vẽ để bạn nắm bắt được. Giải pháp cho tất cả các vấn đề thì được trình
bày trong phần tiếp theo.

Để các vấn đề này giúp ích được cho bạn, bạn cần phải cố gắng suy nghĩ về chúng. Dù bạn có giải được vấn đề
hay không, bạn sẽ vẫn có suy nghĩ về đúng vấn đề và giải pháp đưa ra sẽ làm rõ vấn đề hơn.

Những lập trình viên tốt có thể hình dung ra cấu trúc dữ liệu để thấy mã nguồn và bộ nhớ sẽ tương tác như thế
nào. Hãy dùng những vấn đề để phát triển kĩ năng tưởng tượng của bạn. Hãy vẽ những hình mô phỏng quá trình hoạt động
của mã nguồn. Dùng các hình vẽ để chuẩn bị cho việc hình dung ra giải pháp.

Viết một hàm Count() để đếm số lần xuất hiện của một biến int được đưa ra trong sách.

void CountTest() {
List myList = BuildOneTwoThree(); // build {1, 2, 3}
int count = Count(myList, 2); // returns 1 since there's 1 '2' in the list
}
/*
Given a list and an int, return the number of times that int occurs
in the list.
*/
int Count(struct node* head, int searchFor) {
// Your code

Viết một hàm GetNth() để nhận một danh sách liên kết và một chỉ số index nguyên và trả về giá
trị dữ liệu lưu trong node ở vị trí chỉ số. GetNth() dùng quy ước đánh số trong C : node đầu có index 0,
thứ hai là index 1….Ví dụ với danh sách {42, 13, 666} GetNth() với index 1 sẽ trả về 13. Index sẽ có
giá trị trong khoảng range [0..length-1]. Nếu nó không phải vậy, GetNth() sẽ thất bại.

void GetNthTest() {
struct node* myList = BuildOneTwoThree(); // build {1, 2, 3}
int lastNode = GetNth(myList, 2); // returns the value 3
}

Về cơ bản, GetNth() thì tương tự như array[i] . Tuy nhiên, GetNth() thì xử lý chậm hơn cú pháp [ ] của mảng.
Thuận lợi của danh sách liên kết là nó quản lý bộ nhớ linh động hơn – chúng ta có thể Push() bất cứ lúc nào để thêm một
phần tử khi cần.

4
More information: www.itspiritclub.net Translater: huahongquan2007

Viết một hàm DeleteList() để nhận một danh sách và giải phóng tất cả bộ nhớ của nó và đặt con trỏ head về
NULL.

void DeleteListTest() {
struct node* myList = BuildOneTwoThree(); // build {1, 2, 3}
DeleteList(&myList); // deletes the three nodes and sets myList to NULL
}

Hình vẽ mô phỏng bộ nhớ DeleteList()

Hình vẽ sau đây sẽ mô phỏng trạng thái của bộ nhớ sau khi DeleteList() hoạt động trong ví dụ trên. Con trỏ được
ghi đè sẽ hiện lên trong màu xám và bộ nhớ được giải phóng có một dấu X trên nó. Về cơ bản DeleteList() cần gọi hàm
free() mỗi lần cho mỗi node và đặt con trỏ head về NULL.

DeleteList() cần sử dụng đối số tham chiếu giống như Push() để nó có thể thay đổi bộ nhớ của hàm gọi. Hàm cũng cần
phải cẩn thận không truy cập vào trường .next của một node sau khi node bị giải phóng.

void DeleteList(struct node** headRef) {

// Your code

5
More information: www.itspiritclub.net Translater: huahongquan2007

Viết một hàm Pop() mà nó ngược lại với Push(). Pop() nhận một danh sách không rỗng và xóa node head và trả về
dữ liệu node head. Nếu bạn đã từng sử dụng Push() và Pop(), thì danh sách của bạn sẽ giống như một STACK. Tuy nhiên,
chúng ta cung cấp nhiều hàm hỗ trợ như GetNth() nên danh sách chúng ta sẽ giống một danh sách liên kết đơn hơn chỉ là
một stack. Pop() sẽ thất bại nếu như không có node để Pop. Dưới đây là vài ví dụ:

void PopTest() {
struct node* head = BuildOneTwoThree(); // build {1, 2, 3}
int a = Pop(&head); // deletes "1" node and returns 1
int b = Pop(&head); // deletes "2" node and returns 2
int c = Pop(&head); // deletes "3" node and returns 3
int len = Length(head); // the list is now empty, so len == 0
}

Hình vẽ mô phỏng bộ nhớ Pop()

Pop()

/*
The opposite of Push(). Takes a non-empty list
and removes the front node, and returns the data
which was in that node.
*/
int Pop(struct node** headRef) {
// your code...

6
More information: www.itspiritclub.net Translater: huahongquan2007

Một vấn đề khó hơn là viết một hàm InsertNth() mà nó chèn thêm một node mới vào một vị trí thứ index trong
danh sách. Push() thì tương tự nhưng chỉ có thể chèn một node ở đầu trong danh sách (index 0). Index phải ở trong
khoảng [0 …length], và node mới sẽ được chèn ở vị trí index.

void InsertNthTest() {
struct node* head = NULL; // start with the empty list
InsertNth(&head, 0, 13); // build {13)
InsertNth(&head, 1, 42); // build {13, 42}
InsertNth(&head, 1, 5); // build {13, 5, 42}
DeleteList(&head); // clean up after ourselves
}

InsertNth() thì khá phức tạp – có lẽ bạn sẽ cần vẽ hình mô phỏng để suy nghĩ về hướng giải và kiểm tra độ chính
xác.

/*
A more general version of Push().
Given a list, an index 'n' in the range 0..length,
and a data element, add a new node to the list so
that it has the given index.
*/
void InsertNth(struct node** headRef, int index, int data) {
// your code...

Viết một hàm SortedInsert() mà nhận ra một danh sách đã được sắp tăng và một node đơn lẻ, sau đó chèn node
vào đúng vị trí được sắp xếp trong danh sách. Trong khi hàm Push() cấp phát một node mới và thêm vào danh sách,
SortedInsert() nhận một node có sẵn và chỉ thay đổi con trỏ để nó chèn vào trong danh sách. Có nhiều giải pháp cho vấn
đề này.

void SortedInsert(struct node** headRef, struct node* newNode) {


// Your code...

Viết một hàm InsertSort() mà nhận một danh sách, và sắp xếp các node của nó để chúng được sắp xếp theo thứ tự
tăng dần. Nó sẽ dùng SortedInsert().

7
More information: www.itspiritclub.net Translater: huahongquan2007

// Given a list, change it to be in sorted order (using SortedInsert()).


void InsertSort(struct node** headRef) {
// Your code

Viết một hàm Append() mà nhận hai danh sách , “a” và “b” , nối “b” vào cuối của “a” ,và khi đó đặt “b” thành
NULL. Dưới đây là hình vẽ mô phỏng Append(a, b) với trạng thái ban đầu là màu xám và trạng thái cuối cùng là màu
đen. Ở cuối của lần gọi, danh sách sẽ là {1, 2, 3, 4} và “b” thì rỗng

Rõ ràng là cả hai con trỏ đầu được truyền vào trong Append(a, b) cần phải là đối số tham chiếu vì chúng đều phải
bị thay đổi. Đối số thứ hai “b” thì luôn phải được gán về NULL. Vậy khi nào thì “a” thay đổi ? Trường hợp đó xảy ra khi
danh sách “a” lúc bắt đầu là một danh sách rỗng. Trong trường hợp đó, con trỏ head của “a” cần phải chuyển từ NULL
sang trỏ tới danh sách “b”.

// Append 'b' onto the end of 'a', and then set 'b' to NULL.
void Append(struct node** aRef, struct node** bRef) {
// Your code...

8
More information: www.itspiritclub.net Translater: huahongquan2007

Hàm FrontBackSplit() sẽ nhận một hàm danh sách và sau đó cắt danh sách thành hai danh sách nhỏ - một danh
sách ở trước và một danh sách ở sau. Nếu số lượng các phần tử thì lạ, phần tử thừa sẽ đi vào danh sách trước. Vậy
FrontBackSplit() đối với danh sách {2, 3, 5, 7, 11} sẽ chia thành hai danh sách nhỏ {2, 3, 5} và {7, 11}. Để làm đúng tất
cả các trường hợp thì khá là khó. Bạn cần phải kiểm tra giải pháp của bạn đối với các trường hợp (length = 2, length = 3,
length=4) để chắc chắn rằng danh sách được cắt đúng như điều kiện đề bài. Nếu nó hoạt động đúng cho length = 4, nó sẽ
hoạt động cho length = 1000. Bạn cũng cần phải xử lý trường hợp đặc biệt là length < 2.

Gợi ý: Chiến lược dễ nhất là tính toán chiều dài của danh sách, sau đó dùng một vòng lặp để nhảy qua các node để tìm
node cuối cùng của nửa danh sách trước và cắt danh sách đúng điểm đó. Có một kĩ thuật là sử dụng hai con trỏ để lướt
qua danh sách. Một con trỏ “chậm” tiến tới một node mỗi lần, trong khi con trỏ “nhanh” thì tới hai node mỗi lần. Khi mà
con trỏ nhanh đến cuối danh sách, con trỏ “chậm” sẽ ở giữa quãng đường. Đối với các chiến lược khác, điều cần quan tâm
là chia danh sách đúng vị trí.

/*
Split the nodes of the given list into front and back halves,
and return the two lists using the reference parameters.
If the length is odd, the extra node should go in the front list.
*/
void FrontBackSplit(struct node* source,
struct node** frontRef, struct node** backRef) {
// Your code...

Viết một hàm RemoveDuplicates() để nhận một danh sách, sắp xếp chúng theo thứ tự tăng dần và xóa những node
trùng nhau trong danh sách. Để cho lý tưởng, danh sách cần được nghiên cứu toàn bộ một lần.

/*
Remove duplicates from a sorted list.
*/
void RemoveDuplicates(struct node* head) {
// Your code...

Đây là một biến thể của Push(). Thay vì tạo ra một node và push nó vào trong danh sách co sẵn, MoveNode() sẽ
nhận hai danh sách, bỏ đi node trước (front node) của danh sách thứ haiv à push nó vào đầu danh sách thứ nhất. Hàm
MoveNode() trở thành một hàm tiện ích rất có lợi cho nhiều vấn đề sau. Cả hai Push() và MoveNode() được thiết kế
quanh đặc trưng là các hàm trong danh sách thường hoạt động chủ yếu ở đầu danh sách. Dưới đây là vài ví dụ:

9
More information: www.itspiritclub.net Translater: huahongquan2007

void MoveNodeTest() {
struct node* a = BuildOneTwoThree(); // the list {1, 2, 3}
struct node* b = BuildOneTwoThree();
MoveNode(&a, &b);
// a == {1, 1, 2, 3}
// b == {2, 3}
}
/*
Take the node from the front of the source, and move it to
the front of the dest.
It is an error to call this with the source list empty.
*/
void MoveNode(struct node** destRef, struct node** sourceRef) {
// Your code

Viết một hàm AlternatingSplit() mà nhận một danh sách và chia các node của nó để tạo thành hai danh sách nhỏ
hơn. Danh sách nhỏ phải được làm tử các phần tử luân phiên nhau trong danh sách gốc. Nếu danh sách là {a , b, a, b, a}
thì danh sách phụ là {a, a, a} và danh sách còn lại là { b, b}. Bạn sẽ cần dùng hàm MoveNode(). Các phần tử trong danh
sách mới có thể ở thứ tự bất kì.

/*
Given the source list, split its nodes into two shorter lists.
If we number the elements 0, 1, 2, ... then all the even elements
should go in the first list, and all the odd elements in the second.
The elements in the new lists may be in any order.
*/
void AlternatingSplit(struct node* source,
struct node** aRef, struct node** bRef) {
// Your code

Nhận hai danh sách, trộn các node với nhau để tạo thành một danh sách, nhận các node luân phiên nhau giữa hai
danh sách. Vậy ShuffleMerge() với {1, 2, 3} và {7 , 13, 1} sẽ tạo ra danh sách {1, 7, 2, 13, 3, 1}. Nếu một trong hai danh
sách hết node, hàm sẽ lấy hết tất cả các node còn lại của danh sách kia. Giải pháp thì phụ thuộc trên khả năng di chuyển
các node tới cuối của một danh sách như đã bàn ở phần 1. Bạn có thể sử dụng hàm MoveNode(). Nếu sử dụng hàm
FrontBackSplit(), bạn có thể giả lập quá trình xáo bài.

/*
Merge the nodes of the two lists into a single list taking a node
alternately from each list, and return the new list.
*/
struct node* ShuffleMerge(struct node* a, struct node* b) {
// Your code

10
More information: www.itspiritclub.net Translater: huahongquan2007

Viết một hàm SortedMerge() để nhận hai danh sách, mỗi cái thì được sắp xếp tăng dần, và trộn hai cái lại với
nhau thành một mảng tăng dần. Có nhiều trường sẽ xảy ra: có thể “a” hoặc “b” có thể trống, trong quá trình xử lý có thể
“a” hoặc “b” hết trước và cuối cùng thì có vấn đề từ việc bắt đầu từ một danh sách rỗng và xây dựng nó trong khi đi qua
“a” và “b”.

/*
Takes two lists sorted in increasing order, and
splices their nodes together to make one big
sorted list which is returned.
*/
struct node* SortedMerge(struct node* a, struct node* b) {
// your code...

(Vấn đề này yêu cầu đệ quy) Nhận một FrontBackSplit() và SortedMerge(), nó thì khá dễ để viết một hàm
MergeSort() đệ quy: cắt danh sách làm hai phần nhỏ hơn, sắp xếp theo đệ quy hai danh sách và cuối cùng nối các danh
sách được sắp xếp lại với nhau thành một danh sách đơn. Trớ trêu thay, vấn đề này thì dễ hơn là FrontBackSplit hay
SortedMerge.

void MergeSort(struct node* headRef) {


// Your code...

Nhận hai danh sách đã sắp xếp tăng, tạo và trả về một danh sách mới đại diện cho chỗ giao nhau của hai danh
sách. Danh sách mới nên có bộ nhớ của chính nó – các danh sách gốc thì không nên bị thay đổi. Nói cách khác, nên sử
dụng Push() để xây dựng danh sách chứ không phải MoveNode().

/*
Compute a new sorted list that represents the intersection
of the two given sorted lists.
*/
struct node* SortedIntersect(struct node* a, struct node* b) {
// Your code

11
More information: www.itspiritclub.net Translater: huahongquan2007

Viết một hàm Reverse() lặp để đảo ngược một danh sách bằng cách sửa lại tất cả trường .next và con trỏ head.
Giải pháp lặp thì khá là phức tạp. Nó thì không quá khó để đặt ở cuối tài liệu này, nhưng nó cần thiết để xử lý vấn đề thứ
18.

void ReverseTest() {
struct node* head;
head = BuildOneTwoThree();
Reverse(&head);
// head now points to the list {3, 2, 1}
DeleteList(&head); // clean up after ourselves
}

"Push" Reverse Hint


Quá trình lặp qua danh sách chính. Di chuyển mỗi node tới đâu trước của danh sách kết quả khi bạn thực hiện.
Giống như là Push() cho mỗi node, ngoại trừ bạn thay đổi con trỏ trong các node có sẵn thay vì cấp phát lại nó. Bạn có thể
dùng MoveNode() để thực hiện hầu hết công việc hoặc là thay đổi con trỏ bằng tay.

"3 Pointers" Hint


Kĩ thuật này không tốt như kĩ thuật Push. Thay vì chạy một con trỏ current xuống danh sách, chạy ba con trỏ
(front, middle, back) xuống danh sách theo thứ tự : front là một node, middle là node sau đó và back là node sau middle.
Khi code chạy 3 con trỏ xuống đã rõ ràng, kiểm tra trong một hình minh họa, thêm code để đảo trường .next của node
middle trong quá trình lặp. Thêm code để xử lý danh sách rỗng và thay đổi con trỏ head.

/*
Reverse the given linked list by changing its .next pointers and
its head pointer. Takes a pointer (reference) to the head pointer.
*/
void Reverse(struct node** headRef) {
// your code...

12
More information: www.itspiritclub.net Translater: huahongquan2007

(Vấn đề này thì khó và chỉ nên làm nếu bạn đã quen với đệ quy) Có một giải pháp đệ quy hiệu quả và ngắn gọn cho vấn
đề này.

/*
Recursively reverses the given linked list by changing its .next
pointers and its head pointer in one pass of the list.
*/
void RecursiveReverse(struct node** headRef) {
// your code...

13
More information: www.itspiritclub.net Translater: huahongquan2007

Phần 3 – Giải pháp đối với các vấn đề

Một vòng lặp thẳng xuống danh sách – giống như Length().

int Count(struct node* head, int searchFor) {


struct node* current = head;
int count = 0;
while (current != NULL) {
if (current->data == searchFor) count++;
current = current->next;
}
return count;
}

Có thể dùng for thay cho while

int Count2(struct node* head, int searchFor) {


struct node* current;
int count = 0;
for (current = head; current != NULL; current = current->next) {
if (current->data == searchFor) count++;
}
return count;
}

Kết hợp vòng lặp danh sách cơ bản với vấn đề đếm để tìm ra node đúng. Có những lỗi thì thông dụng trong
loại code này. Kiểm tra nó kĩ trong những trường hợp đơn giản. Nếu code hoạt động đúng cho n = 0, n = 1 và n =2 thì nó
sẽ đúng cho n = 1000

int GetNth(struct node* head, int index) {


struct node* current = head;
int count = 0; // the index of the node we're currently looking at
while (current != NULL) {
if (count == index) return(current->data);
count++;
current = current->next;
}
assert(0); // if we get to this line, the caller was asking
// for a non-existent element so we assert fail.
}

14
More information: www.itspiritclub.net Translater: huahongquan2007

Xóa toàn bộ danh sách và đặt con trỏ head về NULL. Có một sự phức tạp trong vòng lặp do chúng ta cần phải
tách con trỏ .next trước khi chúng ta xóa node, vì sau khi xóa, nó sẽ không khả dụng nữa.

void DeleteList(struct node** headRef) {


struct node* current = *headRef; // deref headRef to get the real head
struct node* next;
while (current != NULL) {
next = current->next; // note the next pointer
free(current); // delete the node
current = next; // advance to the next node
}
*headRef = NULL; // Again, deref headRef to affect the real head back
// in the caller.
}

Tách dữ liệu khỏi node đầu, xóa node, tiến con trỏ head để trỏ tới node tiếp theo. Dùng đối số tham chiếu vì nó
thay đổi con trỏ head.

int Pop(struct node** headRef) {


struct node* head;
int result;
head = *headRef;
assert(head != NULL);
result = head->data; // pull out the data before the node is deleted
*headRef = head->next; // unlink the head node for the caller
// Note the * -- uses a reference-pointer
// just like Push() and DeleteList().
free(head); // free the head node
return(result); // don't forget to return the data from the link
}

Đoạn mã này xử lý việc chèn vào đầu như là một trường hợp đặc biệt. Mặc dù nó hoạt động bằng cách chạy
con trỏ current tới node trước node cần thêm vào. Dùng một vòng lặp để kiểm tra con trỏ chính xác.

15
More information: www.itspiritclub.net Translater: huahongquan2007

void InsertNth(struct node** headRef, int index, int data) {


// position 0 is a special case...
if (index == 0) Push(headRef, data);
else {
struct node* current = *headRef;
int i;
for (i=0; i<index-1; i++) {
assert(current != NULL); // if this fails, index was too big
current = current->next;
}
assert(current != NULL); // tricky: you have to check one last time
Push(&(current->next), data); // Tricky use of Push() --
// The pointer being pushed on is not
// in the stack. But actually this works
// fine -- Push() works for any node pointer.
}
}

Chiến lược cơ bản là duyệt xuống danh sách để tìm nơi chèn node mới vào. Có thể là cuối của một danh sách
hoặc là một điểm trước node mà lớn hơn node mới. Có ba giải pháp cho ba cách khác nhau:

// Uses special case code for the head end


void SortedInsert(struct node** headRef, struct node* newNode) {
// Special case for the head end
if (*headRef == NULL || (*headRef)->data >= newNode->data) {
newNode->next = *headRef;
*headRef = newNode;
}
else {
// Locate the node before the point of insertion
struct node* current = *headRef;
while (current->next!=NULL && current->next->data<newNode->data) {
current = current->next;
}
newNode->next = current->next;
current->next = newNode;
}
}

16
More information: www.itspiritclub.net Translater: huahongquan2007

// Dummy node strategy for the head end


void SortedInsert2(struct node** headRef, struct node* newNode) {
struct node dummy;
struct node* current = &dummy;
dummy.next = *headRef;
while (current->next!=NULL && current->next->data<newNode->data) {
current = current->next;
}
newNode->next = current->next;
current->next = newNode;
*headRef = dummy.next;
}

// Local references strategy for the head end


void SortedInsert3(struct node** headRef, struct node* newNode) {
struct node** currentRef = headRef;
while (*currentRef!=NULL && (*currentRef)->data<newNode->data) {
currentRef = &((*currentRef)->next);
}
newNode->next = *currentRef; // Bug: this line used to have
// an incorrect (*currRef)->next
*currentRef = newNode;
}

Bắt đầu với một danh sách kết quả rỗng. Duyệt xuống danh sách nguồn và SortedInsert() mỗi nodes của nó
vào danh sách kết quả. Cẩn thận chú ý tới trường .next trong mỗi node trước khi di chuyển nó vào danh sách kết quả.

// Given a list, change it to be in sorted order (using SortedInsert()).


void InsertSort(struct node** headRef) {
struct node* result = NULL; // build the answer here
struct node* current = *headRef; // iterate over the original list
struct node* next;
while (current!=NULL) {
next = current->next; // tricky - note the next pointer before we change it
SortedInsert(&result, current);
current = next;
}
*headRef = result;
}

17
More information: www.itspiritclub.net Translater: huahongquan2007

Trường hợp mà danh sách “a” là rỗng là một trường hợp được xử lý đầu tiên – trong trường hợp đó con trỏ head
“a” cần phải thay đổi trực tiếp. Mặc khác, chúng ta duyệt xuống danh sách “a” cho tới khi chúng ta tìm thấy node cuối với
phép kiểm tra (current->next != NULL), và đính danh sách “b” ở đây. Cuối cùng, con trỏ head “b” thì được gán về NULL.
Đoạn code chứng minh việc dùng rộng rãi của đối số tham chiếu của con trỏ và những vấn đề thông thường mà cần thiết
để xác định vị trí của node cuối trong danh sách.

void Append(struct node** aRef, struct node** bRef) {


struct node* current;
if (*aRef == NULL) { // Special case if a is empty
*aRef = *bRef;
}
else { // Otherwise, find the end of a, and append b there
current = *aRef;
while (current->next != NULL) { // find the last node
current = current->next;
}
current->next = *bRef; // hang the b list off the last node
}
*bRef=NULL; // NULL the original b, since it has been appended above
}

Append() Test and Drawing

Hàm AppendTest() sau gọi hàm Append() để nối hai danh sách. Bộ nhớ sẽ như thế nào sau khi hàm Append()
thoát?

void AppendTest() {
struct node* a;
struct node* b;
// set a to {1, 2}
// set b to {3, 4}
Append(&a, &b);
}

Hãy chú ý cách mà đối số tham chiếu trong Append() trỏ về con trỏ head trong AppendTest()…

18
More information: www.itspiritclub.net Translater: huahongquan2007

Có hai giải pháp cho trường hợp này

// Uses the "count the nodes" strategy


void FrontBackSplit(struct node* source,
struct node** frontRef, struct node** backRef) {
int len = Length(source);
int i;
struct node* current = source;
if (len < 2) {
*frontRef = source;
*backRef = NULL;
}
else {
int hopCount = (len-1)/2; //(figured these with a few drawings)
for (i = 0; i<hopCount; i++) {
current = current->next;
}
// Now cut at current
*frontRef = source;
*backRef = current->next;
current->next = NULL;
}
}

// Uses the fast/slow pointer strategy


void FrontBackSplit2(struct node* source,
struct node** frontRef, struct node** backRef) {
struct node* fast;
struct node* slow;
if (source==NULL || source->next==NULL) { // length < 2 cases
*frontRef = source;
*backRef = NULL;
}
else {
slow = source;
fast = source->next;
// Advance 'fast' two nodes, and advance 'slow' one node
while (fast != NULL) {
fast = fast->next;
if (fast != NULL) {
slow = slow->next;
fast = fast->next;
}
}
// 'slow' is before the midpoint in the list, so split it in two
// at that point.
*frontRef = source;
*backRef = slow->next;
slow->next = NULL;
}
}

19
More information: www.itspiritclub.net Translater: huahongquan2007

Do danh sách đã được sắp xếp, chúng ta có thể duyệt xuống danh sách và so sánh các node liền kề. Khi các node liền kề
giống nhau, xóa cái thứ hai.

// Remove duplicates from a sorted list


void RemoveDuplicates(struct node* head) {
struct node* current = head;
if (current == NULL) return; // do nothing if the list is empty
// Compare current node with next node
while(current->next!=NULL) {
if (current->data == current->next->data) {
struct node* nextNext = current->next->next;
free(current->next);
current->next = nextNext;
}
else {
current = current->next; // only advance if no deletion
}
}
}

Code của MoveNode() thi tương tự như code của Push(). Nó thì ngắn và chỉ thay đổi một cặp con trỏ - nhưng
nó khá phức tạp. Hãy thử vẽ hình vẽ mô phỏng.

void MoveNode(struct node** destRef, struct node** sourceRef) {


struct node* newNode = *sourceRef; // the front source node
assert(newNode != NULL);
*sourceRef = newNode->next; // Advance the source pointer
newNode->next = *destRef; // Link the old dest off the new node
*destRef = newNode; // Move dest to point to the new node
}

Các tiếp cận đơn giản nhất là duyệ qua danh sách nguồn và dùng MoveNode() để kéo các node ra khỏi nguồn và
luân phiên đặt nó vào “a” và “b”. Phần kì lạ duy nhất là các node sẽ ở thứ tự ngược lại so với danh sách gốc.

20
More information: www.itspiritclub.net Translater: huahongquan2007

AlternatingSplit()

void AlternatingSplit(struct node* source,


struct node** aRef, struct node** bRef) {
struct node* a = NULL; // Split the nodes to these 'a' and 'b' lists
struct node* b = NULL;
struct node* current = source;
while (current != NULL) {
MoveNode(&a, &current); // Move a node to 'a'
if (current != NULL) {
MoveNode(&b, &current); // Move a node to 'b'
}
}
*aRef = a;
*bRef = b;
}

AlternatingSplit() Using Dummy Nodes

Đây là một cách tiếp cận thay thế mà nó xây dựng các hàm con trong thứ tự giống như danh sách gốc. Mã nguồn
sử dụng một node đầu dummy tạm cho danh sách “a” và “b”. Mỗi danh sách con có một con trỏ tail để trỏ tới node cuối
cùng của nó – bằng cách đó, node mới có thể được nối thêm vào đuôi của mỗi danh sách một cách dễ dàng. Node dummy
thì hữu dụng trong trường trường hợp này vì chúng ta tạm và được cấp phát ở trong stack. Một cách thay thế, kĩ thuật
tham chiếu cục bộ ( local references) có thể dùng để loại bỏ các node dummy( xem phần 1 để biết thêm chi tiết).

void AlternatingSplit2(struct node* source,


struct node** aRef, struct node** bRef) {
struct node aDummy;
struct node* aTail = &aDummy; // points to the last node in 'a'
struct node bDummy;
struct node* bTail = &bDummy; // points to the last node in 'b'
struct node* current = source;
aDummy.next = NULL;
bDummy.next = NULL;
while (current != NULL) {
MoveNode(&(aTail->next), &current); // add at 'a' tail
aTail = aTail->next; // advance the 'a' tail
if (current != NULL) {
MoveNode(&(bTail->next), &current);
bTail = bTail->next;
}
}
*aRef = aDummy.next;
*bRef = bDummy.next;
}

21
More information: www.itspiritclub.net Translater: huahongquan2007

Dưới đây là bốn giải pháp riêng biệt. Xem phần 1 để biết thêm về dummy node và local references.

SuffleMerge() — Dummy Node Not Using MoveNode()

struct node* ShuffleMerge(struct node* a, struct node* b) {


struct node dummy;
struct node* tail = &dummy;
dummy.next = NULL;
while (1) {
if (a==NULL) { // empty list cases
tail->next = b;
break;
}
else if (b==NULL) {
tail->next = a;
break;
}
else { // common case: move two nodes to tail
tail->next = a;
tail = a;
a = a->next;
tail->next = b;
tail = b;
b = b->next;
}
}
return(dummy.next);
}

SuffleMerge() — Dummy Node Using MoveNode()

Tương tự như ở trên nhưng sử dụng hàm MoveNode()

struct node* ShuffleMerge(struct node* a, struct node* b) {


struct node dummy;
struct node* tail = &dummy;
dummy.next = NULL;
while (1) {
if (a==NULL) {
tail->next = b;
break;
}
else if (b==NULL) {
tail->next = a;
break;
}
else {
MoveNode(&(tail->next), &a);
tail = tail->next;
MoveNode(&(tail->next), &b);
tail = tail->next;
}
}
return(dummy.next);
}

22
More information: www.itspiritclub.net Translater: huahongquan2007

SuffleMerge() — Local References

Dùng tham chiếu cục bộ (local references) để loại bỏ các dummy node

struct node* ShuffleMerge(struct node* a, struct node* b) {


struct node* result = NULL;
struct node** lastPtrRef = &result;
while (1) {
if (a==NULL) {
*lastPtrRef = b;
break;
}
else if (b==NULL) {
*lastPtrRef = a;
break;
}
else {
MoveNode(lastPtrRef, &a);
lastPtrRef = &((*lastPtrRef)->next);
MoveNode(lastPtrRef, &b);
lastPtrRef = &((*lastPtrRef)->next);
}
}
return(result);
}

SuffleMerge() — Recursive

Giải pháp đệ quy thì súc tích hơn cả, nhưng nó có thể không phù hợp cho mã nguồn thương mại vì nó dùng không gian
space tương ứng với độ dài của danh sách.

struct node* ShuffleMerge(struct node* a, struct node* b) {


struct node* result;
struct node* recur;
if (a==NULL) return(b); // see if either list is empty
else if (b==NULL) return(a);
else {
// it turns out to be convenient to do the recursive call first --
// otherwise a->next and b->next need temporary storage.
recur = ShuffleMerge(a->next, b->next);
result = a; // one node from a
a->next = b; // one from b
b->next = recur; // then the rest
return(result);
}
}

23
More information: www.itspiritclub.net Translater: huahongquan2007

SortedMerge() Using Dummy Nodes

Kĩ thuật này dùng node dummy tạm như là node bắt đầu của danh sách kết quả. Con trỏ tail luôn luôn trỏ tới node cuối
cùng của danh sách kết quả nên nối node mới thì đơn giản. Dummy node thì hữu dụng vì nó là tạm thời, nó cấp phát ở
trong stack. Vòng lặp hoạt động, loại bỏ một node khỏi “a” hoặc “b” và thêm nó vào tail. Khi hoàn thành, kết quả trả về là
dummy.next.

struct node* SortedMerge(struct node* a, struct node* b) {


struct node dummy; // a dummy first node to hang the result on
struct node* tail = &dummy; // Points to the last result node --
// so tail->next is the place to add
// new nodes to the result.
dummy.next = NULL;
while (1) {
if (a == NULL) { // if either list runs out, use the other list
tail->next = b;
break;
}
else if (b == NULL) {
tail->next = a;
break;
}
if (a->data <= b->data) {
MoveNode(&(tail->next), &a);
}
else {
MoveNode(&(tail->next), &b);
}
tail = tail->next;
}
return(dummy.next);
}

SortedMerge() Using Local References

Giải pháp này về cấu trúc thì rất tương tự như trên, nhưng nó tránh dùng node dummy. Thay vào đó, nó duy trì con trỏ
struct node **, lastPtrRef, nó luôn trỏ về con trỏ cuối của danh sách kết quả. Điều này giải quyết trường hợp tương tự như
node dummy – xử lý với danh sách kết quả khi nó rỗng.

24
More information: www.itspiritclub.net Translater: huahongquan2007

struct node* SortedMerge2(struct node* a, struct node* b) {


struct node* result = NULL;
struct node** lastPtrRef = &result; // point to the last result pointer
while (1) {
if (a==NULL) {
*lastPtrRef = b;
break;
}
else if (b==NULL) {
*lastPtrRef = a;
break;
}
if (a->data <= b->data) {
MoveNode(lastPtrRef, &a);
}
else {
MoveNode(lastPtrRef, &b);
}
lastPtrRef = &((*lastPtrRef)->next); // tricky: advance to point to
// the next ".next" field
}
return(result);
}

SortedMerge() Using Recursion

Merge() là một trong những vấn đề đệ quy đẹp khi đệ quy thì rõ ràng hơn là code lặp.

struct node* SortedMerge3(struct node* a, struct node* b) {


struct node* result = NULL;
// Base cases
if (a==NULL) return(b);
else if (b==NULL) return(a);
// Pick either a or b, and recur
if (a->data <= b->data) {
result = a;
result->next = SortedMerge3(a->next, b);
}
else {
result = b;
result->next = SortedMerge3(a, b->next);
}
return(result);
}

25
More information: www.itspiritclub.net Translater: huahongquan2007

Ý tưởng của MergeSort là : cắt thành các danh sách nhỏ, sắp xếp chúng bằng đệ quy và trộn hai danh sách đã được sắp
xếp với nhau để tạo thành câu trả lời.

void MergeSort(struct node** headRef) {


struct node* head = *headRef;
struct node* a;
struct node* b;
// Base case -- length 0 or 1
if ((head == NULL) || (head->next == NULL)) {
return;
}
FrontBackSplit(head, &a, &b); // Split head into 'a' and 'b' sublists
// We could just as well use AlternatingSplit()
MergeSort(&a); // Recursively sort the sublists
MergeSort(&b);
*headRef = SortedMerge(a, b); // answer = merge the two sorted lists together
}

Kĩ thuật là tiến tới trong cả hai danh sách và xây dựng danh sách kết quả. Khi vị trí current trong cả hai danh sách
thì giống nhau, thêm một node vào trong kết quả.Bằng cách khai thác việc cả hai danh sách được sắp xếp, chúng ta chỉ
cần đi qua danh sách một lần. Bằng cách xây dựng danh sách kết quả, có thể dùng cả hai node dummy và local reference
như sau:

// This solution uses the temporary dummy to build up the result list
struct node* SortedIntersect(struct node* a, struct node* b) {
struct node dummy;
struct node* tail = &dummy;
dummy.next = NULL;
// Once one or the other list runs out -- we're done
while (a!=NULL && b!=NULL) {
if (a->data == b->data) {
Push((&tail->next), a->data);
tail = tail->next;
a = a->next;
b = b->next;
}
else if (a->data < b->data) { // advance the smaller list
a = a->next;
}
else {
b = b->next;
}
}
return(dummy.next);

26
More information: www.itspiritclub.net Translater: huahongquan2007

// This solution uses the local reference


struct node* SortedIntersect2(struct node* a, struct node* b) {
struct node* result = NULL;
struct node** lastPtrRef = &result;
// Advance comparing the first nodes in both lists.
// When one or the other list runs out, we're done.
while (a!=NULL && b!=NULL) {
if (a->data == b->data) { // found a node for the intersection
Push(lastPtrRef, a->data);
lastPtrRef = &((*lastPtrRef)->next);
a=a->next;
b=b->next;
}
else if (a->data < b->data) { // advance the smaller list
a=a->next;
}
else {
b=b->next;
}
}
return(result);
}

27
More information: www.itspiritclub.net Translater: huahongquan2007

Giải pháp đầu tiên là dùng kĩ thuật “Push” với sự thay đổi con trỏ bằng tay trong vòng lặp. Giải pháp này khá là mánh nếu
chúng ta cần phải lưu giá trị của con trỏ “current ->next” ở đầu vòng lặp khi thân của vòng lặp sẽ ghi đè con trỏ đó.

/*
Iterative list reverse.
Iterate through the list left-right.
Move/insert each node to the front of the result list --
like a Push of the node.
*/
static void Reverse(struct node** headRef) {
struct node* result = NULL;
struct node* current = *headRef;
struct node* next;
while (current != NULL) {
next = current->next; // tricky: note the next node
current->next = result; // move the node onto the result
result = current;
current = next;
}
*headRef = result;
}

Một biến thể sử dụng hàm MoveNode()

static void Reverse2(struct node** headRef) {


struct node* result = NULL;
struct node* current = *headRef;
while (current != NULL) {
MoveNode(&result, &current);
}
*headRef = result;
}

28
More information: www.itspiritclub.net Translater: huahongquan2007

Cuối cùng đây là kĩ thuật front-middle-back

// Reverses the given linked list by changing its .next pointers and
// its head pointer. Takes a pointer (reference) to the head pointer.
void Reverse(struct node** headRef) {
if (*headRef != NULL) { // special case: skip the empty list
/*
Plan for this loop: move three pointers: front, middle, back
down the list in order. Middle is the main pointer running
down the list. Front leads it and Back trails it.
For each step, reverse the middle pointer and then advance all
three to get the next node.
*/
struct node* middle = *headRef; // the main pointer
struct node* front = middle->next; // the two other pointers (NULL ok)
struct node* back = NULL;
while (1) {
middle->next = back; // fix the middle node
if (front == NULL) break; // test if done
back = middle; // advance the three pointers
middle = front;
front = front->next;
}
*headRef = middle; // fix the head pointer to point to the new front
}
}

Có lẽ phần khó nhất là nắm bắt được khái niệm của RecursiveReverse(&rest). Hãy vẽ hình mô phỏng để hiểu
cách hàm hoạt động.

void RecursiveReverse(struct node** headRef) {


struct node* first;
struct node* rest;
if (*headRef == NULL) return; // empty list base case
first = *headRef; // suppose first = {1, 2, 3}
rest = first->next; // rest = {2, 3}
if (rest == NULL) return; // empty rest base case
RecursiveReverse(&rest); // Recursively reverse the smaller {2, 3} case
// after: rest = {3, 2}
first->next->next = first; // put the first elem on the end of the list
first->next = NULL; // (tricky step -- make a drawing)
*headRef = rest; // fix the head pointer
}

Giải pháp kém hiệu quả là đảo những phầ tử n-1 cuối của danh sách, và sau đó duyệt xuống tất cả xuống tới tail
mới và đặt node head cũ đó. Giải pháp đó thì rất là chậm so với trên mà nó lấy node đầu ở đúng vị trí mà không cần vòng
lặp thêm.

29
More information: www.itspiritclub.net Translater: huahongquan2007

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
Xin chân thành cám ơn!

30

You might also like