Nhập môn

Sắp xếp chọn (Selection Sort): giải thích từng bước, so sánh với Insertion Sort và code C++, Python

Sắp xếp chọn (Selection Sort) chia mảng thành hai phần: đoạn đầu đã sắp xếp và đoạn cuối chưa sắp xếp. Ở mỗi lượt thứ `i`, thuật toán quét qua toàn bộ đoạn chưa sắp xếp `a[i .. n - 1]` để tìm vị trí `min_idx` của phần tử nhỏ nhất, rồi hoán đổi một lần duy nhất với `a[i]`. Nhờ chỉ đổi chỗ tối đa `n - 1` lần, Selection Sort là thuật toán tiết kiệm thao tác ghi bộ nhớ nhất trong nhóm cơ bản `O(n²)`.

Ý chính
  • Ở mỗi lượt thứ i (từ 0 đến n - 2), tìm vị trí min_idx của phần tử nhỏ nhất trong đoạn a[i .. n - 1], rồi gọi swap(a[i], a[min_idx]) đúng 1 lần ở cuối lượt.
  • Số lần hoán đổi (swap) tối đa của Selection Sort chỉ là n - 1 lần (O(n)), ít nhất trong các thuật toán sắp xếp cơ bản.
  • Số phép so sánh luôn cố định là n(n - 1) / 2 (O(n²)) trong mọi trường hợp, kể cả khi mảng ban đầu đã tăng dần sẵn.
  • Selection Sort cơ bản không ổn định (unstable): phép hoán đổi nhảy cóc từ đầu đoạn tới min_idx có thể làm đảo thứ tự hai phần tử bằng nhau.

Ý tưởng cốt lõi: Tìm phần tử nhỏ nhất và đặt thẳng vào vị trí đích

Khi xếp một dãy số trên bảng theo thứ tự tăng dần từ trái sang phải, cách làm tự nhiên nhất thường không phải là đổi chỗ hai số cạnh nhau như Sắp xếp nổi bọt (Bubble Sort), mà là:

  1. Nhìn lướt qua toàn bộ dãy số để tìm số nhỏ nhất, rồi đưa nó về vị trí đầu tiên (a[0]).
  2. Bỏ qua vị trí đầu tiên đã xong, nhìn lướt qua các số còn lại từ vị trí 1 đến cuối để tìm số nhỏ nhất tiếp theo, rồi đưa nó về vị trí thứ hai (a[1]).
  3. Lặp lại quy trình “chọn số nhỏ nhất trong phần còn lại” cho tới khi chỉ còn 1 số cuối cùng ở vị trí n - 1.

Đó chính xác là nguyên lý của Sắp xếp chọn (Selection Sort). Tại đầu mỗi lượt thứ i (với i chạy từ 0 đến n - 2), mảng luôn được tách thành hai vùng rõ rệt:

  • Vùng bên trái a[0 .. i - 1]: chứa i phần tử nhỏ nhất của toàn mảng, đã nằm cố định ở vị trí cuối cùng.
  • Vùng bên phải a[i .. n - 1]: đoạn chưa sắp xếp. Ta dùng một vòng lặp tìm kiếm tuyến tính cho j chạy từ i + 1 đến n - 1 để tìm chỉ số min_idx của phần tử nhỏ nhất trong vùng này, rồi đổi chỗ a[i] với a[min_idx].

Chạy tay từng bước trên mảng [64, 25, 12, 22, 11]

Hãy theo dõi từng lượt của Selection Sort trên mảng 5 phần tử a = [64, 25, 12, 22, 11]. Vì n = 5, vòng lặp ngoài chỉ cần chạy n - 1 = 4 lượt (i = 0, 1, 2, 3):

a[0] a[1] a[2] a[3] a[4] Ban đầu 64 25 12 22 11 min[0..4] = 11 Sau lượt 1 11 25 12 22 64 min[1..4] = 12 Sau lượt 2 11 12 25 22 64 min[2..4] = 22 Sau lượt 3 11 12 22 25 64 min[3..4] = 25 Sau lượt 4 11 12 22 25 64 tổng 3 lần swap
Ban đầu: tìm min[0..4] = 11 tại a[4] 64 25 12 22 11 Sau lượt 1: đổi (64, 11), min[1..4] = 12 11 25 12 22 64 Sau lượt 2: đổi (25, 12), min[2..4] = 22 11 12 25 22 64 Sau lượt 3: đổi (25, 22), min[3..4] = 25 11 12 22 25 64 Sau lượt 4: 25 đã đứng đúng chỗ -> Hoàn tất 11 12 22 25 64
Hình 1. Bốn lượt chạy của Selection Sort trên mảng [64, 25, 12, 22, 11]: ô cam là phần tử nhỏ nhất tìm được trong đoạn chưa xét, ô xanh lá là các phần tử đã cố định.
Lượt thứ i Đoạn đang xét a[i .. 4] Phần tử nhỏ nhất tìm được Phép hoán đổi cuối lượt Trạng thái mảng sau lượt i
Ban đầu (i = 0) [64, 25, 12, 22, 11] a[4] = 11 (min_idx = 4) Đổi a[0] (64) và a[4] (11) [11 | 25, 12, 22, 64]
i = 1 [25, 12, 22, 64] a[2] = 12 (min_idx = 2) Đổi a[1] (25) và a[2] (12) [11, 12 | 25, 22, 64]
i = 2 [25, 22, 64] a[3] = 22 (min_idx = 3) Đổi a[2] (25) và a[3] (22) [11, 12, 22 | 25, 64]
i = 3 [25, 64] a[3] = 25 (min_idx = 3) Không đổi (vì min_idx == i) [11, 12, 22, 25, 64]

