Bài 27.218 phút đọc
Selection sort
Sau bài này bạn sẽ làm được
- Cài selection sort đúng
- Giải thích vì sao số phép hoán đổi luôn nhỏ hơn n
- Chỉ ra ví dụ làm mất tính ổn định
- Nêu tình huống hiếm hoi nó phù hợp
Selection sort tìm phần tử nhỏ nhất rồi đưa nó về đầu, lặp lại cho phần còn lại. Nó luôn tốn O(n bình phương) phép so sánh, không có trường hợp tốt, nhưng số phép hoán đổi thì ít nhất trong mọi thuật toán so sánh.
#Ý tưởng
Selection sort
Chia mảng thành hai phần: phần đầu đã sắp và đã đúng chỗ vĩnh viễn, phần sau chưa sắp. Mỗi lượt tìm phần tử nhỏ nhất trong phần chưa sắp và đổi chỗ nó về đầu phần đó.
selection.c
#include <stddef.h>
static void doi_cho(int *a, int *b) { int t = *a; *a = *b; *b = t; }
void selection_sort(int *a, size_t n)
{
for (size_t i = 0; i + 1 < n; ++i) {
size_t nho = i;
for (size_t j = i + 1; j < n; ++j)
if (a[j] < a[nho])
nho = j;
if (nho != i) /* tránh đổi chỗ vô ích */
doi_cho(&a[i], &a[nho]);
}
}| Lượt | Phần đã sắp | Phần chưa sắp | Nhỏ nhất | Sau khi đổi |
|---|---|---|---|---|
| 0 | 64 25 12 22 11 | 11 | 11 | 25 12 22 64 | |
| 1 | 11 | 25 12 22 64 | 12 | 11 12 | 25 22 64 |
| 2 | 11 12 | 25 22 64 | 22 | 11 12 22 | 25 64 |
| 3 | 11 12 22 | 25 64 | 25 | 11 12 22 25 | 64 |
#Ít hoán đổi nhất
| Thuật toán | Số so sánh | Số hoán đổi hoặc gán |
|---|---|---|
| Selection sort | n(n-1)/2 luôn luôn | Nhiều nhất n - 1 |
| Bubble sort | Tới n(n-1)/2 | Tới n(n-1)/2 |
| Insertion sort | Tới n(n-1)/2 | Tới n(n-1)/2 phép dịch |
| Quick sort | Khoảng 1.39 n log2 n | Khoảng n log2 n / 3 |
| Heap sort | Khoảng 2 n log2 n | Khoảng n log2 n |
terminal
./dem-hoan-doi 1000
n = 1000, du lieu ngau nhien
so sanh hoan doi
selection 499500 997
bubble 498012 249481
insertion 249735 248736#Vì sao nó không ổn định
khong-on-dinh.c
/* Mảng: 4a 2 3 4b 1
Chữ a và b để phân biệt hai số 4 giống nhau.
Lượt 0: nhỏ nhất là 1 ở chỉ số 4. Đổi chỗ a[0] với a[4]:
1 2 3 4b 4a <- 4a NHẢY XUỐNG SAU 4b
Lượt 1: nhỏ nhất trong 2 3 4b 4a là 2, đã ở chỗ, không đổi.
Lượt 2: nhỏ nhất trong 3 4b 4a là 3, không đổi.
Lượt 3: nhỏ nhất trong 4b 4a là 4b, không đổi.
Kết quả: 1 2 3 4b 4a
Thứ tự của 4a và 4b đã bị ĐẢO so với ban đầu. */terminal
./thu-on-dinh
truoc: 4a 2 3 4b 1 sau : 1 2 3 4b 4a <- 4a va 4b da doi cho, KHONG on dinh
#Không có trường hợp tốt
dem.c
/* Vòng trong LUÔN chạy đủ, bất kể dữ liệu:
for (size_t j = i + 1; j < n; ++j)
if (a[j] < a[nho]) nho = j;
Không có cách nào biết sớm rằng phần còn lại đã sắp,
vì phải xem hết mới biết cái nào nhỏ nhất.
Tổng số phép so sánh, với MỌI dữ liệu vào:
(n-1) + (n-2) + ... + 1 = n(n-1)/2 */terminal
./dem-selection 1000
n = 1000, ly thuyet: 499500 so sanh cho MOI truong hop du lieu so sanh hoan doi ngau nhien 499500 997 da sap 499500 0 sap nguoc 499500 500 nhieu trung 499500 953
| Tốt nhất | Trung bình | Xấu nhất | |
|---|---|---|---|
| Selection sort | O(n bình phương) | O(n bình phương) | O(n bình phương) |
| Bubble sort có cờ | O(n) | O(n bình phương) | O(n bình phương) |
| Insertion sort | O(n) | O(n bình phương) | O(n bình phương) |
#Khi nào nó phù hợp
| Tình huống hiếm hoi | Vì sao selection thắng |
|---|---|
| Phép ghi rất đắt so với phép đọc | Ví dụ bộ nhớ flash có số lần ghi giới hạn. Selection ghi nhiều nhất n-1 lần. |
| Chỉ cần k phần tử nhỏ nhất, k rất nhỏ | Dừng sau k lượt là xong, tốn O(kn). Nhưng heap kích thước k ở Bài 23.4 vẫn tốt hơn với k lớn. |
| Cần bảo đảm số phép ghi cố định | Hệ thống thời gian thực đôi khi cần điều đó hơn là tốc độ trung bình. |
k-nho-nhat.c
/* Chỉ cần k phần tử nhỏ nhất, không cần sắp cả mảng. */
void k_nho_nhat(int *a, size_t n, size_t k)
{
if (k > n) k = n;
for (size_t i = 0; i < k; ++i) { /* dừng sau k lượt */
size_t nho = i;
for (size_t j = i + 1; j < n; ++j)
if (a[j] < a[nho]) nho = j;
if (nho != i) doi_cho(&a[i], &a[nho]);
}
/* a[0..k) giờ là k phần tử nhỏ nhất, đã sắp. */
}terminal
./do-k-nho-nhat 1000000
n = 1000000 k selection cat ngan heap kich thuoc k qsort ca mang 1 0.412 ms 2.104 ms 162.0 ms 5 2.061 ms 2.210 ms 162.0 ms 10 4.120 ms 2.318 ms 162.0 ms 50 20.600 ms 2.712 ms 162.0 ms 1000 412.000 ms 3.914 ms 162.0 ms
Một biến thể có ích: sắp hai đầu cùng lúc
selection-hai-dau.c
/* Mỗi lượt tìm CẢ nhỏ nhất lẫn lớn nhất, đưa về hai đầu.
Số lượt giảm còn một nửa, dù tổng số phép so sánh gần như không đổi. */
void selection_hai_dau(int *a, size_t n)
{
if (n < 2) return;
size_t trai = 0, phai = n - 1;
while (trai < phai) {
size_t nho = trai, lon = trai;
for (size_t j = trai; j <= phai; ++j) {
if (a[j] < a[nho]) nho = j;
if (a[j] > a[lon]) lon = j;
}
doi_cho(&a[trai], &a[nho]);
/* Nếu phần tử lớn nhất vừa bị đẩy khỏi vị trí lon bởi phép đổi trên */
if (lon == trai) lon = nho;
doi_cho(&a[phai], &a[lon]);
++trai;
--phai;
}
}Tự làm thử
- Cài selection sort và đếm số phép so sánh với bốn loại dữ liệu, xác nhận số so sánh luôn là
n(n-1)/2. - Chạy trên mảng
4a 2 3 4b 1và xác nhận thứ tự hai số 4 bị đảo. - Đo thời gian selection và insertion trên mảng năm mươi nghìn phần tử đã sắp có một trăm phần tử bị đổi ngẫu nhiên.
- Cài
k_nho_nhatvà so với heap kích thướckở Bài 23.4 vớikbằng 1, 10, 100 và 1000. - Cài selection hai đầu, bỏ dòng sửa
lon, rồi tìm một mảng năm phần tử làm nó cho kết quả sai.
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
- Selection sort luôn tốn đúng
n(n-1)/2phép so sánh, không có trường hợp tốt nào. - Đổi lại nó chỉ tốn nhiều nhất
n - 1phép hoán đổi, ít nhất trong mọi thuật toán so sánh. - Nó không ổn định vì phép đổi chỗ xa có thể ném một phần tử qua bên kia một phần tử bằng nó.
- Với dữ liệu gần sắp, insertion sort nhanh hơn hàng trăm lần vì selection sort không nhận ra được điều đó.
- Ưu thế ít hoán đổi biến mất khi bạn sắp mảng con trỏ, nên trong thực tế gần như không bao giờ nên dùng selection sort.