Quick sort
Sau bài này bạn sẽ làm được
- Cài phân hoạch Lomuto đúng
- Chọn chốt bằng trung vị của ba
- Giải thích khi nào quicksort thành O(n bình phương)
- Giới hạn độ sâu ngăn xếp bằng cách chỉ đệ quy vào nửa nhỏ
Quick sort là thuật toán sắp xếp nhanh nhất trong thực tế, và cũng là thuật toán có nhiều cách viết sai nhất. Trường hợp xấu nhất của nó là O(n bình phương), và cách chọn chốt là thứ quyết định bạn có gặp nó không.
#Phân hoạch quanh một chốt
/* Khuôn chung:
1. Chọn chốt
2. Phân hoạch quanh chốt, trả về vị trí cuối cùng p của chốt
3. Đệ quy sắp a[lo..p) và a[p+1..hi]
Khác merge sort ở chỗ: merge sort chia dễ trộn khó,
quick sort chia khó gộp dễ. Sau khi phân hoạch xong thì
KHÔNG cần bước gộp nào cả. */| Merge sort | Quick sort | |
|---|---|---|
| Chia | Dễ, lấy điểm giữa | Khó, phải phân hoạch |
| Gộp | Khó, phải trộn | Không cần |
| Điểm chia | Luôn ở giữa | Phụ thuộc dữ liệu |
| Xấu nhất | O(n log n) | O(n bình phương) |
| Bộ nhớ phụ | O(n) | O(log n) ngăn xếp |
| Ổn định | Có | Không |
| Tốc độ thực tế | Chuẩn | Nhanh hơn 2 tới 3 lần |
#Phân hoạch Lomuto
#include <stddef.h>
static void doi_cho(int *a, int *b) { int t = *a; *a = *b; *b = t; }
/* Phân hoạch Lomuto trên khoảng ĐÓNG [lo, hi], chốt là a[hi].
Trả về vị trí cuối cùng của chốt.
Bất biến: a[lo..i) toàn phần tử NHỎ HƠN chốt,
a[i..j) toàn phần tử LỚN HƠN HOẶC BẰNG chốt. */
static size_t lomuto(int *a, size_t lo, size_t hi)
{
int chot = a[hi];
size_t i = lo;
for (size_t j = lo; j < hi; ++j)
if (a[j] < chot)
doi_cho(&a[i++], &a[j]);
doi_cho(&a[i], &a[hi]); /* đưa chốt về đúng chỗ */
return i;
}| j | a[j] | So với chốt 5 | i trước | Việc làm | Mảng sau |
|---|---|---|---|---|---|
| - | - | - | 0 | bắt đầu | 7 2 9 1 8 3 5 |
| 0 | 7 | không nhỏ hơn | 0 | bỏ qua | 7 2 9 1 8 3 5 |
| 1 | 2 | nhỏ hơn | 0 | đổi a[0] a[1], i=1 | 2 7 9 1 8 3 5 |
| 2 | 9 | không nhỏ hơn | 1 | bỏ qua | 2 7 9 1 8 3 5 |
| 3 | 1 | nhỏ hơn | 1 | đổi a[1] a[3], i=2 | 2 1 9 7 8 3 5 |
| 4 | 8 | không nhỏ hơn | 2 | bỏ qua | 2 1 9 7 8 3 5 |
| 5 | 3 | nhỏ hơn | 2 | đổi a[2] a[5], i=3 | 2 1 3 7 8 9 5 |
| hết | - | - | 3 | đổi a[3] a[6] | 2 1 3 5 8 9 7 |
Chốt 5 về vị trí 3. Bên trái là 2, 1, 3 đều nhỏ hơn 5. Bên phải là 8, 9, 7 đều lớn hơn 5. Hai bên chưa sắp, nhưng ô của chốt thì xong vĩnh viễn.
#Phân hoạch Hoare
/* Phân hoạch Hoare: hai con trỏ đi từ hai đầu vào giữa.
Trả về chỉ số j sao cho a[lo..j] và a[j+1..hi] là hai phần.
Chú ý: chốt KHÔNG nhất thiết nằm ở vị trí j. */
static size_t hoare(int *a, size_t lo, size_t hi)
{
int chot = a[lo + (hi - lo) / 2]; /* chốt là GIÁ TRỊ, không phải vị trí */
size_t i = lo;
size_t j = hi;
for (;;) {
while (a[i] < chot) ++i;
while (a[j] > chot) --j;
if (i >= j) return j;
doi_cho(&a[i], &a[j]);
++i;
--j;
}
}
void quick_hoare(int *a, size_t lo, size_t hi)
{
if (lo >= hi) return;
size_t p = hoare(a, lo, hi);
quick_hoare(a, lo, p); /* CHÚ Ý: p, không phải p-1 */
quick_hoare(a, p + 1, hi);
}| Lomuto | Hoare | |
|---|---|---|
| Số phép đổi chỗ trung bình | n/2 | n/6 |
| Dễ viết đúng | Có | Ba cái bẫy ở trên |
| Với mảng toàn phần tử bằng nhau | O(n bình phương) | O(n log n) |
| Chốt có ở đúng chỗ sau phân hoạch không | Có | Không |
| Dùng trong sách giáo khoa | Phổ biến | Ít |
| Dùng trong thư viện thật | Không | Có, hoặc biến thể |
ngau nhien: lomuto : 0.712 s, 12482910 phep doi cho hoare : 0.418 s, 4102847 phep doi cho nhanh hon 1.70 lan nhieu phan tu trung (10 gia tri khac nhau): lomuto : 41.208 s hoare : 0.284 s nhanh hon 145 lan
#Chọn chốt
| Cách chọn | Trường hợp xấu nhất xảy ra khi | Đánh giá |
|---|---|---|
| Phần tử đầu hoặc cuối | Mảng đã sắp hoặc sắp ngược | Rất tệ, vì dữ liệu đã sắp là chuyện thường ngày |
| Phần tử giữa | Mảng dạng răng cưa được dựng cố ý | Khá, xử lý được dữ liệu đã sắp |
| Trung vị của ba | Cần dữ liệu dựng rất cố ý | Tốt, và rẻ |
| Ngẫu nhiên | Xác suất rất nhỏ, không dựng cố ý được | Tốt, nhưng tốn một lời gọi rand mỗi lần |
| Trung vị thật sự | Không bao giờ | O(n) nhưng hằng số quá lớn, không đáng |
/* Trung vị của ba: sắp a[lo], a[giua], a[hi] rồi lấy cái giữa.
Đưa nó về vị trí hi để dùng với Lomuto. */
static void trung_vi_ba(int *a, size_t lo, size_t hi)
{
size_t giua = lo + (hi - lo) / 2;
if (a[giua] < a[lo]) doi_cho(&a[giua], &a[lo]);
if (a[hi] < a[lo]) doi_cho(&a[hi], &a[lo]);
if (a[hi] < a[giua]) doi_cho(&a[hi], &a[giua]);
/* Giờ a[lo] <= a[giua] <= a[hi], tức a[giua] là trung vị */
doi_cho(&a[giua], &a[hi]); /* đưa trung vị về cuối làm chốt */
}ngau nhien da sap sap nguoc rang cua chot cuoi 0.162 s SAP SAP 0.184 s chot giua 0.164 s 0.088 s 0.091 s SAP trung vi cua ba 0.158 s 0.084 s 0.086 s 0.171 s chot ngau nhien 0.181 s 0.094 s 0.095 s 0.178 s SAP = tran ngan xep do de quy sau 1000000 tang
#Chặn đệ quy quá sâu
#define NGUONG_NHO 24
void quick_sort(int *a, size_t lo, size_t hi)
{
while (lo < hi) {
/* 1. Đoạn nhỏ thì dùng insertion sort, xem Bài 27.3 */
if (hi - lo + 1 <= NGUONG_NHO) {
insertion_sort(a + lo, hi - lo + 1);
return;
}
trung_vi_ba(a, lo, hi);
size_t p = lomuto(a, lo, hi);
/* 2. Đệ quy vào nửa NHỎ HƠN, lặp trên nửa lớn.
Nhờ vậy độ sâu ngăn xếp không bao giờ vượt log2(n). */
if (p - lo < hi - p) {
if (p > lo) quick_sort(a, lo, p - 1);
lo = p + 1;
} else {
quick_sort(a, p + 1, hi);
if (p == lo) return;
hi = p - 1;
}
}
}Đoạn nhỏ chuyển sang insertion sort
Dưới khoảng 24 phần tử thì insertion sort nhanh hơn, vì hằng số nhỏ hơn và không có chi phí gọi hàm. Bài 27.3 đã đo.
Chỉ đệ quy vào nửa nhỏ hơn
Nửa nhỏ có nhiều nhất
n/2phần tử, nên mỗi tầng đệ quy ít nhất giảm một nửa. Độ sâu tối đa làlog2(n), tức 20 với một triệu phần tử.Lặp trên nửa lớn thay vì đệ quy
Vòng
whileở ngoài đóng vai lời gọi đệ quy thứ hai, mà không tốn khung ngăn xếp nào. Đây là tối ưu đệ quy đuôi làm bằng tay.
de quy ca hai nua, chot cuoi, du lieu da sap: do sau toi da: tran ngan xep tai tang 262143 de quy ca hai nua, trung vi cua ba, ngau nhien: do sau toi da: 44 chi de quy nua nho, trung vi cua ba, ngau nhien: do sau toi da: 20 chi de quy nua nho, chot cuoi, du lieu da sap: do sau toi da: 20 <- van an toan, chi cham
#Phân hoạch ba đường cho dữ liệu nhiều trùng
Với dữ liệu chỉ có vài giá trị khác nhau, cả Lomuto lẫn Hoare đều làm việc thừa: chúng tiếp tục phân hoạch những đoạn mà mọi phần tử đều bằng chốt.
/* Phân hoạch ba đường của Dijkstra, còn gọi là bài toán cờ Hà Lan.
Chia mảng thành BA phần: nhỏ hơn, bằng, lớn hơn chốt.
Trả về hai biên qua tham số ra. */
static void ba_duong(int *a, size_t lo, size_t hi,
size_t *ra_lt, size_t *ra_gt)
{
int chot = a[lo + (hi - lo) / 2];
size_t lt = lo; /* a[lo..lt) nhỏ hơn chốt */
size_t i = lo; /* a[lt..i) bằng chốt */
size_t gt = hi; /* a(gt..hi] lớn hơn chốt */
while (i <= gt) {
if (a[i] < chot) doi_cho(&a[lt++], &a[i++]);
else if (a[i] > chot) { doi_cho(&a[i], &a[gt]); if (gt == lo) break; --gt; }
else ++i;
}
*ra_lt = lt;
*ra_gt = gt;
}
void quick_ba_duong(int *a, size_t lo, size_t hi)
{
if (lo >= hi) return;
size_t lt, gt;
ba_duong(a, lo, hi, <, >);
if (lt > lo) quick_ba_duong(a, lo, lt - 1);
quick_ba_duong(a, gt + 1, hi);
}| Mảng | Sau phân hoạch ba đường với chốt 5 |
|---|---|
| 5 3 5 8 5 1 5 9 5 | 3 1 | 5 5 5 5 5 | 8 9 |
| 1 1 1 1 1 1 1 1 1 | | 1 1 1 1 1 1 1 1 1 | (không còn gì để đệ quy) |
so gia tri khac nhau trong mang:
2 gia tri 10 gia tri 1000 gia tri tat ca khac
lomuto SAP SAP 2.412 s 0.712 s
hoare 0.084 s 0.112 s 0.284 s 0.418 s
ba duong 0.021 s 0.048 s 0.198 s 0.442 sTự làm thử
- Cài
lomutovà kiểm vết của nó khớp với bảng trong bài trên mảng7 2 9 1 8 3 5. - Cài
hoare, thử với chốt làa[hi]và xác nhận nó lặp vô hạn với mảng đã sắp. - Đo thời gian của bốn cách chọn chốt trên dữ liệu ngẫu nhiên, đã sắp, sắp ngược và răng cưa.
- Cài bản chỉ đệ quy vào nửa nhỏ, đếm độ sâu ngăn xếp tối đa và xác nhận nó không vượt
log2(n). - Cài phân hoạch ba đường, đo trên mảng có 2, 10, 1000 và toàn giá trị khác nhau.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Quick sort phân hoạch quanh một chốt rồi đệ quy hai nửa, không cần bước gộp nào.
- Lomuto dễ viết nhưng đổi chỗ nhiều gấp ba Hoare và tệ hẳn với dữ liệu nhiều phần tử trùng.
- Chốt là phần tử đầu hoặc cuối biến dữ liệu đã sắp thành trường hợp xấu nhất. Dùng trung vị của ba.
- Chỉ đệ quy vào nửa nhỏ hơn và lặp trên nửa lớn thì độ sâu ngăn xếp không bao giờ vượt
log2(n). - Phân hoạch ba đường chậm hơn sáu phần trăm với dữ liệu thường nhưng nhanh hơn hàng chục lần khi có nhiều giá trị trùng.