Quan sát cột “Phép hoán đổi cuối lượt”, bạn sẽ thấy điểm đặc biệt nhất của thuật toán này: suốt cả quá trình sắp xếp 5 phần tử, dù thực hiện 4 + 3 + 2 + 1 = 10 phép so sánh, chương trình chỉ thực hiện tổng cộng 3 lần đổi chỗ (swap).

Code Sắp xếp chọn (Selection Sort) bằng C++ và Python

Khi cài đặt Selection Sort, trong vòng lặp trong j ta chỉ cập nhật chỉ số min_idx = j chứ tuyệt đối không gọi swap bên trong vòng lặp j. Chỉ khi vòng lặp j kết thúc và nếu min_idx != i, ta mới hoán đổi một lần duy nhất:

#include <iostream>
#include <utility>
#include <vector>
using namespace std;

// Sắp xếp chọn (Selection Sort): trả về số lần hoán đổi thực sự (khi min_idx != i)
int selection_sort(vector<int>& a) {
    int n = (int)a.size();
    int so_lan_swap = 0;
    for (int i = 0; i < n - 1; ++i) {
        int min_idx = i;
        for (int j = i + 1; j < n; ++j) {
            if (a[j] < a[min_idx]) {
                min_idx = j;
            }
        }
        if (min_idx != i) {
            swap(a[i], a[min_idx]);
            ++so_lan_swap;
        }
    }
    return so_lan_swap;
}

void in_ket_qua(const vector<int>& a, int so_lan_swap) {
    cout << "Swaps: " << so_lan_swap << " | Mang: ";
    for (int i = 0; i < (int)a.size(); ++i) {
        cout << a[i] << (i + 1 == (int)a.size() ? "" : " ");
    }
    cout << "\n";
}

int main() {
    // Test 1: Mảng ví dụ trong bài [64, 25, 12, 22, 11]
    vector<int> a1 = {64, 25, 12, 22, 11};
    int s1 = selection_sort(a1);
    in_ket_qua(a1, s1); // Swaps: 3 | Mang: 11 12 22 25 64

    // Test 2: Mảng đã tăng dần sẵn -> 0 lần swap (nhưng vẫn chạy đủ phép so sánh)
    vector<int> a2 = {3, 7, 11, 15};
    int s2 = selection_sort(a2);
    in_ket_qua(a2, s2); // Swaps: 0 | Mang: 3 7 11 15

    // Test 3: Mảng có phần tử âm và trùng nhau
    vector<int> a3 = {5, -3, 5, 0, -8};
    int s3 = selection_sort(a3);
    in_ket_qua(a3, s3); // Swaps: 2 | Mang: -8 -3 0 5 5

    return 0;
}
Học cùng giảng viên

Chủ đề này nằm trong Lecture 11: Sorting (Thuật toán Sắp xếp) của Khóa Green. Xem đề cương

Ưu điểm và nhược điểm đặc trưng của Selection Sort

1. Ưu điểm độc nhất: Tối thiểu hóa số lần ghi vào bộ nhớ (O(n) lần swap)

Trong cả Bubble Sort lẫn Insertion Sort, một phần tử muốn di chuyển về đúng chỗ phải trượt qua từng ô một, nên trong trường hợp xấu nhất chúng cần tới O(n²) phép ghi vào mảng.

Ngược lại, Selection Sort đưa thẳng phần tử nhỏ nhất về đích chỉ bằng 1 lần hoán đổi mỗi lượt. Với mảng n phần tử, tổng số lần gọi swap không bao giờ vượt quá n - 1 lần (tối đa 2(n - 1) lần ghi vào mảng). Tính chất này rất có lợi khi làm việc trên bộ nhớ Flash, EEPROM hoặc khi các phần tử là bản ghi lớn mà chi phí sao chép đắt hơn nhiều lần chi phí so sánh khóa.

