Bỏ qua điều hướng, tới nội dung chính
Học C
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ượtPhần đã sắpPhần chưa sắpNhỏ nhấtSau khi đổi
064 25 12 22 111111 | 25 12 22 64
11125 12 22 641211 12 | 25 22 64
211 1225 22 642211 12 22 | 25 64
311 12 2225 642511 12 22 25 | 64

#Ít hoán đổi nhất

Thuật toánSố so sánhSố hoán đổi hoặc gán
Selection sortn(n-1)/2 luôn luônNhiều nhất n - 1
Bubble sortTới n(n-1)/2Tới n(n-1)/2
Insertion sortTới n(n-1)/2Tới n(n-1)/2 phép dịch
Quick sortKhoảng 1.39 n log2 nKhoảng n log2 n / 3
Heap sortKhoảng 2 n log2 nKhoả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ấtTrung bìnhXấu nhất
Selection sortO(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 sortO(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 hoiVì sao selection thắng
Phép ghi rất đắt so với phép đọcVí 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ố địnhHệ 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ử

  1. 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.
  2. Chạy trên mảng 4a 2 3 4b 1 và xác nhận thứ tự hai số 4 bị đảo.
  3. Đ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.
  4. Cài k_nho_nhat và so với heap kích thước k ở Bài 23.4 với k bằng 1, 10, 100 và 1000.
  5. 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)/2 phé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 - 1 phé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.