Sắp xếp danh sách
Một danh sách có thứ tự (danh sách được sắp) là một danh sách mà các phần tử của nó được sắp xếp theo một thứ tự nào đó dựa trên một trường khoá. Ví dụ, danh sách các phần tử số có thứ tự tăng là danh sách mà với mọi cặp phần tử X, Y ta ...
Một danh sách có thứ tự (danh sách được sắp) là một danh sách mà các phần tử của nó được sắp xếp theo một thứ tự nào đó dựa trên một trường khoá. Ví dụ, danh sách các phần tử số có thứ tự tăng là danh sách mà với mọi cặp phần tử X, Y ta luôn có X?Y nếu X xuất hiện trước Y trong danh sách (danh sách có 1 hoặc không có phần tử nào được xem là một danh sách được sắp). Ðể sắp xếp một danh sách, ta có thể thực hiện một trong 2 phương án sau:
- Phương án 1: Hoán vị nội dung các phần tử trong danh sách (thao tác trên vùng Info). Với phương án này, có thể chọn một trong những thuật toán sắp xếp đã biết để cài đặt lại trên xâu như thực hiện trên mảng, điểm khác biệt duy nhất là cách thức truy xuất đến các phần tử trên xâu thông qua liên kết thay vì chỉ số như trên mảng. Do dựa trên việc hoán vị nội dung của các phần tử, phương pháp này đòi hỏi sử dụng thêm vùng nhớ trung gian nên chỉ thích hợp với các xâu có các phần tử có thành phần Info kích thước nhỏ. Hơn nữa số lần hoán vị có thể lên đến bậc n2 với xâu n phần tử. Khi kích thước của trường Info lớn, việc hoán vị giá trị của hai phân tử sẽ chiếm chi phí đáng kể. Ðiều này sẽ làm cho thao tác xắp xếp chậm lại. Như vậy, phương án này không tận dụng được các ưu điểm của xâu .
Ví dụ :
Cài đặt thuật toán sắp xếp Chọn trực tiếp trên xâu :
void ListSelectionSort (LIST &l){
NODE *min; // chỉ đến phần tử có giá trị nhỏ nhất trong xâu NODE *p,*q;
p = l.pHead;
while(p != l.pTail){ q = p->pNext; min = p; while(q != NULL) {if(q->Info< min->Info ) min = q; // ghi nhận vị trí phần tử min hiện hành q
q= q->pNext;}// Hoán vị nội dung 2 phần tử
Hoanvi(min->Info, p->Info]);p = p->pNext;
}
}
- Phương án 2: Thay đổi các mối liên kết (thao tác trên vùng Next)
Do các nhược điểm của các phương pháp sắp xếp theo phương án 1, khi dữ liệu lưu tại mỗi phần tử trong xâu có kích thước lớn người ta thường dùng một cách tiếp cận khác. Thay vì hoán đổi giá trị, ta sẽ tìm cách thay đổi trình tự móc nối của các phần tử sao cho tạo lập nên được thứ tự mong muốn. Cách tiếp cận này sẽ cho phép ta chỉ thao tác trên các móc nối (trường pNext). Như ta đã biết, kích thước của trường này không phụ thuộc vào bản chất dữ liệu lưu trong xâu vì có bằng đúng một con trỏ (2 byte hoặc 4 byte trong môi trường 16 bit và 4 byte hoặc 8 byte trong môi trường 32 bit, .). Tuy nhiên thao tác trên các mọc nối thường sẽ phức tạp hơn là thao tác trực tiếp trên dữ liệu. Vì vậy, ta cần cân nhắc kỹ lưỡng trước khi chọn cách tiếp cận. Nếu dũ liệu không quá lớn thì ta nên chọn phương án 1 hoặc một thuật toán hiệu quả nào đó.
Một trong những cách thay đổi móc nối đơn giản nhất là tạo một danh sách mới là danh sách có thứ tự từ danh sách cũ (đồng thời hủy danh sách cũ). Giả sử danh sách mới sẽ được quản lý bằng con trỏ đầu xâu Result, ta có phương án 2 của thuật toán chọn trực tiếp như sau :
- Bước1: Khởi tạo danh sách mới Result là rỗng;
- Bước2: Tìm trong danh sách cũ l phần tử nhỏ nhất;
- Bước3: Tách min khỏi danh sách l;
- Bước4: Chèn min vào cuối danh sách Result;
- Bước5: Lặp lại bước 2 khi chưa hết danh sách Head;
Ta có thể cài đặt thuật toán trên như sau:
void ListSelectionSort2 (LIST &l){ LIST lRes;
NODE *min; // chỉ đến phần tử có giá trị nhỏ nhất trong xâu
NODE *p,*q, minprev;
lRes.pHead = lRes.pTail = NULL; // khởi tạo lRes
while(l.pHead != NULL){
p = l.pHead;q = p->pNext; min = p; minprev = NULL;while(q != NULL){
if(q->Info< min->Info ) {
min = q; minprev = p
}
p = q; q = q->pNext;
}if(minprev != NULL)
minprev->pNext = min->pNext;
else
l.pHead = min->pNext;
min->pNext = NULL;
AddTail(lRes, min);
}
l = lRes;
}
Thuật toán Quick Sort
Trong số các thuật toán sắp xếp, có lẽ nổi tiếng nhất về hiệu quả là thuật toán Quick Sort. Các cài đặt của thuật toán này thường thấy trên cấu trúc dữ liệu mảng. Trong chương 2 chúng ta đã khảo sát thuật toán này. Tuy nhiên ít ai để ý rằng nó cũng là một trong những thuật toán sắp xếp hiệu quả nhất trên xâu. Hơn nữa, khi cài đặt trên xâu, bản chất của thuật toán này thể hiện một cách rõ ràng hơn bao giờ hết:
- Thuật toán Quick Sort:
Bước 1:
Chọn X là phần tử đầu xâu L làm phần tử cầm canh. Loại X ra khỏi L.
Bước 2:
Tách xâu L ra làm 2 xâu L1 (gồm các phần tử nhỏ hơn hay bằng X) và L2 (gồm các phần tử lớn hơn X).
Bước 3:
Nếu L1 != NULL thì Quick Sort (L1).
Bước 4:
Nếu L2 != NULL thì Quick Sort (L2).
Bước 5:
Nối L1, X, và L2 lại theo trình tự ta có xâu L đã được sắp xếp.
- Ví dụ
Cho dãy số a:
4 6 5 1 8 2
Chọn X = 4 làm phần tử cầm canh và tách l thành l1, l2:
Sắp xếp l1: |
Sắp xếp l2:
Chọn X = 6 làm phần tử cầm canh và tách l2 thanh l21, l22:
Nối l12, X2, l22 thành l2:
Nối l1, X, l2 thành l:
- Cài đặt :
void ListQSort(LIST & l){ NODE *p, *X; // X chỉ đến phần tử cầm canh
LIST l1, l2;
if(l.pHead == l.pTail) return;//đã có thứ tự
l1.pHead == l1.pTail = NULL; //khởi tạo
l2.pHead == l2.pTail = NULL;
X = l.pHead; l.pHead = X->pNext;while(l.pHead != NULL) //Tách l thành l1, l2;{
p = l.pHead;
l.pHead = p->pNext; p->pNext = NULL;if (p->Info <= X->Info)
AddTail(l1, p);
else
AddTail(l2, p);
}
ListQSort(l1); //Gọi đệ qui để sort l1
ListQSort(l2); //Gọi đệ qui để sort l2
//Nối l1, X và l2 lại thành l đã sắp xếp.
if(l1.pHead != NULL)
{
l.pHead = l1.pHead; l1.pTail->pNext = X;
}
else
l.pHead = X;
X->pNext = l2;
if(l2.pHead != NULL)
l.pTail = l2.pTail;
else
l.pTail = X;
}
Như chúng ta đã thấy, Quick sort trên xâu đơn đơn giản hơn phiên bản của nó trên mảng một chiều nhiều. Hãy cài đặt thử thuật toán này các bạn sẽ thấy hiệu quả của nó khó có thuật toán nào sánh bằng. Một điều đáng lưu ý là khí dùng quick sort sắp xếp một xâu đơn, ta chỉ có một chọn lựa phần tử cầm canh duy nhất hợp lý là phân tử đầu xâu. Chọn bất kỳ phần tử nào khác cũng làm tăng chi phí một cách không cần thiết do cấu trức tự nhiên của xấu.
Thuật toán Merge Sort
Cũng như thuật toán Quick Sort, Merge sort là một trong những thuật toán sắp xếp hiệu quả nhất trên xâu. Cài đặt của thuật toán này trên cấu trúc dữ liệu mảng rất rắc rối như các bạn đã thấy trong chương 2. Người ta hay nhắc đến Merge Sort như là một thuật toán sắp xếp trên file (sắp xếp ngoài). Cũng như Quick Sort, khi cài đặt trên xâu, bản chất của thuật toán này thể hiện rất rõ ràng:
- Thuật toán Merge Sort:
Bước 1:
Phân phối luân phiên từng đường chạy của xâu L vào 2 xâu con L1 và L2.
Bước 2:
Nếu L1 != NULL thì Merge Sort (L1).
Bước 3:
Nếu L2 != NULL thì Merge Sort (L2).
Bước 4:
Trộn L1 và L2 đã sắp xếp lại ta có xâu L đã được sắp xếp.
- Ví dụ
Cho dãy số a:
4 6 5 1 8 2
Phân phối các đường chạy của l vào l1, l2:
Sắp xếp l1:
Phân phối các đường chạy của l1 vào l11, l12:
Trộn l11, l12 lại thành l1:
Sắp xếp l2:
Phân phối các đường chạy của l2 vào l21, l22:
Trộn l11, l12 lại thành l2:
Trộn l1, l2 lại thành l:
- Cài đặt :
void ListMergeSort(LIST & l){ LIST l1, l2;
if(l.pHead == l.pTail) return;//đã có thứ tự
l1.pHead == l1.pTail = NULL; //khởi tạo
l2.pHead == l2.pTail = NULL;
//Phân phối l thành l1 và l2 theo từng đưòng chạy
DistributeList(l, l1, l2);
ListMergeSort(l1); //Gọi đệ qui để sort l1
ListMergeSort(l2); //Gọi đệ qui để sort l2
//Trộn l1 và l2 đã có thứ tự thành l
MergeList(l, l1, l2);
}
Trong đó, các hàm DistributeList và MergeList được viết như sau:
void DistributeList(LIST& l,LIST& l1,LIST& l2)
{ NODE *p;
do //Tách l thành l1, l2;{
p = l.pHead;
l.pHead = p->pNext; p->pNext = NULL;AddTail(l1, p);
}while((l.pHead)&&(p->Info<=l.pHead->Info));
if(l.pHead)
DistributeList(l, l2, l1);
else
l.pTail = NULL;
}
void MergeList(LIST& l,LIST& l1,LIST& l2)
{ NODE *p;
while((l1.pHead)&&(l2.pHead))
{
if(l1.pHead->Info <= l2.pHead->Info) {
p = l1.pHead;
l1.pHead = p->pNext;
}
else {
p = l2.pHead;
l2.pHead = p->pNext;
}
p->pNext = NULL; AddTail(l, p);
};
if(l1.pHead) {//Nối phần còn lại của l1 vào cuối l
l.pTail->pNext = l1.pHead;
l.pTail = l1.pTail;
}
else if(l2.pHead) {//Nối phần còn lại của l2 vào cuối l
l.pTail->pNext = l2.pHead;
l.pTail = l2.pTail;
}
}
Như chúng ta đã thấy, Merge sort trên xâu đơn đơn giản hơn phiên bản của nó trên mảng một chiều. Một điều đáng lưu ý là khí dùng Merge sort sắp xếp một xâu đơn, ta không cần dùng thêm vùng nhớ phụ như khi cài đặt trên mảng một chiều. Ngoài ra, thủ tục Merge trên xâu cũng không phức tạp như trên mảng vì ta chỉ phải trộng hai xâu đã có thứ tự, trong khi trên mảng ta phải trộn hai mảng bất kỳ.
3. Thuật toán Radix Sort
Thuật toán Radix Sort đã được giới thiệu trong chương 2. Khi cài đặt trên cấu trúc dữ liệu mảng một chiều, thuật toán này gặp một hạn chế lớn là đòi hỏi thêm quá nhiều bộ nhớ. Trong chương 2, chúng ta cũng đã đề cập đến khả năng cài đặt trên danh sách liên kết của thuật toán này. Sau đây là chi tiết thuật toán:
- Thuật toán Radix Sort:
Bước 1:
Khởi tạo các danh sách (lô) rỗng B0, B1, ., B9?; k = 0;
Bước 2:
Trong khi L khác rỗng:
B21: p = L.pHead; L.pHead->pNext;
B22: Ðặt phần tử p vào cuối lô Bd với d là chữ số thứ k của L.pHead->Info;
Bước 3:
Nối B0, B1, ., B9? lại thành L; Làm B0, B1, ., B9;
Bước 4:
k = k+1;
Nếu k < m: quay lại Bước 2
- Cài đặt :
void ListRadixSort(LIST & l, int m){ LIST B[10];
NODE *p;
int i, k;
if(l.pHead == l.pTail) return;//đã có thứ tự
for(i = 0; i < 10; i++)
B[i].pHead = B[i].pTail = NULL;
for(k = 0; k < 10; k++)
{
while(l.pHead) {
p = l.pHead;
l.pHead = p->pNext; p->pNext = NULL;
i = GetDigit(p->Info, k);
AddTail(B[i], p);
}
l = B[0];
for(i = 1; i < 10; i++)
AppendList(l, B[i]);//Nối B[i] vào cuối l
}
}
Trong đó, các hàm AppendList và GetDigit được viết như sau:
void AppendList(LIST& l,LIST& l1)
{
if(l.pHead) {
l.pTail->pNext = l1.pHead;
l.pTail = l1. pTail;
}
else //xâu l rỗng
l = l1;
}
int GetDigit(unsign long N, int k)
{
switch(k) {
case 0: return (N % 10);
case 1: return ((N/10) % 10);
case 2: return ((N/100) % 10);
case 3: return ((N/1000) % 10);
case 4: return ((N/10000) % 10);
case 5: return ((N/100000) % 10);
case 6: return ((N/1000000) % 10);
case 7: return ((N/10000000) % 10);
case 8: return ((N/100000000) % 10);
case 9: return ((N/1000000000) % 10);
}
}