Đoạn chương trình dưới đây đếm trực tiếp số lần ghi vào mảng a[...] của cả 3 thuật toán trên cùng bộ dữ liệu:

#include <iostream>
#include <utility>
#include <vector>
using namespace std;

// Đếm số phép ghi vào mảng a[...] của Selection Sort (mỗi lần swap ghi 2 ô nhớ)
int ghi_selection(vector<int> a) {
    int n = (int)a.size(), ghi = 0;
    for (int i = 0; i < n - 1; ++i) {
        int min_idx = i;
        for (int j = i + 1; j < n; ++j) {
            if (a[j] < a[min_idx]) min_idx = j;
        }
        if (min_idx != i) {
            swap(a[i], a[min_idx]);
            ghi += 2;
        }
    }
    return ghi;
}

// Đếm số phép ghi vào mảng a[...] của Insertion Sort
int ghi_insertion(vector<int> a) {
    int n = (int)a.size(), ghi = 0;
    for (int i = 1; i < n; ++i) {
        int key = a[i], j = i - 1;
        while (j >= 0 && a[j] > key) {
            a[j + 1] = a[j];
            --j;
            ++ghi;
        }
        if (j + 1 != i) {
            a[j + 1] = key;
            ++ghi;
        }
    }
    return ghi;
}

// Đếm số phép ghi vào mảng a[...] của Bubble Sort (mỗi lần swap ghi 2 ô nhớ)
int ghi_bubble(vector<int> a) {
    int n = (int)a.size(), ghi = 0;
    for (int i = 0; i < n - 1; ++i) {
        bool swapped = false;
        for (int j = 0; j < n - 1 - i; ++j) {
            if (a[j] > a[j + 1]) {
                swap(a[j], a[j + 1]);
                ghi += 2;
                swapped = true;
            }
        }
        if (!swapped) break;
    }
    return ghi;
}

int main() {
    vector<vector<int>> cac_mang = {
        {64, 25, 12, 22, 11},  // Mảng ví dụ 5 phần tử
        {6, 5, 4, 3, 2, 1},    // Mảng giảm dần 6 phần tử
        {1, 2, 3, 4, 5, 6}     // Mảng đã tăng dần
    };

    for (const auto& a : cac_mang) {
        cout << "Selection: " << ghi_selection(a)
             << " | Insertion: " << ghi_insertion(a)
             << " | Bubble: " << ghi_bubble(a) << "\n";
    }
    return 0;
}

Khi chạy với mảng giảm dần 6 phần tử [6, 5, 4, 3, 2, 1], Bubble Sort phải ghi vào mảng 30 lần, Insertion Sort ghi 20 lần, trong khi Selection Sort chỉ ghi đúng 6 lần (tương ứng 3 phép đổi chỗ (6, 1), (5, 2) và (4, 3)).

2. Nhược điểm 1: Luôn tốn cố định n(n - 1) / 2 phép so sánh

Để khẳng định một phần tử là nhỏ nhất trong đoạn a[i .. n - 1], vòng lặp trong bắt buộc phải xem xét hết mọi phần tử từ i + 1 đến n - 1 mà không có cách nào dừng sớm. Ngay cả khi truyền vào một mảng đã tăng dần hoàn hảo [1, 2, 3, ..., n], Selection Sort vẫn chạy đủ n(n - 1) / 2 phép so sánh (O(n²)), trong khi Insertion Sort và Bubble Sort (có cờ swapped) đã dừng lại chỉ sau n - 1 phép so sánh (O(n)).

3. Nhược điểm 2: Không ổn định (Unstable Sort) — Phản ví dụ [4a, 4b, 1]

Một thuật toán sắp xếp được gọi là ổn định (stable) nếu hai phần tử bằng nhau vẫn giữ nguyên thứ tự đứng trước – đứng sau như ban đầu. Selection Sort không ổn định vì phép swap(a[i], a[min_idx]) ở cuối lượt là một cú nhảy xa qua đầu các phần tử ở giữa.

Xét phản ví dụ gồm 3 phần tử a = [4a, 4b, 1] (trong đó 4a và 4b cùng bằng 4, nhưng 4a đứng trước 4b):

  1. Ở lượt i = 0, phần tử nhỏ nhất trong [4a, 4b, 1] là a[2] = 1 (min_idx = 2).
  2. Thuật toán đổi chỗ a[0] (4a) với a[2] (1), mảng trở thành [1, 4b, 4a].
  3. Lúc này 4a đã bị đẩy ra đứng sau 4b — thứ tự ban đầu của hai số 4 đã bị đảo ngược!

