Chương 02

Nhóm Thuật Toán
Sắp Xếp Cơ Bản Trên Mảng

Ba thuật toán nền tảng mà mọi lập trình viên cần thuần thục. Tuy đơn giản, chúng ẩn chứa nhiều kỹ thuật quan trọng và là nền tảng để hiểu các thuật toán nâng cao hơn.

📥 Sắp xếp chèn (Insertion Sort)

Ý tưởng: Giống cách bạn sắp xếp bài trên tay — lấy từng lá bài mới và chèn vào đúng vị trí trong phần đã sắp xếp.

🧠 Bản chất thuật toán
  1. Xét phần tử thứ i (gọi là key)
  2. So sánh key với các phần tử bên trái (phần đã sắp xếp)
  3. Dịch các phần tử lớn hơn key sang phải một vị trí
  4. Chèn key vào vị trí trống — lặp lại từ i = 1 đến n-1
⚡ C++ — Insertion Sort
#include <bits/stdc++.h>
using namespace std;

void insertionSort(vector<int>& a) {
    int n = a.size();
    for (int i = 1; i < n; i++) {
        int key = a[i];
        int j = i - 1;

        // Dịch các phần tử > key sang phải
        while (j >= 0 && a[j] > key) {
            a[j + 1] = a[j];
            j--;
        }
        a[j + 1] = key;  // Chèn key vào vị trí đúng
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; i++) cin >> a[i];

    insertionSort(a);

    for (int x : a) cout << x << " ";
    return 0;
}
Ưu điểm
  • Simple, dễ code
  • Stable — giữ thứ tự phần tử bằng nhau
  • In-place — O(1) bộ nhớ phụ
  • Best case O(n) khi mảng gần sắp xếp
  • Hiệu quả với mảng nhỏ (n ≤ 1000)
Nhược điểm
  • Worst/Average: O(n²)
  • Nhiều phép dịch chuyển (shifts)
  • Không phù hợp mảng lớn

🎯 Sắp xếp chọn (Selection Sort)

Ý tưởng: Lặp đi lặp lại: tìm phần tử nhỏ nhất trong phần chưa sắp xếp, rồi hoán đổi nó về đầu phần chưa sắp xếp.

🧠 Bản chất thuật toán
  1. Duyệt từ vị trí i = 0 đến n-2
  2. Tìm vị trí minIdx của phần tử nhỏ nhất trong đoạn [i..n-1]
  3. Hoán đổi (swap) a[i] với a[minIdx]
  4. Phần tử tại vị trí i giờ đã đúng — tiếp tục với i+1
⚡ C++ — Selection Sort
#include <bits/stdc++.h>
using namespace std;

void selectionSort(vector<int>& a) {
    int n = a.size();
    for (int i = 0; i < n - 1; i++) {
        int minIdx = i;

        // Tìm phần tử nhỏ nhất trong [i+1..n-1]
        for (int j = i + 1; j < n; j++) {
            if (a[j] < a[minIdx]) {
                minIdx = j;
            }
        }

        // Hoán đổi (đây là lý do UNSTABLE!)
        if (minIdx != i) {
            swap(a[i], a[minIdx]);
        }
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; i++) cin >> a[i];

    selectionSort(a);

    for (int x : a) cout << x << " ";
    return 0;
}
❌ Selection Sort là UNSTABLE

Phép swap có thể nhảy qua phần tử bằng nhau → thứ tự tương đối bị phá vỡ. Ví dụ: [5a, 3, 5b, 1] → sau swap: [1, 3, 5b, 5a] — 5a và 5b đổi chỗ!

Ưu điểm
  • Ít phép hoán đổi: tối đa n-1 swaps
  • In-place, O(1) bộ nhớ phụ
  • Dễ hiểu, dễ code
Nhược điểm
  • Luôn O(n²) — cả best/worst/average
  • Unstable
  • Không thể tối ưu thêm

🔢 Sắp xếp đếm phân phối (Counting Sort)

Ý tưởng: Không so sánh trực tiếp! Đếm số lần xuất hiện của mỗi giá trị, rồi "rải" chúng ra theo thứ tự. Đây là "vũ khí bí mật" O(n) khi values có giới hạn nhỏ.

🧠 Bản chất thuật toán
  1. Tìm giá trị lớn nhất maxVal trong mảng
  2. Tạo mảng đếm count[0..maxVal] khởi tạo = 0
  3. Duyệt mảng gốc: count[a[i]]++
  4. Duyệt mảng đếm: ghi lại các giá trị lần lượt vào mảng kết quả
⚡ C++ — Counting Sort
#include <bits/stdc++.h>
using namespace std;

void countingSort(vector<int>& a) {
    if (a.empty()) return;

    int maxVal = *max_element(a.begin(), a.end());
    vector<int> count(maxVal + 1, 0);

    // Bước 1: Đếm tần suất
    for (int x : a) {
        count[x]++;
    }

    // Bước 2: Ghi lại theo thứ tự
    int idx = 0;
    for (int val = 0; val <= maxVal; val++) {
        while (count[val] > 0) {
            a[idx++] = val;
            count[val]--;
        }
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; i++) cin >> a[i];

    countingSort(a);

    for (int x : a) cout << x << " ";
    return 0;
}
💡 Khi nào dùng Counting Sort?

Khi đề bài nói "giá trị ≤ 10⁵" hoặc "điểm từ 0 đến 100" — đó là dấu hiệu cho Counting Sort. Nếu maxVal quá lớn (>10⁷), mảng đếm sẽ quá tốn bộ nhớ!

Thuộc tínhGiá trị
Thời gianO(n + k), k = maxVal
Không gianO(k) — mảng đếm
Stable✅ Có (phiên bản chuẩn)
So sánhKhông cần so sánh!

⚔️ So sánh & Nhận diện dấu hiệu áp dụng

Thuật toánBestAverageWorstSpaceStableDấu hiệu đề bài
Insertion Sort O(n)O(n²)O(n²) O(1) N nhỏ, mảng gần sorted, đếm shifts
Selection Sort O(n²)O(n²)O(n²) O(1) N nhỏ, hỏi số swaps, minh chứng unstable
Counting Sort O(n+k)O(n+k)O(n+k) O(k) Giá trị giới hạn nhỏ, sắp xếp điểm/tuổi
📝 Quy tắc chọn nhanh

• N ≤ 10³ → Insertion / Selection đều được
• N lớn + giá trị nhỏ → Counting Sort
• Cần stable → Insertion Sort hoặc Counting Sort
• Đếm shifts → Insertion Sort | Đếm swaps → Selection Sort