Đặt cạnh nhau 3 thuật toán cơ bản: Selection, Insertion và Bubble Sort

Tiêu chí so sánh Selection Sort (Sắp xếp chọn) Insertion Sort (Sắp xếp chèn) Bubble Sort (Sắp xếp nổi bọt)
Cơ chế chính ở bước i Chọn min trong a[i..n-1], đổi về a[i] Lấy key = a[i], dịch các số lớn hơn trong a[0..i-1] sang phải Đổi chỗ các cặp kề nhau ngược thứ tự, đẩy max về a[n-1-i]
Số phép so sánh (Tốt nhất) n(n - 1) / 2 — O(n²) n - 1 — O(n) n - 1 — O(n) (có cờ swapped)
Số phép so sánh (Xấu nhất) n(n - 1) / 2 — O(n²) n(n - 1) / 2 — O(n²) n(n - 1) / 2 — O(n²)
Số lần swap / dịch (Xấu nhất) Tối đa n - 1 lần swap — O(n) n(n - 1) / 2 lần dịch — O(n²) n(n - 1) / 2 lần swap — O(n²)
Tính ổn định (Stable) Không ổn định (Unstable) Ổn định (Stable) Ổn định (Stable)
Khi nào nên chọn? Chi phí ghi bộ nhớ rất đắt hoặc cần k phần tử nhỏ nhất Mảng nhỏ, mảng gần như đã sắp xếp, dữ liệu đến liên tục Kiểm tra mảng đã sắp xếp hoặc mô phỏng đổi chỗ kề nhau

Lỗi hay gặp khi cài đặt Selection Sort

Cẩn thận
  • Gọi swap(a[i], a[j]) ngay bên trong vòng lặp trong j. Nếu mỗi lần thấy a[j] nhỏ hơn a[i] bạn lại đổi chỗ ngay thay vì chỉ cập nhật biến chỉ số min_idx = j, số lần swap sẽ vọt từ O(n) lên O(n²).
  • Chỉ lưu giá trị min_val mà quên lưu chỉ số min_idx. Nếu không biết phần tử nhỏ nhất nằm ở ô nào để hoán đổi mà chỉ gán đè a[i] = min_val, giá trị cũ tại a[i] sẽ bị mất. Chỉ cần lưu duy nhất chỉ số min_idx, giá trị nhỏ nhất chính là a[min_idx].
  • Cập nhật min_idx cả khi a[j] == a[min_idx]. Việc dời min_idx sang phần tử bằng nó ở xa hơn là không cần thiết và làm phát sinh phép swap thừa.

Luyện ngay trên Big-O Coding

Thực hành ngay các bài toán chọn phần tử tối ưu và sắp xếp cơ bản trên hệ thống Big-O Coding:

Câu hỏi thường gặp

Có cách nào biến Selection Sort thành thuật toán sắp xếp ổn định (stable) không?

Có. Sau khi tìm được min_idx, thay vì gọi swap(a[i], a[min_idx]), bạn lưu key = a[min_idx], dịch các phần tử từ a[i] đến a[min_idx - 1] sang phải 1 ô rồi gán a[i] = key. Tuy nhiên làm vậy sẽ khiến số phép ghi tăng lên O(n²), lúc đó dùng thẳng Insertion Sort sẽ gọn hơn.

Làm sao nâng cấp Selection Sort từ O(n²) lên O(n log n)?

Nếu thay vòng lặp tìm min_idx tuyến tính O(n) ở mỗi lượt bằng cấu trúc Đống nhị phân (Binary Heap) để lấy phần tử cực trị trong O(log n), thuật toán sẽ trở thành Sắp xếp vun đống (Heap Sort) với độ phức tạp O(n log n).

Nếu chỉ cần tìm 3 phần tử nhỏ nhất trong mảng 1.000 phần tử thì làm thế nào?

Bạn chỉ cần cho vòng lặp ngoài i của Selection Sort chạy đúng 3 lượt (i = 0, 1, 2) rồi dừng lại! Khi đó 3 phần tử nhỏ nhất nằm ở a[0], a[1], a[2] chỉ sau O(3n) = O(n) phép so sánh, nhanh hơn cả việc sắp xếp toàn mảng bằng Quick Sort hay Merge Sort (O(n log n)).

Muốn học bài bản cùng giảng viên?

Khóa Green cho người mới, sinh viên năm 1–2: xây nền tảng lập trình và thuật toán vững chắc, biết dùng AI hỗ trợ. 16 buổi online, học thử buổi 1 miễn phí.

Xem Khóa Green

Nguồn tham khảo

Chia sẻ: Facebook LinkedIn
Nhắn Zalo tư vấnZalo Gọi 0937 401 483