Chuyển tới nội dung chính

CHƯƠNG 2: CÁC THUẬT TOÁN TÌM KIẾM VÀ SẮP XẾP

Chương này làm trên mảng (a[0..n-1]). DSLK sắp xếp bằng hoán đổi data — xem Ch.3 SapXep (đổi chỗ trực tiếp).
Ch.1 đã đếm O(n)O(n) / O(logn)O(\log n)n(n1)/2n(n-1)/2. Ở đây cài đủ, chạy tay, rồi mới nói OO.


🎯 MỤC TIÊU CHƯƠNG 2

Sau khi học xong chương này, sinh viên có thể:

  1. Phát biểu bài toán tìm kiếm: vào mảng + khóa, ra chỉ số hoặc 1-1.
  2. Cài ba giải thuật tìm (khớp giáo trình/đề thực hành Phần 1): tuần tự, nhị phân, nội suy; đếm so sánh; nêu tốt / xấu.
  3. Phát biểu bài toán sắp xếp; phân biệt ổn định, tại chỗ, tăng / giảm.
  4. Cài bốn giải thuật đơn giản: đổi chỗ trực tiếp, chọn trực tiếp, chèn trực tiếp, nổi bọt — không nhầm Interchange với Selection.
  5. Cài bốn giải thuật nhanh: trộn, nhanh, vun đống, cơ số; nêu OO tốt / TB / xấu và bộ nhớ phụ.
  6. Chọn thuật toán theo nn, dữ liệu đã gần sort hay chưa, có cần ổn định không.

📋 NỘI DUNG CHƯƠNG 2

2.1. Bài toán tìm kiếm
2.1.1. Khái niệm
2.1.2. Tìm kiếm tuần tự
2.1.3. Tìm kiếm nhị phân
2.1.4. Tìm kiếm nội suy ← giáo trình / đề thực hành Phần 1 (bắt buộc)
2.2. Bài toán sắp xếp
2.2.1. Khái niệm
2.2.2. Các thuật toán sắp xếp đơn giản
a. Sắp xếp đổi chỗ trực tiếp (Interchange)
b. Sắp xếp chọn trực tiếp (Selection)
c. Sắp xếp chèn trực tiếp (Insertion)
d. Sắp xếp nổi bọt (Bubble)
2.2.3. Các thuật toán sắp xếp nhanh
a. Sắp xếp trộn (Merge)
b. Sắp xếp nhanh (Quick)
c. Sắp xếp vun đống (Heap)
d. Sắp xếp theo cơ số (Radix)

Cài đặt: C++ (iostream), tên hàm khớp thực hành / đề: TimTuyenTinh, TimNhiPhan, TimNoiSuy, InterchangeSort, SelectionSort, InsertionSort, BubbleSort, QuickSort + partition, MergeSort, HeapSort, RadixSort.
Ảnh đề cương lý thuyết 2.1 dừng ở nhị phân. Giáo trình thực hành Phần 1 và đề thi bắt đủ 3 phương pháp tìm — mục 2.1.4 nội suy soạn đầy đủ, không bỏ.


📖 CÁCH ĐỌC (ZERO → HERO)

MốcĐọcXong khi
Zero2.1.1 + 2.2.1Nói được “tìm = trả chỉ số”; “sắp = đưa về thứ tự khóa”
Đề thi tìm2.1.2 + 2.1.3 + 2.1.4Viết ba hàm; bảng L,R,ML,R,M; công thức pos nội suy
Đề thi sort đơn giản2.2.2 cả bốnChạy tay n=4n=4; phân biệt Interchange / Selection
Đề thi sort nhanh2.2.3.a–bMerge hai mảng đã sort; partition pivot cuối
Hero2.2.3.c–d + bảng chọnHeapify; LSD từng chữ số; chọn thuật toán

Mỗi thuật toán: ý tưởng → lưu đồ → hình / chạy tay từng bước → mã giả → C++ → toán (OO) → ứng dụng → bẫy.

Checklist chất lượng (mỗi giải thuật phải có):

Tiêu chíCó trong chương
Đầy đủ theo đề cương + giáo trình (kể cả nội suy)2.1.2–2.1.4, 2.2.2.a–d, 2.2.3.a–d
Dễ hiểu (zero → hero, ẩn dụ)đầu mỗi mục
Trực quan (ô mảng, cây chia, đống)
Lưu đồ giải thuậtkhung Bắt đầu / điều kiện / xử lý / Kết thúc
Mã giả + mã C++
Chạy tay từng bướcbảng + mảng [5,1,4,2] hoặc dãy tìm
Toán (số so sánh, công thức OO)khối Toán
Ví dụ + bài tậptrong mục + 2.3
Ứng dụng thực tế (đề án SV, không thống kê bịa)khối Ứng dụng

Mảng chạy tay dùng xuyên chương (thuộc mặt):

Tìm: a = [10, 20, 30, 40, 50, 60, 70, 80] (đã tăng)
Sắp: a = [5, 1, 4, 2] (n = 4)

📚 BẢNG THUẬT NGỮ

Thuật ngữNghĩaGhi nhớ
Khóa (key)Trường dùng để tìm / sắpMã SV, điểm, a[i] nếu mảng int
Tìm thành côngCó khóa, trả chỉ sốCó thể có nhiều phần tử trùng — ta trả một vị trí
Tìm thất bạiKhông có, trả 1-1Vẫn tốn so sánh
Tuần tự / tuyến tínhĐi a[0], a[1], …Không cần sort
Nội suyƯớc vị trí theo giá trị, không luôn lấy giữaMảng tăng + phân bố khá đều
Lính canh (sentinel)Ghi khóa vào a[n] rồi tìmBớt kiểm i < n
Ổn định (stable)Hai phần tử bằng khóa giữ thứ tự tương đốiInsertion, Merge, Bubble (bản >) thường ổn định
Tại chỗ (in-place)Phụ O(1)O(1) (không kể stack đệ quy)Selection, Heap; Merge không
Pass / lượtMột vòng ngoàiBubble: mỗi pass “neo” max về cuối
PivotChốt QuickĐề thi thường a[high]
Heap (đống)Cây nhị phân hoàn chỉnh + tính chất heapCha ≥ con (max-heap)
LSDLeast Significant DigitRadix: hàng đơn vị rồi chục, trăm…

2.1. BÀI TOÁN TÌM KIẾM

2.1.1. Khái niệm

A. Bài toán (thuộc)

Tìm kiếm: cho dãy nn phần tử a[0..n1]a[0..n-1] và khóa xx.
Ra: một chỉ số ii (0i<n0\le i<n) sao cho a[i]a[i] “khớp” xx, hoặc 1-1 nếu không có.

Với int: khớp = a[i] == x.
Với SinhVien: khớp = strcmp(a[i].ma, ma)==0 (Ch.1).

Vào: a[0..n-1], x
Ra: i hoặc −1
chỉ số: 0 1 2 3 4
┌────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │
└────┴────┴────┴────┴────┘

tìm x = 30 → trả 2
tìm x = 99 → trả −1

Không phải “in ra phần tử”. Thi và hàm C++ trả vị trí để còn sửa, xóa (Ch.1 SuaDiem, XoaTai).

B. Hai họ — chọn theo mảng đã sort chưa

HọÝĐiều kiện mảngOO xấu
Tuần tự (2.1.2)So lần lượtKhông cần sortO(n)O(n)
Nhị phân (2.1.3)So giữa, bỏ nửaPhải đã sắp một chiềuO(logn)O(\log n)
Nội suy (2.1.4)Ước pos theo tỷ lệ giá trịĐã sắp phân bố khá đềuO(n)O(n) (lệch)

Nhị phân nhanh không bù cho mảng lộn: kết quả có thể sai. Muốn nhị phân → sắp trước (mục 2.2), chi phí sắp thường lớn hơn một lần tìm. Nhiều lần tìm trên cùng dữ liệu: sắp một lần, rồi nhị phân mãi.

C. Tốt / trung bình / xấu (ôn Ch.1)

Với tìm tuần tự, nn phần tử, mỗi lần so một phần tử:

Tình huốngSố so sánh khóa
Tốtx=a[0]x = a[0]11
Xấukhông có, hoặc x=a[n1]x=a[n-1]nn
TB (khóa có, vị trí đều)(n+1)/2\approx (n+1)/2

Đề không nói thì lấy xấu. OO tuần tự: O(n)O(n). OO nhị phân: O(logn)O(\log n) (xấu cũng log, vì mỗi bước bỏ nửa).

D. Ứng dụng trong đề án (nối Ch.1)

ViệcGiải thuật
Tìm MSSV trên mảng nhập lung tungTuần tự
Từ điển 100 000 từ đã sort theo alphabetNhị phân
Tìm xong rồi SuaDiemDùng chỉ số vừa trả

Không nhồi Google / “70% CPU”. Vai trò đủ: mọi thao tác “có khóa này không?” đều là tìm.

✅ Kiểm tra nhanh 2.1.1

  1. Hàm tìm trả giá trị a[i] thay vì ii — bất tiện lúc nào?
  2. Mảng chưa sort, dùng nhị phân cho “nhanh” — được không?
  3. Một lần tìm trên n=20n=20: tuần tự hay nhị phân (nếu phải sort trước)?

Đáp án: (1) Sửa/xóa cần chỉ số. (2) Không — sai kết quả. (3) Tuần tự: sort đã O(nlogn)O(n\log n) hoặc O(n2)O(n^2), đắt hơn 2020 so sánh.


A. Ý tưởng (zero)

Đứng ở đầu dãy, hỏi từng ô: “Có phải xx không?” Gặp thì dừng. Hết dãy thì 1-1.

tìm x = 40 trên [10, 20, 30, 40, 50]

i=0: 10 ≠ 40
i=1: 20 ≠ 40
i=2: 30 ≠ 40
i=3: 40 = 40 → trả 3, DỪNG

Không nhảy, không cần thứ tự. Giống lật từng trang danh sách lớp.

Lưu đồ

┌──────────┐
│ Bắt đầu │
└────┬─────┘

┌──────────┐
│ i ← 0 │
└────┬─────┘

┌───────────────┐ không
┌───►│ i < n ? ├──────────► ┌────────────┐
│ └───────┬───────┘ │ return −1 │
│ có │ └─────┬──────┘
│ ▼ │
│ ┌───────────────┐ có │
│ │ a[i] = x ? ├──────────► ┌─────┴──────┐
│ └───────┬───────┘ │ return i │
│ không│ └─────┬──────┘
│ ▼ │
│ ┌───────────────┐ │
│ │ i ← i + 1 │ ▼
│ └───────┬───────┘ ┌──────────┐
│ │ │ Kết thúc │
└────────────┘ └──────────┘

Hình thoi ? = quyết định. Mũi tên / không phải đọc được — đó là lưu đồ thi vẽ.

THUẬT TOÁN TimTuyenTinh(a, n, x) → i hoặc −1
1. i ← 0
2. while i < n
3. nếu a[i] = x thì trả về i
4. i ← i + 1
5. trả về −1
int TimTuyenTinh(int a[], int n, int x) {
for (int i = 0; i < n; i++)
if (a[i] == x) return i;
return -1;
}

SinhVien: đổi == thành strcmp(a[i].ma, ma) == 0.

C. Chạy tay + đếm so sánh

a=[10,20,30,40,50]a=[10,20,30,40,50], n=5n=5.

xxSo vớiSố so sánh ssKết quả
101010 (tốt)
4010,20,30,4043
50năm ô54 (cuối)
99năm ô5−1 (thất bại)
(n=0)không vào vòng0−1

D. Bản đếm — đúng đề thực hành

int TimTuyenTinh(int a[], int n, int x, int &ss) {
ss = 0;
for (int i = 0; i < n; i++) {
ss++;
if (a[i] == x) return i;
}
return -1;
}

Đếm trước if thì mỗi lần xét một phần tử tính một so sánh — khớp “xấu = nn”.

Toán

Gọi T(n)T(n) = số so sánh khóa (xấu: không có xx).

Ttoˆˊt=1,Txaˆˊu=n,TTBn+12T_{\text{tốt}}=1,\qquad T_{\text{xấu}}=n,\qquad T_{\text{TB}}\approx\frac{n+1}{2}

(giả sử xx có, vị trí đều). Viết O(n)O(n). Không gian phụ S(n)=O(1)S(n)=O(1).

Ứng dụng thực tế

Tình huốngVì sao tuần tự
Danh sách lớp 40 SV, tìm MSSV, chưa sortnn nhỏ, không tốn sort
Tìm trên DSLK (Ch.3)Không có a[i], chỉ đi pNext
Tìm lần đầu trên file nhập lung tungChưa biết thứ tự

Không dùng khi: từ điển 10510^5 từ đã alphabet — chuyển 2.1.3.

Ghi xx vào ô sau cùng (cần a còn chỗ, hoặc mảng kích thước n+1n+1):

THUẬT TOÁN TimLinhCanh(a, n, x) → i hoặc −1
1. a[n] ← x // lính
2. i ← 0
3. while a[i] ≠ x
4. i ← i + 1
5. nếu i < n thì trả về i else −1

Vòng trong không kiểm i < n. Vẫn O(n)O(n), chỉ bớt một phép so sánh biên mỗi vòng. Đề thi khóa này thường bản D là đủ.

F. OO, ưu / nhược, lúc dùng

Thời gianTốt O(1)O(1), xấu / TB O(n)O(n) → viết O(n)O(n)
Không gian phụO(1)O(1)
Ổn định?Không sắp — không áp dụng
ƯuMọi mảng; code ngắn; nn nhỏ ổn
Nhượcnn lớn, tìm nhiều lần → chậm

Bẫy 2.1.2

  1. == trên char[] — dùng strcmp.
  2. Quên return -1.
  3. n=0n=0 mà vẫn đọc a[0].
  4. Tìm thấy rồi vẫn chạy hết vòng — mất “tốt O(1)O(1)”, vẫn đúng kết quả nếu lấy chỉ số đầu.

✅ Kiểm tra nhanh 2.1.2

  1. n=6n=6, không có xx. ss?
  2. Có hai phần tử bằng xx. Trả chỉ số nào (bản D)?
  3. Vì sao tuần tự không cần sort?

Đáp án: (1) 6. (2) Chỉ số nhỏ nhất (gặp trước, return). (3) Chỉ so khớp, không dùng thứ tự trái/phải.


A. Ý tưởng

Mảng đã tăng. So xx với phần tử giữa. Bằng thì xong. xx lớn hơn → chỉ còn nửa phải. xx nhỏ hơn → nửa trái. Lặp đến khi hết khoảng.

L M R
10 20 30 40 50 60 70 80

a[M] ? x → bỏ một nửa

Ẩn dụ: từ điển mở giữa; chữ cần tìm đứng trước/sau trang giữa.

Lưu đồ

┌──────────┐
│ Bắt đầu │
└────┬─────┘

┌───────────────────┐
│ L ← 0, R ← n−1 │
└─────────┬─────────┘

┌────────────────┐ không
┌───►│ L ≤ R ? ├──────────► return −1
│ └────────┬───────┘
│ có │
│ ▼
│ ┌────────────────┐
│ │ M ← (L+R)/2 │
│ └────────┬───────┘
│ ▼
│ ┌────────────────┐ có
│ │ a[M] = x ? ├──────────► return M
│ └────────┬───────┘
│ không│
│ ▼
│ ┌────────────────┐ có
│ │ a[M] < x ? ├────► L ← M+1 ──┐
│ └────────┬───────┘ │
│ không│ │
│ ▼ │
│ R ← M−1 │
│ │ │
└─────────────┴────────────────────────┘

Điều kiện bắt buộc: mảng đã tăng (hoặc đã giảm, đổi nhánh). Mảng lộn → sai kết quả.

Phản ví dụ:

a = [10, 50, 20, 40] // lộn
tìm 20, L=0, R=3, M=1, a[1]=50 > 20 → R=0
còn [10], 10 ≠ 20 → −1 SAI (20 đang ở chỉ số 2)

B. Mã giả và C++ (vòng while — đề thi)

THUẬT TOÁN TimNhiPhan(a, n, x) → i hoặc −1 // a tăng dần
1. L ← 0, R ← n − 1
2. while L ≤ R
3. M ← (L + R) / 2 // chia nguyên
4. nếu a[M] = x thì trả về M
5. nếu a[M] < x thì L ← M + 1
6. ngược lại R ← M − 1
7. trả về −1
int TimNhiPhan(int a[], int n, int x) {
int L = 0, R = n - 1;
while (L <= R) {
int M = (L + R) / 2;
if (a[M] == x) return M;
if (a[M] < x) L = M + 1;
else R = M - 1;
}
return -1;
}

Khoảng còn lại luôn a[L..R]. L > R → thất bại.

M = L + (R - L) / 2 tránh tràn L+R khi chỉ số lớn. Đề n100n\le 100: (L+R)/2 đủ.

C. Chạy tay — thành công

a=[10,20,30,40,50,60,70,80]a=[10,20,30,40,50,60,70,80], n=8n=8, x=70x=70.

BướcLLRRM=(L+R)/2M=(L+R)/2a[M]a[M]Việc
10734040<7040<70L=4L=4
24756060<7060<70L=6L=6
367670bằng → trả 6

Ba so sánh, không phải 7.

x=40x=40 (trúng giữa ngay): M=3M=3, một so sánh.

D. Chạy tay — thất bại

x=35x=35, cùng mảng.

BướcLLRRMMa[M]a[M]Việc
10734040>3540>35R=2R=2
20212020<3520<35L=2L=2
32223030<3530<35L=3L=3
432L>RL>R−1

E. Hai mép: x=a[0]x=a[0]x=a[n1]x=a[n-1]

x=10x=10: M=3M=3 (40) → R=2R=2; M=1M=1 (20) → R=0R=0; M=0M=0 (10) → trả 0.
x=80x=80: luôn đi phải đến M=7M=7.

Vẫn O(logn)O(\log n) bước, không O(n)O(n).

F. Bản đếm so sánh

int TimNhiPhan(int a[], int n, int x, int &ss) {
ss = 0;
int L = 0, R = n - 1;
while (L <= R) {
int M = (L + R) / 2;
ss++;
if (a[M] == x) return M;
if (a[M] < x) L = M + 1;
else R = M - 1;
}
return -1;
}

Xấu: khoảng còn 1, 2, 4, … phần tử → log2n+1\lfloor\log_2 n\rfloor + 1 so sánh kiểu. n=8n=8 tối đa khoảng 4.

Toán

Txaˆˊu(n)=log2n+1T_{\text{xấu}}(n)=\lfloor \log_2 n \rfloor + 1

Mỗi bước khoảng còn \le một nửa. n=8=23n=8=2^3 → tối đa 4. n=106n=10^620\approx 20.
Đệ quy: T(n)=T(n/2)+O(1)T(n)=T(\lfloor n/2\rfloor)+O(1). Phụ vòng while: O(1)O(1).

Ứng dụng thực tế

Tình huốngVì sao nhị phân
Sổ điểm đã sort MSSV, tra nhiều lầnSort một lần, mỗi lần logn\log n
Từ điển / danh bạ alphabet“Mở giữa sách”
Tìm ngưỡng trên dãy tăng (ngày, điểm sàn)

Cấm: chưa sort; DSLK đơn (không nhảy giữa).

int TimNhiPhanDQ(int a[], int L, int R, int x) {
if (L > R) return -1;
int M = (L + R) / 2;
if (a[M] == x) return M;
if (a[M] < x) return TimNhiPhanDQ(a, M + 1, R, x);
return TimNhiPhanDQ(a, L, M - 1, x);
}
// gọi: TimNhiPhanDQ(a, 0, n - 1, x);

T(n)=T(n/2)+O(1)O(logn)T(n)=T(n/2)+O(1)\Rightarrow O(\log n). Stack đệ quy O(logn)O(\log n). Bản while phụ O(1)O(1) — ưu tiên thi.

Mảng giảm: đổi nhánh a[M] < x thành đi trái (vì trái lớn hơn). Hoặc so với bản tăng: đừng lẫn.

H. OO, ưu / nhược

Thời gianO(logn)O(\log n) tốt / TB / xấu (cùng bậc)
PhụO(1)O(1) vòng; O(logn)O(\log n) đệ quy
Ưun=106n=10^6 ~ 20 so sánh (Ch.1)
NhượcPhải sort; code dễ sai L=M không +1 (lặp vô hạn)

Bẫy 2.1.3

  1. Mảng chưa sort.
  2. while (L < R) quên trường hợp L=RL=R còn một ô.
  3. L = M khi $a[M]<xM` đã loại, dễ kẹt.
  4. Trùng khóa: bản này trả một vị trí giữa, không nhất thiết trái nhất.
  5. == chuỗi.

I. So tuần tự và nhị phân

Cùng aa đã tăng, n=8n=8, xx không có: tuần tự 8 so sánh; nhị phân ~4. nn lớn khe càng rộng. Một lần tìm n=10n=10 chưa sort: đừng sort rồi nhị phân.

n tăng →
so sánh

│ tuần tự ────────────
│ ╱
│ nhị phân
└────────────────────── n

✅ Kiểm tra nhanh 2.1.3

  1. Nhị phân yêu cầu mảng thế nào?
  2. n=16n=16, xấu khoảng bao nhiêu so sánh (bậc)?
  3. [2,4,6,8,10,12,14,16], tìm 1010: MM lần 1?
  4. L=M thay vì L=M+1 khi đi phải — rủi ro?

Đáp án: (1) Đã sắp một chiều. (2) O(logn)O(\log n), ~4–5. (3) (0+7)/2=3(0+7)/2=3, a[3]=8<10a[3]=8<10. (4) M không tiến, vòng có thể không kết.


Giáo trình thực hành Phần 1: ba phương pháp bắt buộc — tuần tự, nhị phân, nội suy. Đề thi hỏi OO và bắt viết TimNoiSuy.

A. Ý tưởng (zero)

Nhị phân luôn mở giữa trang từ điển. Nội suy hỏi: “xx nằm khoảng bao nhiêu phần trên đoạn giá trị?” rồi nhảy tới vị trí ước lượng.

Ẩn dụ: danh bạ 100 trang, tên bắt đầu AZ đều. Tìm “Nguyen” (~chữ N, giữa bảng chữ) → mở khoảng trang 50, không mở trang 50 vì “luôn giữa số trang” mà vì tỷ lệ giá trị.

a tăng, phân bố đều:

a[L] = 1 a[R] = 19
├────────────────────────────────┤
1 3 5 7 9 11 13 15 17 19

x = 13 → ước gần chỉ số 6, không phải giữa (chỉ số 4)

Điều kiện:

  1. Mảng đã tăng (như nhị phân).
  2. Giá trị trải khá đều — khoảng cách các phần tử không nhảy cóc quá lệch. Lệch mạnh → có thể chậm hơn nhị phân, xấu O(n)O(n).

Lưu đồ

┌──────────┐
│ Bắt đầu │
└────┬─────┘

L ← 0, R ← n−1

┌─────────────────────────────┐ không
┌───►│ L≤R và x trong [a[L],a[R]] ?├────► return −1
│ └─────────────┬───────────────┘
│ có │
│ ▼
│ ┌────────────┐ có
│ │ L = R ? ├────► return (a[L]=x ? L : −1)
│ └─────┬──────┘
│ không
│ ▼
│ pos ← công thức nội suy
│ ▼
│ ┌────────────┐ có
│ │ a[pos]=x ? ├────► return pos
│ └─────┬──────┘
│ không
│ ▼
│ a[pos]<x ? ──có──► L ← pos+1
│ không
│ ▼
│ R ← pos−1
│ │
└──────────────────┘

B. Công thức (thuộc, chia nguyên)

pos=L+(xa[L])(RL)a[R]a[L]\mathrm{pos} = L + \frac{(x - a[L])\cdot (R - L)}{a[R] - a[L]}
Thành phầnÝ
xa[L]x-a[L]xx hơn đầu đoạn bao nhiêu”
a[R]a[L]a[R]-a[L]“Cả đoạn giá trị dài bao nhiêu”
Tỷ lệ ×(RL)\times (R-L)Đổi tỷ lệ giá trị ra số ô

C++ (chia nguyên, đúng đề):

int pos = L + ((x - a[L]) * (R - L)) / (a[R] - a[L]);

Bắt buộc a[R]a[L]a[R] \ne a[L] (mẫu giáo trình: nếu L==R thì so một ô rồi return).
Nếu x<a[L]x < a[L] hoặc x>a[R]x > a[R]không có, return 1-1 (không tính pos).

C. Mã giả và C++ (khớp giáo trình)

THUẬT TOÁN TimNoiSuy(a, n, x) → i hoặc −1 // a tăng
1. L ← 0, R ← n−1
2. while L ≤ R và x ≥ a[L] và x ≤ a[R]
3. nếu L = R thì
4. nếu a[L] = x thì trả về L else −1
5. pos ← L + ((x − a[L]) × (R − L)) / (a[R] − a[L])
6. nếu a[pos] = x thì trả về pos
7. nếu a[pos] < x thì L ← pos+1
8. ngược lại R ← pos−1
9. trả về −1
int TimNoiSuy(int a[], int n, int x) {
int L = 0, R = n - 1;
while (L <= R && x >= a[L] && x <= a[R]) {
if (L == R)
return (a[L] == x) ? L : -1;
int pos = L + ((x - a[L]) * (R - L)) / (a[R] - a[L]);
if (a[pos] == x) return pos;
if (a[pos] < x) L = pos + 1;
else R = pos - 1;
}
return -1;
}

Bản đếm ss: ss++ mỗi lần so a[pos] == x (và lần a[L]==x khi L==R).

int TimNoiSuy(int a[], int n, int x, int &ss) {
ss = 0;
int L = 0, R = n - 1;
while (L <= R && x >= a[L] && x <= a[R]) {
if (L == R) {
ss++;
return (a[L] == x) ? L : -1;
}
int pos = L + ((x - a[L]) * (R - L)) / (a[R] - a[L]);
ss++;
if (a[pos] == x) return pos;
if (a[pos] < x) L = pos + 1;
else R = pos - 1;
}
return -1;
}

D. Chạy tay — phân bố đều (ví dụ giáo trình)

a=[1,3,5,7,9,11,13,15,17,19]a = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19], n=10n=10, chỉ số 0..90..9, x=13x=13.

pos=0+(131)(90)191=12918=6\mathrm{pos} = 0 + \frac{(13-1)\cdot(9-0)}{19-1} = \frac{12\cdot 9}{18} = 6

a[6]=13a[6]=13một so sánh, trả 6.

Cùng mảng, tuần tự: 7 so sánh (đi 1,3,,131,3,\ldots,13). Nhị phân: 4 bước (giữa 9 → 15 → 11 → 13). Đó là lý do giáo trình gọi nội suy “nhanh khi đều”.

x=1x=1 (mép trái): pos=0+(09)/18=0\mathrm{pos}=0+(0\cdot 9)/18=0, trúng ngay.
x=19x=19: pos=0+(189)/18=9\mathrm{pos}=0+(18\cdot 9)/18=9, trúng ngay.
x=0x=0 hoặc x=20x=20: điều kiện x >= a[L] && x <= a[R] sai ngay → 1-1, không chia.

E. Chạy tay — không trúng lần đầu

Cùng mảng, x=15x=15:

pos=0+(151)918=14918=7\mathrm{pos} = 0 + \frac{(15-1)\cdot 9}{18} = \frac{14\cdot 9}{18} = 7

(chia nguyên 126/18=7126/18=7). a[7]=15a[7]=15 → vẫn một bước.

x=11x=11: (111)9/18=90/18=5(11-1)*9/18=90/18=5, a[5]=11a[5]=11.

Dãy cách đều 2 nên công thức gần như “đúng chỗ”. Đó là trường hợp tốt O(loglogn)O(\log\log n) — với dãy đều thường rất ít bước, đôi khi 1.

F. Khi lệch — xấu O(n)O(n) (phải nói được)

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1000] // 9 số nhỏ, một số khổng
tìm x = 9

a[R]a[L]=999a[R]-a[L]=999, xa[L]=8x-a[L]=8, RL=9R-L=9:

pos=0+89999=0\mathrm{pos} = 0 + \frac{8\cdot 9}{999} = 0

a[0]=1<9a[0]=1 < 9L=1L=1. Lần sau vẫn ước gần đầu mảng. Có thể bò từng ô → xấu như tuần tự O(n)O(n). Nhị phân trên mảng này vẫn O(logn)O(\log n).

Kết luận chọn: đều → nội suy; không chắc đều → nhị phân; chưa sort → tuần tự.

G. OO, ưu / nhược, so ba thuật toán

Tuần tựNhị phânNội suy
Mảngmọiđã sắpđã sắp + khá đều
TốtO(1)O(1)O(1)O(1)O(1)O(1) (trúng pos)
TBO(n)O(n)O(logn)O(\log n)O(loglogn)O(\log\log n) nếu đều
XấuO(n)O(n)O(logn)O(\log n)O(n)O(n) nếu lệch
PhụO(1)O(1)O(1)O(1)O(1)O(1)

Đề điền chỗ trống: nội suy tốt O(loglogn)O(\log\log n), xấu O(n)O(n), cần mảng đã sắp.

Ứng dụng thực tế

Tình huốngVì sao nội suy
Mã SV / số báo danh tăng đều (SV001, SV002, …)pos gần đúng chỗ
Bảng điểm 0, 0.5, 1, … 10 cách đềuÍt bước hơn nhị phân
Dữ liệu nhảy cóc (1,2,3,…,9,1000)Đừng — dùng nhị phân

Bẫy 2.1.4

  1. Không kiểm xx ngoài [a[L],a[R]][a[L], a[R]]pos âm hoặc >R>R, vượt biên.
  2. L==R mà vẫn chia a[R]a[L]=0a[R]-a[L]=0.
  3. Nhân (x-a[L])*(R-L) tràn int khi số lớn — đề n100n\le 100, giá trị nhỏ thì ổn; biết là có.
  4. Mảng chưa sort / giảm dần.
  5. Nhầm “nội suy luôn nhanh hơn nhị phân” — sai khi phân bố lệch.

✅ Kiểm tra nhanh 2.1.4

  1. Công thức pos? Khi nào cấm tính?
  2. [1,3,5,7,9,11,13,15,17,19], x=13x=13: pos = ? a[pos]a[\mathrm{pos}]?
  3. Tốt / xấu (ký hiệu OO)?
  4. Mảng lệch, tìm gần max của cụm nhỏ — nội suy hay nhị phân an toàn hơn?

Đáp án: (1) L+(xa[L])(RL)/(a[R]a[L])L + (x-a[L])(R-L)/(a[R]-a[L]); cấm khi a[R]=a[L]a[R]=a[L] hoặc xx ngoài đoạn. (2) 66, 1313. (3) O(loglogn)O(\log\log n) / O(n)O(n). (4) Nhị phân.


2.2. BÀI TOÁN SẮP XẾP

2.2.1. Khái niệm

A. Bài toán

Sắp xếp: đưa a[0..n1]a[0..n-1] về thứ tự theo khóa (thường tăng: a[0]a[1]a[n1]a[0]\le a[1]\le\cdots\le a[n-1]).

TRƯỚC SAU (tăng)
┌─┬─┬─┬─┐ ┌─┬─┬─┬─┐
│5│1│4│2│ │1│2│4│5│
└─┴─┴─┴─┘ └─┴─┴─┴─┘

Giảm: đổi mọi > thành < (và ngược lại) trong so sánh.

Sau khi sắp: tìm nhị phân được; in “top điểm”; gộp hai danh sách (Merge).

B. Ổn định và tại chỗ

Ổn định: hai hồ sơ cùng khóa, ai đứng trước lúc vào thì vẫn đứng trước lúc ra.

Vào (cùng điểm 8, khác tên): An(8), Binh(8), Chi(7)
Sort ổn định theo điểm tăng: Chi(7), An(8), Binh(8)
An vẫn trước Binh
Sort KHÔNG ổn định: Chi(7), Binh(8), An(8) ← có thể

Thi lý thuyết: Insertion / Merge / Bubble (so >) thường ổn định. Selection / Interchange / Heap / Quick (bản partition nhảy) không (hoặc không đảm bảo).

Tại chỗ: không cấp mảng nn phần tử phụ. Merge không tại chỗ (cần tmp[]). Quick tại chỗ về mảng, nhưng stack đệ quy O(logn)O(\log n) trung bình.

C. Họ O(n2)O(n^2) và họ O(nlogn)O(n\log n)

HọMụcOO xấu điển hìnhn=105n=10^5
Đơn giản (2.2.2)So cặp, lồng hai vòngO(n2)O(n^2)nặng
Nhanh (2.2.3)Chia để trị / heap / chữ sốO(nlogn)O(n\log n) hoặc O(d(n+k))O(d(n+k))vừa

Cận dưới sort so sánh: Ω(nlogn)\Omega(n\log n) trung bình. Radix không so hai khóa nguyên — đi từng chữ số, nên có thể O(n)O(n) theo nn khi d,kd,k nhỏ.

D. Tiêu chuẩn chọn (nối 1.2)

  1. nn nhỏ / bài tập: bốn thuật toán 2.2.2 — dễ vẽ, dễ thi viết tay.
  2. nn lớn, so sánh khóa: Merge (ổn định, O(nlogn)O(n\log n) luôn) hoặc Quick (TB nhanh, xấu O(n2)O(n^2)).
  3. Cần O(nlogn)O(n\log n) luôn + phụ O(1)O(1): Heap.
  4. Khóa là số nguyên không âm, nhiều chữ số ít: Radix.

E. Ứng dụng sắp xếp trong đề án (thực tế khóa này)

ViệcThuật toán gợi ý
40 SV, in theo tên / điểm — đề thiInsertion (điểm), Selection (tên)
Thêm 1 hồ sơ vào ds đã sortInsertion O(n)O(n)
Hai lớp đã sort MSSV, gộp một danh sáchMerge
10 000 điểm, không cần ổn địnhQuick (tránh file đã tăng + pivot cuối) hoặc Heap
Xếp MSSV / số báo danhRadix
Thi viết tay, n=4n=466Interchange / Bubble / Selection — vẽ từng bước

Không bịa “Google dùng Quick”. Trong môn: quản lý sinh viên trên mảng (thực hành bài 5) là ứng dụng đủ để thi.

✅ Kiểm tra nhanh 2.2.1

  1. Ổn định quan trọng khi nào?
  2. Merge tại chỗ không? Heap?
  3. Sort xong mới nhị phân: chi phí một lần tìm nn nhỏ — có đáng?

Đáp án: (1) Sắp theo khóa 2 nhưng muốn giữ thứ tự khóa 1. (2) Merge không; Heap coi là tại chỗ. (3) Thường không — dùng tuần tự.


2.2.2. Các thuật toán sắp xếp đơn giản

Cả bốn: hai vòng, O(n2)O(n^2), phụ O(1)O(1), dễ viết. Khác nhau chỗ so với ai, đổi lúc nào. Học thuộc khác biệt Interchange ≠ Selection — đề / Ch.3 SapXep là Interchange.

Mảng minh họa xuyên suốt: [5, 1, 4, 2].

Hoán đổi hai ô (dùng mọi thuật toán):

void DoiCho(int &x, int &y) {
int t = x; x = y; y = t;
}

Ba phép gán — đề thực hành đếm gan += 3.


2.2.2.a. Sắp xếp đổi chỗ trực tiếp (Interchange Sort)

Ý tưởng: Người ở vị trí ii nhìn mọi người phía sau (j=i+1..n1j=i+1..n-1). Hễ a[i]>a[j]a[i] > a[j] thì đổi ngay, rồi nhìn jj tiếp theo. Sau vòng ii, ô ii đã là min của đoạn i..n1i..n-1.

Đây là bản mảng của Ch.3 SapXep. Không phải Selection: Selection tìm một chỉ số min rồi đổi một lần.

Lưu đồ

Bắt đầu → i ← 0

┌────────────────┐ không
┌───►│ i ≤ n−2 ? ├────► Kết thúc
│ └───────┬────────┘
│ có │
│ ▼
│ j ← i+1
│ ▼
│ ┌────────────────┐ không
│ ┌──►│ j ≤ n−1 ? ├──► i ← i+1 ──┐
│ │ └───────┬────────┘ │
│ │ có │ │
│ │ ▼ │
│ │ a[i] > a[j] ? │
│ │ có: đổi a[i] ↔ a[j] │
│ │ ▼ │
│ │ j ← j+1 │
│ └───────────┘ │
└─────────────────────────────────────┘
THUẬT TOÁN InterchangeSort(a, n) // tang dan
1. i ← 0
2. while i ≤ n-2
3. j ← i+1
4. while j ≤ n-1
5. nếu a[i] > a[j] thì đổi a[i] ↔ a[j]
6. j ← j+1
7. i ← i+1
void InterchangeSort(int a[], int n) {
for (int i = 0; i < n - 1; i++)
for (int j = i + 1; j < n; j++)
if (a[i] > a[j])
DoiCho(a[i], a[j]);
}

Chạy tay [5, 1, 4, 2] — sau mỗi đổi, a[i]a[i] đã mới, so tiếp với giá trị mới (giống bảng Ch.3):

iijjSoĐổi?Mảng sau
015>11, 5, 4, 2
021>4không1, 5, 4, 2
031>2không1, 5, 4, 2
125>41, 4, 5, 2
134>21, 2, 5, 4
235>41, 2, 4, 5

Số so sánh luôn n(n1)/2=6n(n-1)/2=6. Số đổi: 4 (nhiều hơn Selection trên cùng mảng).

Toán

Số cặp (i,j)(i,j) với 0i<jn10\le i<j\le n-1:

C=n(n1)2=Θ(n2)C = \frac{n(n-1)}{2}=\Theta(n^2)

Luôn đủ CC so sánh (kể cả mảng đã tăng). Số lần đổi: 00CC tùy dữ liệu.

Ứng dụng thực tế

Dùng khi nn nhỏ, đề bắt “đổi chỗ trực tiếp”, hoặc sort DSLK hoán data (Ch.3 SapXep). Không dùng danh sách 10 000 SV mỗi lần F5.

Bẫy: nộp Selection khi đề “đổi chỗ trực tiếp”. Nhìn code: có min_idx không? Không → Interchange.


2.2.2.b. Sắp xếp chọn trực tiếp (Selection Sort)

Ý tưởng: Mỗi vòng ii, tìm chỉ số phần tử nhỏ nhất trên i..n1i..n-1, rồi đổi một lần với a[i]a[i].

i=0: [5, 1, 4, 2] min ở j=1 (số 1) đổi với a[0]
[1 | 5, 4, 2] ô 0 xong
i=1: min của 5,4,2 là 2 đổi với a[1]
[1, 2 | 4, 5]
i=2: min của 4,5 là 4 đã đúng chỗ
[1, 2, 4, 5]

Lưu đồ

Bắt đầu → i ← 0

┌──────────────┐ không
│ i ≤ n−2 ? ├────► Kết thúc
└──┬───────────┘
có │

min ← i ; j ← i+1

┌──────────────┐ không
│ j ≤ n−1 ? ├────► (min≠i thì đổi a[i]↔a[min]) → i++ → lặp
└──┬───────────┘
có │

a[j] < a[min] ? có: min ← j

j ← j+1 → (lặp vòng j)
THUẬT TOÁN SelectionSort(a, n)
1. for i ← 0 to n-2
2. min ← i
3. for j ← i+1 to n-1
4. nếu a[j] < a[min] thì min ← j
5. nếu min ≠ i thì đổi a[i] ↔ a[min]
void SelectionSort(int a[], int n) {
for (int i = 0; i < n - 1; i++) {
int min = i;
for (int j = i + 1; j < n; j++)
if (a[j] < a[min]) min = j;
if (min != i)
DoiCho(a[i], a[min]);
}
}

Chạy tay — chỉ đổi khi min != i:

iimin (sau vòng trong)ĐổiMảng
01 (giá trị 1)5↔11, 5, 4, 2
13 (giá trị 2)5↔21, 2, 4, 5
22 (giá trị 4)không1, 2, 4, 5

So sánh vẫn 66. Đổi chỉ 2 lần.

Toán

So sánh luôn C=n(n1)/2C=n(n-1)/2. Số lần DoiCho n1\le n-1 (ưu: ít ghi bộ nhớ). S=O(1)S=O(1). Không ổn định.

Ứng dụng thực tế

Đề Phần 1: sắp theo họ tên (strcmp). Hợp khi swap đắt (struct lớn) vì mỗi vòng ngoài tối đa một đổi. nn lớn → chuyển Heap/Quick.


2.2.2.c. Sắp xếp chèn trực tiếp (Insertion Sort)

Ý tưởng: Tay trái đã xếp bài. Rút quân mới (key = a[i]), dời các quân lớn hơn sang phải, nhét key vào lỗ.

sorted | unsorted
[5] | 1 4 2 rút 1, dời 5 → [1 5 | 4 2]
[1 5] | 4 2 rút 4, dời 5 → [1 4 5 | 2]
[1 4 5]| 2 rút 2, dời 5,4 → [1 2 4 5]

Lưu đồ

Bắt đầu → i ← 1

┌─────────────┐ không
│ i ≤ n−1 ? ├────► Kết thúc
└──┬──────────┘
có │

key ← a[i] ; j ← i−1

┌──────────────────────────┐ không
│ j≥0 và a[j] > key ? ├────► a[j+1] ← key ; i++ → lặp
└──┬───────────────────────┘
có │

a[j+1] ← a[j] ; j ← j−1 → (lặp while)
THUẬT TOÁN InsertionSort(a, n)
1. for i ← 1 to n-1
2. key ← a[i]
3. j ← i-1
4. while j ≥ 0 và a[j] > key
5. a[j+1] ← a[j]
6. j ← j-1
7. a[j+1] ← key
void InsertionSort(int a[], int n) {
for (int i = 1; i < n; i++) {
int key = a[i];
int j = i - 1;
while (j >= 0 && a[j] > key) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
}

Dùng > không >=ổn định (gặp bằng thì dừng, key đứng sau phần tử bằng).

Chạy tay [5, 1, 4, 2]:

iikeyViệc dờiMảng sau chèn
11a[1]=51, 5, 4, 2
24a[2]=51, 4, 5, 2
32a[3]=5, a[2]=41, 2, 4, 5

Toán

Ttoˆˊt=n1a˜ ta˘ng, while khoˆng chạy),Txaˆˊu=n(n1)2(mảng giảm)T_{\text{tốt}}=n-1\quad\text{(đã tăng, while không chạy)},\qquad T_{\text{xấu}}=\frac{n(n-1)}{2}\quad\text{(mảng giảm)}

Ổn định nếu so >. S=O(1)S=O(1).

Ứng dụng thực tế

Đề: sắp theo điểm TB. Thêm 1 SV vào danh sách đã sort điểm → Insertion O(n)O(n), không cần Quick. Bài đang gõ (gần đúng thứ tự) cũng hợp.

Bẫy: while thiếu j>=0a[-1]. Nhầm dời thành swap kiểu Interchange. Khác Selection: không tìm min cả đoạn — chỉ dời để chèn key.


2.2.2.d. Sắp xếp nổi bọt (Bubble Sort)

Ý tưởng: Chỉ so hai ô kề. Lớn hơn thì đổi — số lớn “nổi” dần về cuối. Mỗi pass ii neo chắc a[n-1-i].

Pass 0, so kề:
5 1 4 2 → 1 5 4 2 → 1 4 5 2 → 1 4 2 5 (5 xong)
Pass 1:
1 4 2 |5 → 1 4 2 5 → 1 2 4 5 (4 xong)
Pass 2:
1 2 |4 5 → không đổi xong

Lưu đồ

Bắt đầu → i ← 0

┌──────────────┐ không
│ i ≤ n−2 ? ├────► Kết thúc
└──┬───────────┘
có │

swapped ← false ; j ← 0

┌──────────────────┐ không
│ j ≤ n−2−i ? ├────► swapped=false? có: Kết thúc
└──┬───────────────┘ không: i++ → lặp
có │

a[j] > a[j+1] ? có: đổi, swapped ← true

j ← j+1 → (lặp j)
THUẬT TOÁN BubbleSort(a, n)
1. for i ← 0 to n-2
2. swapped ← false
3. for j ← 0 to n-2-i
4. nếu a[j] > a[j+1] thì đổi; swapped ← true
5. nếu swapped = false thì break // da sort
void BubbleSort(int a[], int n) {
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]) {
DoiCho(a[j], a[j + 1]);
swapped = true;
}
}
if (!swapped) break;
}
}

Không swapped: vẫn đúng, mất cờ dừng sớm. Thực hành có cờ.

Chạy tay (có cờ):

Pass iiCác so kề (mảng biến)swappedCuối pass
05↔1, 5↔4, 5↔2true1, 4, 2, 5
11–4, 4↔2true1, 2, 4, 5
21–2falsebreak

Toán

Không cờ: C=n(n1)/2C=n(n-1)/2 so sánh kề. Có cờ + mảng tăng: T=n1=O(n)T=n-1=O(n). Xấu (giảm): Θ(n2)\Theta(n^2). Ổn định (so >).

Ứng dụng thực tế

Dạy / thi viết tay (dễ vẽ “bọt nổi”). Kiểm tra mảng đã sort: một pass, swapped=false. nn lớn không dùng.

Khác Interchange: chỉ so kề. Bẫy: j < n-1 không trừ i thì so thừa; j <= n-1 vượt a[j+1].


e. Bốn thuật toán trên một tờ — thuộc để thi

Cùng nn, số so sánh Interchange = Selection = n(n1)/2n(n-1)/2. Bubble (không cờ) cùng bậc, số so sánh n(n1)/2n(n-1)/2 luôn. Insertion thay đổi theo dữ liệu.

So với aiĐổi khi nàoỔn địnhTốt
Interchangea[i]a[i] và mọi a[j]a[j] sauHễ a[i]>a[j]KhôngKhông (luôn n2n^2)
SelectionTìm min đoạnMột swap / iiKhôngKhông (luôn n2n^2 so)
InsertionDời đoạn đã sortChèn keyĐã tăng O(n)O(n)
BubbleHai ô kềĐổi kềĐã tăng O(n)O(n) nếu có cờ
i cố định:
Interchange: a[i] ⇔ a[i+1], a[i+2], ... (nhiều swap)
Selection: tìm min rồi a[i] ⇔ a[min] (≤ 1 swap)
Bubble: a[0]⇔a[1], a[1]⇔a[2], ... (sóng kề)
Insertion: lấy a[i] nhét vào a[0..i-1]

✅ Kiểm tra nhanh 2.2.2

  1. Ch.3 SapXep hai vòng, gặp lớn thì đổi data ngay — tên thuật toán?
  2. Selection đổi tối đa bao nhiêu lần?
  3. Insertion tốt khi nào? Bubble xấu khi nào?
  4. [5,1,4,2] Interchange: sau i=0i=0 mảng?

Đáp án: (1) Đổi chỗ trực tiếp. (2) n1n-1. (3) Đã (gần) tăng; mảng giảm. (4) [1, 5, 4, 2].


2.2.3. Các thuật toán sắp xếp nhanh

nn lớn: n2n^2 không chơi (Ch.1: n=106n=10^6101210^{12}). Bốn giải thuật dưới: chia để trị hoặc không so cả khóa.


2.2.3.a. Sắp xếp trộn (Merge Sort)

Ý tưởng: Chia mảng đôi đến còn 1 phần tử (đã “sort”). Trộn hai đoạn đã sort thành một đoạn sort.

[5, 1, 4, 2]
/ \
[5, 1] [4, 2]
/ \ / \
[5] [1] [4] [2]
\ / \ /
[1, 5] [2, 4]
\ /
[1, 2, 4, 5]

Lưu đồ (một lời gọi MergeSort)

┌─────────────────────┐
│ MergeSort(left,right)│
└──────────┬──────────┘

left ≥ right ?
có → return
không

mid ← (left+right)/2

MergeSort(left, mid)

MergeSort(mid+1, right)

Merge(left, mid, right)

Lưu đồ Merge: hai con trỏ đầu nửa; lấy phần tử nhỏ hơn; hết một nửa thì copy phần còn.

THUẬT TOÁN Merge(a, left, mid, right) // a[left..mid], a[mid+1..right] da sort

L: 1 5 R: 2 4 out: ▲ ▲ 1<2 → lấy 1 1 ▲ ▲ 5>2 → lấy 2 1 2 ▲ ▲ 5>4 → lấy 4 1 2 4 ▲ lấy 5 1 2 4 5

THUẬT TOÁN Merge(a, left, mid, right) // a[left..mid], a[mid+1..right] da sort

  1. chép hai nửa vào tmp
  2. i ← đầu nửa trái, j ← đầu nửa phải, k ← left
  3. while còn cả hai nửa
  4. nếu tmp[i] ≤ tmp[j] thì a[k] ← tmp[i], i++ // ≤ để ổn định
  5. else a[k] ← tmp[j], j++
  6. k++
  7. chép phần còn lại

THUẬT TOÁN MergeSort(a, left, right)

  1. nếu left ≥ right thì return
  2. mid ← (left + right) / 2
  3. MergeSort(a, left, mid)
  4. MergeSort(a, mid+1, right)
  5. Merge(a, left, mid, right)

```cpp
void Merge(int a[], int left, int mid, int right) {
int n1 = mid - left + 1, n2 = right - mid;
int *L = new int[n1], *R = new int[n2];
for (int i = 0; i < n1; i++) L[i] = a[left + i];
for (int j = 0; j < n2; j++) R[j] = a[mid + 1 + j];
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) a[k++] = L[i++];
else a[k++] = R[j++];
}
while (i < n1) a[k++] = L[i++];
while (j < n2) a[k++] = R[j++];
delete[] L;
delete[] R;
}

void MergeSort(int a[], int left, int right) {
if (left >= right) return;
int mid = (left + right) / 2;
MergeSort(a, left, mid);
MergeSort(a, mid + 1, right);
Merge(a, left, mid, right);
}
// goi: MergeSort(a, 0, n - 1);

new không kiểm NULL (quy ước Ch.3). Có thể dùng một tmp[MAX] toàn cục cho đề nn nhỏ.

Chạy tay đủ cây [5, 1, 4, 2], MergeSort(a, 0, 3):

Gọileft,rightViệcMảng a sau bước
MS(0,3)chia mid=1gọi trái, phải, rồi Merge
MS(0,1)mid=0[5][1]
Merge(0,0,1)trộn 5 và 11, 5, 4, 2
MS(2,3)[4][2]1, 5, 2, 4 sau Merge(2,2,3)
Merge(0,1,3)trộn [1,5] với [2,4]lấy 1,2,4,51, 2, 4, 5

Trộn Merge(0,1,3) từng phần tử:

So L[i] ? R[j]Lấya[k] đang thành
1 ≤ 211, …
5 ≤ 2? không21, 2, …
5 ≤ 4? không41, 2, 4, …
hết R51, 2, 4, 5

Mỗi tầng trộn O(n)O(n), sâu logn\log n tầng → luôn O(nlogn)O(n\log n). Phụ O(n)O(n). Ổn định nếu lấy trái khi <=.

Toán

T(n)=2T(n/2)+O(n),T(1)=O(1)    T(n)=O(nlogn)T(n)=2T(n/2)+O(n),\quad T(1)=O(1) \;\Longrightarrow\; T(n)=O(n\log n)

(cây đệ quy: log2n\log_2 n tầng, mỗi tầng tổng việc trộn O(n)O(n)). S=O(n)S=O(n) mảng phụ.

Ứng dụng thực tế

Gộp hai danh sách lớp đã sort MSSV. Cần ổn định (sort điểm rồi sort tên, tên bằng thì giữ thứ tự điểm). DSLK: Merge hợp hơn Quick (Ch.3).

Bẫy: quên copy phần còn lại; mid không chia; so < thay <= mất ổn định.


2.2.3.b. Sắp xếp nhanh (Quick Sort)

Ý tưởng: Chọn chốt (pivot). Đưa phần tử << pivot sang trái, \ge sang phải. Pivot đúng chỗ. Đệ quy hai bên.

Đề thi / thực hành: pivot = a[high], partition Lomuto.

[5, 1, 8, 2] pivot = 2 (cuối)
1 đi trái, rồi đặt 2 vào giữa
[1] 2 [8, 5]
pivot 5 → [5, 8]
[1, 2, 5, 8]

Lưu đồ

┌──────────────────────┐
│ QuickSort(low, high) │
└──────────┬───────────┘

low < high ?
không → return


p ← partition(low, high)

QuickSort(low, p−1)

QuickSort(p+1, high)

partition: pivot=a[high]; j quét; a[j]<pivot thì dồn trái; cuối đặt pivot vào p.

Partition Lomuto — chạy tay [5, 1, 8, 2], low=0, high=3, pivot=2, i=low-1=-1:

jja[j] < pivot?ViệcMảngii
05<2? không5,1,8,2−1
11<2? cói=0i=0, đổi a[0]a[1]1,5,8,20
28<2? không1,5,8,20
đặt pivotđổi a[1]a[3]1,2,8,5trả i+1=1i+1=1

Trái [1] (một phần tử, xong). Phải [8, 5]QuickSort(a, 2, 3):

jja[j]<5?ViệcMảng
28<5? không1, 2, 8, 5
đặt pivotđổi a[2]a[3]1, 2, 5, 8trả 2

Hai bên của 5 rỗng. Xong [1, 2, 5, 8].

Xấu — mảng đã tăng, pivot cuối. [1, 2, 4, 5], high=3, pivot=5: không ai <5, đặt pivot vẫn ở cuối, trái còn n1n-1 phần tử. Mỗi lần cắt 1 ô → n+(n1)+=Θ(n2)n+(n-1)+\cdots = \Theta(n^2).

THUẬT TOÁN partition(a, low, high) → vị trí pivot
1. pivot ← a[high]
2. i ← low − 1
3. for j ← low to high−1
4. nếu a[j] < pivot thì i++; đổi a[i] ↔ a[j]
5. đổi a[i+1] ↔ a[high]
6. trả về i+1

THUẬT TOÁN QuickSort(a, low, high)
1. nếu low < high
2. p ← partition(a, low, high)
3. QuickSort(a, low, p−1)
4. QuickSort(a, p+1, high)
int partition(int a[], int low, int high) {
int pivot = a[high];
int i = low - 1;
for (int j = low; j <= high - 1; j++) {
if (a[j] < pivot) {
i++;
DoiCho(a[i], a[j]);
}
}
DoiCho(a[i + 1], a[high]);
return i + 1;
}

void QuickSort(int a[], int low, int high) {
if (low < high) {
int p = partition(a, low, high);
QuickSort(a, low, p - 1);
QuickSort(a, p + 1, high);
}
}
// goi: QuickSort(a, 0, n - 1);
TB / tốtChia khá đều, O(nlogn)O(n\log n)
XấuPivot luôn min hoặc max (mảng đã tăng/giảm, chốt cuối) → O(n2)O(n^2)
PhụStack O(logn)O(\log n) TB, O(n)O(n) xấu
Ổn địnhKhông (đổi không kề)

Đề: “Quick xấu khi mảng đã sort” — đúng với pivot cuối. (Pivot ngẫu nhiên / median-of-three giảm xác suất xấu — nâng cao, không bắt.)

Toán

Chia đều: T(n)=2T(n/2)+O(n)=O(nlogn)T(n)=2T(n/2)+O(n)=O(n\log n).
Chia lệch (pivot min/max): T(n)=T(n1)+O(n)=Θ(n2)T(n)=T(n-1)+O(n)=\Theta(n^2).
TB (pivot “ngẫu nhiên”): O(nlogn)O(n\log n). Stack TB O(logn)O(\log n), xấu O(n)O(n).

Ứng dụng thực tế

Mảng số nguyên nn lớn, không cần ổn định (điểm không trùng, hoặc chấp nhận đảo người bằng điểm). Tránh pivot cuối trên dữ liệu đã tăng (file xuất đã sort).

Bẫy: QuickSort(a, n) thiếu low, high; đệ quy gồm cả pivot (p không p-1); a[j] <= pivot dồn bằng sang trái — vẫn sort, khác chỗ bằng.


2.2.3.c. Sắp xếp vun đống (Heap Sort)

Đống max (max-heap): cây nhị phân hoàn chỉnh (lấp trái → phải), mọi nút ≥ hai con. Cất bằng mảng (không cần TNode Ch.5):

cha(i)=(i1)/2,traˊi=2i+1,phải=2i+2\text{cha}(i)=\lfloor(i-1)/2\rfloor,\quad \text{trái}=2i+1,\quad \text{phải}=2i+2

(ii từ 00.)

Mảng [4, 10, 3, 5, 1]

4 chỉ số 0
/ \
10 3 1 2
/ \
5 1 3 4

Heapify (sàng xuống từ ii, trong heap kích thước size): nếu con lớn hơn cha thì đổi với con lớn nhất, lặp xuống.

Lưu đồ HeapSort

Bắt đầu

i ← n/2 − 1 … 0: heapify(n, i) ← dựng max-heap

i ← n−1 … 1:
đổi a[0] ↔ a[i]
heapify(i, 0) ← neo max về cuối

Kết thúc

heapify(i): so a[i] với hai con; nếu con lớn hơn thì đổi và sàng tiếp.

Heap Sort:

  1. Dựng heap: heapify từ nút cha cuối n/2-1 về 0.
  2. Lặp: đổi a[0] (max) với a[size-1]; giảm size; heapify(0).
Sau dựng max-heap: [10, 5, 3, 4, 1]
10
/ \
5 3
/ \
4 1

Đổi 10 ↔ 1, heap size=4, sàng 1 → ... 10 đã neo cuối
...
Kết quả tăng dần ở mảng
void heapify(int a[], int size, int i) {
int largest = i;
int L = 2 * i + 1, R = 2 * i + 2;
if (L < size && a[L] > a[largest]) largest = L;
if (R < size && a[R] > a[largest]) largest = R;
if (largest != i) {
DoiCho(a[i], a[largest]);
heapify(a, size, largest);
}
}

void HeapSort(int a[], int n) {
for (int i = n / 2 - 1; i >= 0; i--)
heapify(a, n, i);
for (int i = n - 1; i > 0; i--) {
DoiCho(a[0], a[i]);
heapify(a, i, 0);
}
}

Chạy tay dựng heap [4, 10, 3, 5, 1], n=5n=5, bắt đầu i=n/21=1i=n/2-1=1 (nút 10): con 5,1 — 10 đã ≥. i=0i=0 (nút 4): con 10,3 — 10 lớn hơn, đổi 4↔10 → [10, 4, 3, 5, 1]; sàng tiếp nút 4 với 5,1 → đổi 4↔5 → [10, 5, 3, 4, 1].

Lượt sort (đổi gốc với cuối, giảm size, sàng):

Size trướcĐổi a[0]a[size-1]Sàng gốcÝ
510↔1 → [1, 5, 3, 4, 10]1 xuống → [5, 4, 3, 1, 10]10 neo
45↔1 → [1, 4, 3, 5, 10][4, 1, 3, 5, 10]5 neo
34↔3 → [3, 1, 4, 5, 10][3, 1, 4, 5, 10]4 neo
23↔1 → [1, 3, 4, 5, 10][1, 3, 4, 5, 10]xong

(Sàng chi tiết size=5: gốc 1, trái 5, phải 3 → đổi 1↔5; rồi 1 với 4,1 → đổi 1↔4 → [5,4,3,1,10]. Làm chậm trên giấy như bảng heapify.)

Thời gianLuôn O(nlogn)O(n\log n) (dựng O(n)O(n), nn lần sàng O(logn)O(\log n))
PhụO(1)O(1) mảng (đệ quy heapify O(logn)O(\log n) stack; có thể viết vòng)
Ổn địnhKhông
ƯuKhông xấu n2n^2 như Quick; không mảng phụ như Merge
NhượcThực tế thường chậm hơn Quick TB (không bắt thuộc số liệu)

Cây heap không phải BST. BST: trái < nút < phải. Heap: chỉ cha ≥ con, trái/phải không theo thứ tự tìm kiếm.

Toán

Chiều cao đống hoàn chỉnh h=log2nh=\lfloor\log_2 n\rfloor. Mỗi heapify O(h)=O(logn)O(h)=O(\log n). Dựng heap O(n)O(n); nn lần lấy max O(nlogn)O(n\log n). Luôn O(nlogn)O(n\log n). S=O(1)S=O(1) (không kể stack sàng).

Ứng dụng thực tế

Cần O(nlogn)O(n\log n) không xấu n2n^2, không mảng phụ: xếp hạng điểm cả khoa. “Top kk” (hàng đợi ưu tiên, Ch.4) cùng ý đống.

Bẫy: chỉ số 2*i+1 kiểu 1-based nhầm thành 2*i; heapify sau swap quên giảm size.


2.2.3.d. Sắp xếp theo cơ số (Radix Sort)

Không so hai số nguyên với nhau. Xếp theo từng chữ số, từ hàng đơn vị (LSD) rồi chục, trăm…

Mỗi lượt: phân ổn định vào 10 thùng 0..9 theo chữ số đang xét, rồi đổ ra theo thứ tự thùng (giữ ổn định trong thùng).

Lưu đồ

Tìm max → d ← số chữ số

exp ← 1

┌─────────────────┐ không
│ max/exp > 0 ? ├────► Kết thúc
└──┬──────────────┘
có │

Counting theo chữ số (a[i]/exp)%10 ← ổn định, đi ngược

exp ← exp × 10 → lặp

Ví dụ [170, 45, 75, 90, 2, 24, 802, 66]ba lượt, mỗi lượt đổ thùng 0→9 (trong thùng giữ thứ tự ổn định):

Gốc: 170 45 75 90 2 24 802 66

ĐV (exp=1):
thùng 0: 170, 90
thùng 2: 2, 802
thùng 4: 24
thùng 5: 45, 75
thùng 6: 66
ra: 170, 90, 2, 802, 24, 45, 75, 66

Chục (exp=10):
thùng 0: 2, 802
thùng 2: 24
thùng 4: 45
thùng 6: 66
thùng 7: 170, 75
thùng 9: 90
ra: 2, 802, 24, 45, 66, 170, 75, 90

Trăm (exp=100):
thùng 0: 2, 24, 45, 66, 75, 90
thùng 1: 170
thùng 8: 802
ra: 2, 24, 45, 66, 75, 90, 170, 802

Nếu counting đi xuôi (i=0..n-1) khi đặt vào out, hai số cùng chữ số đảo thứ tự — lượt sau sai. Phải đi ngược.

Cài: mỗi lượt dùng đếm phân phối (counting) theo chữ số (a[i]/exp)%10, exp = 1, 10, 100, …

void CountingTheoChuSo(int a[], int n, int exp) {
int out[100]; // de n <= 100; hoac new int[n]
int cnt[10] = {0};
for (int i = 0; i < n; i++)
cnt[(a[i] / exp) % 10]++;
for (int d = 1; d < 10; d++)
cnt[d] += cnt[d - 1];
for (int i = n - 1; i >= 0; i--) { // di nguoc de on dinh
int d = (a[i] / exp) % 10;
out[cnt[d] - 1] = a[i];
cnt[d]--;
}
for (int i = 0; i < n; i++) a[i] = out[i];
}

void RadixSort(int a[], int n) {
int mx = a[0];
for (int i = 1; i < n; i++)
if (a[i] > mx) mx = a[i];
for (int exp = 1; mx / exp > 0; exp *= 10)
CountingTheoChuSo(a, n, exp);
}

Vòng i = n-1 xuống 0 + cộng dồn cntổn định từng lượt → cả Radix ổn định.

Thời gianO(d(n+k))O(d(n+k)), k=10k=10 (thập phân), dd = số chữ số của max
PhụO(n+k)O(n+k)
KhóaSố nguyên không âm (bản trên). Có âm: tách dấu / lệch miền
Ổn địnhCó (LSD + counting ngược)

dd nhỏ (MSSV 8 số, năm sinh 4 số): gần tuyến tính theo nn. Khóa float / char ten[] không nhét trực tiếp.

Toán

d=log10(max)+1d=\lfloor\log_{10}(\max)\rfloor+1, mỗi lượt counting O(n+k)O(n+k) với k=10k=10:

T=O(d(n+k))T=O\bigl(d(n+k)\bigr)

Không bị cận Ω(nlogn)\Omega(n\log n) của sort so sánh.

Ứng dụng thực tế

Sort MSSV, số báo danh, SĐT, năm sinh (chuỗi chữ số). Không sort họ tên (không phải chữ số thập phân thuần).

Bẫy: counting đi xuôi i=0..n-1 mất ổn định; quên exp*=10; int out[n] VLA.


e. Bảng chọn — tám thuật toán

Thuật toánTốtTBXấuPhụỔn địnhGhi nhớ
Interchangen2n^2n2n^2n2n^2O(1)O(1)KhôngĐổi ngay, ≠ Selection
Selectionn2n^2n2n^2n2n^2O(1)O(1)KhôngÍt swap
Insertionnnn2n^2n2n^2O(1)O(1)Gần sort
Bubble (+cờ)nnn2n^2n2n^2O(1)O(1)Chỉ kề
Mergenlognn\log nnlognn\log nnlognn\log nO(n)O(n)Luôn nlognn\log n
Quicknlognn\log nnlognn\log nn2n^2O(logn)O(\log n)KhôngPivot cuối + đã sort = xấu
Heapnlognn\log nnlognn\log nnlognn\log nO(1)O(1)KhôngĐống ≠ BST
Radixd(n+k)d(n+k)d(n+k)d(n+k)d(n+k)d(n+k)O(n+k)O(n+k)Số nguyên, LSD

Chọn nhanh:

n nhỏ / thi viết tay → 2.2.2 (biết khác Interchange/Selection)
cần ổn định, n lớn → Merge (hoặc Insertion nếu gần sort)
n lớn, không cần ổn định → Quick (tránh pivot xấu) hoặc Heap
khóa số nguyên, d nhỏ → Radix

Chương trình tối thiểu — Interchange + nhị phân

c++ -std=c++11 -o ch2 ch2.cpp && ./ch2
#include <iostream>
using namespace std;

void DoiCho(int &x, int &y) { int t = x; x = y; y = t; }

void InterchangeSort(int a[], int n) {
for (int i = 0; i < n - 1; i++)
for (int j = i + 1; j < n; j++)
if (a[i] > a[j]) DoiCho(a[i], a[j]);
}

int TimNhiPhan(int a[], int n, int x) {
int L = 0, R = n - 1;
while (L <= R) {
int M = (L + R) / 2;
if (a[M] == x) return M;
if (a[M] < x) L = M + 1;
else R = M - 1;
}
return -1;
}

int main() {
int a[] = {5, 1, 4, 2};
int n = 4;
InterchangeSort(a, n);
for (int i = 0; i < n; i++) cout << a[i] << " "; // 1 2 4 5
cout << "\nTim 4: " << TimNhiPhan(a, n, 4) << "\n"; // 2
cout << "Tim 3: " << TimNhiPhan(a, n, 3) << "\n"; // -1
return 0;
}

Ghép SelectionSort / InsertionSort / BubbleSort / QuickSort như mục trên.

Mẫu đếm đúng đề thực hành (ss so sánh khóa, gan gán; swap = 3 gán):

void SelectionSort(int a[], int n, int &ss, int &gan) {
ss = gan = 0;
for (int i = 0; i < n - 1; i++) {
int min = i;
for (int j = i + 1; j < n; j++) {
ss++;
if (a[j] < a[min]) min = j;
}
if (min != i) {
DoiCho(a[i], a[min]);
gan += 3;
}
}
}

Quick: cộng ss mỗi lần a[j] < pivot; gan++ khi pivot = a[high]; mỗi DoiChogan += 3. Copy mảng trước mỗi thuật toán — đừng sort mảng đã sort rồi so với thuật toán sau.

Ba bộ test thực hành: ngẫu nhiên / đã tăng (Insertion, Bubble+cờ ít việc; Quick+pivot cuối nhiều) / đã giảm.

Sort SinhVien (đề bài 3 Phần 1): so diem bằng < / >; so tên strcmp(a[j].ten, a[min].ten) < 0. Tìm MSSV: tuần tự + strcmp. Nhị phân theo MSSV chỉ sau khi đã sort theo MSSV.

Bẫy chương 2 (gom)

  1. Nhị phân trên mảng vừa Interchange chưa gọi.
  2. Nộp Selection mang tên Interchange.
  3. Quick pivot cuối trên mảng tăng → O(n2)O(n^2), không phải “luôn nlognn\log n”.
  4. Merge quên delete[].
  5. Heap dùng công thức BST.
  6. strcmp vs == khi sort/tìm SinhVien.

✅ Kiểm tra nhanh 2.2.3

  1. Merge luôn O(nlogn)O(n\log n) nhờ đâu?
  2. Quick xấu khi nào (bản đề thi)?
  3. Heap: a[0] sau bước dựng max-heap là gì?
  4. Radix LSD: lượt 1 theo chữ số nào?
  5. Cần ổn định + O(nlogn)O(n\log n) luôn: chọn?

Đáp án: (1) logn\log n tầng, mỗi tầng O(n)O(n). (2) Đã sort (pivot luôn biên). (3) Max. (4) Hàng đơn vị. (5) Merge.


2.3. BÀI TẬP

2.3 không nằm trong đề cương 2.1–2.2. Cầu nối thực hành Phần 1.

Làm trên giấy trước khi gõ. Mảng sort: [5,1,4,2] trừ khi bài cho khác.

A. Lý thuyết

Bài 1. Ba giải thuật tìm (tuần tự, nhị phân, nội suy): điều kiện mảng, OO xấu; một ví dụ thất bại của nhị phân khi chưa sort; khi nào nội suy chậm hơn nhị phân.

Bài 2. Ổn định là gì? Trong 2.2.2, thuật toán nào ổn định?

Bài 3. Interchange khác Selection một câu + một dòng code.

Bài 4. Insertion tốt / Bubble xấu / Quick xấu (pivot cuối).

Bài 5. Merge vs Quick vs Heap: OO xấu, phụ, ổn định — bảng 3 dòng.

Bài 6. Radix khác sort so sánh ở chỗ nào? Khóa nào không dùng trực tiếp?

Bài 6b. Vẽ lưu đồ (hình chữ nhật xử lý, hình thoi điều kiện) cho: (1) TimTuyenTinh, (2) TimNhiPhan, (3) InsertionSort hoặc BubbleSort. Đối chiếu mục tương ứng trong chương.

B. Chạy tay (bắt buộc)

Bài 7. Tuần tự [10,20,30,40,50], x=40x=40x=99x=99: ss.

Bài 8. Nhị phân [10,20,30,40,50,60,70,80], x=70x=70: bảng L,R,ML,R,M. x=35x=35: bảng đến thất bại.

Bài 8b. Nội suy [1,3,5,7,9,11,13,15,17,19], x=13x=13: tính pos từng phép. x=100x=100: vì sao không chia?

Bài 9. Interchange [5,1,4,2]: bảng i,ji,j, đổi, mảng. Đếm số đổi.

Bài 10. Selection cùng mảng: mỗi ii ghi min và có đổi không.

Bài 11. Insertion cùng mảng: mỗi key, mảng sau chèn.

Bài 12. Bubble (có cờ) cùng mảng: từng pass.

Bài 13. Partition Lomuto [5,1,8,2], pivot cuối. Mảng + chỉ số trả về.

Bài 14. Trộn [1,5] với [2,4] từng bước lấy phần tử.

Bài 15. Heap: mảng [4,10,3,5,1] — vẽ cây, heapify từ i=n/21i=n/2-1.

Bài 16. Radix một lượt ĐV: [170,45,75,90,2].

C. Cài đặt

Bài 17. TimTuyenTinh / TimNhiPhan / TimNoiSuy&ss. Test mảng tăng n=10n=10 (ví dụ giáo trình).

Bài 18. Bốn hàm 2.2.2. In mảng sau sort. Đếm ss, gan (swap = 3 gán).

Bài 19. QuickSort + partition đúng mẫu đề. Thử mảng tăng n=8n=8 — cảm O(n2)O(n^2) (nhiều lần partition lệch).

Bài 20. MergeSort. So ss không bắt buộc; kiểm tra mảng ra tăng.

Bài 21. HeapSort + RadixSort trên số không âm.

Bài 22. SinhVien: tìm tuần tự theo MSSV; InsertionSort theo diem; SelectionSort theo tên (strcmp). Khớp đề Phần 1 bài 3. Nêu vì sao chọn hai sort đó (ứng dụng, không chỉ copy đề).

D. Tự luận

  1. Bài toán tìm. Hai giải thuật. Khi nào sort rồi nhị phân đáng.
  2. Bốn sort đơn giản: ý tưởng một câu mỗi cái; Interchange ≠ Selection.
  3. Quick: partition; vì sao mảng tăng + pivot cuối xấu.
  4. Merge ổn định nhờ đâu (<=). Heap không ổn định vì sao (đổi cha–con xa).

Đáp án gợi ý B

7. 4; 5.
8. 7070: M=3,5,6M=3,5,6. 3535: như bảng 2.1.3.D.
8b. (131)×9/18=6(13-1)\times 9 / 18 = 6; x=100>19x=100>19-1 ngay.
9. 4 lần đổi; kết [1,2,4,5]. Sau i=0i=0: [1,5,4,2].
10. Đổi 2 lần (ô 0 và ô 1).
13. [1,2,8,5], trả 11.
14. 1, rồi 2, rồi 4, rồi 5.
16. Theo ĐV: 170, 90, 2, 45, 75.


🎯 TÓM TẮT CHƯƠNG 2

Kiến thức cốt lõi

  1. Tìm = trả chỉ số hoặc 1-1. Ba giải thuật giáo trình: tuần tự O(n)O(n); nhị phân O(logn)O(\log n) (đã sắp); nội suy tốt O(loglogn)O(\log\log n), xấu O(n)O(n) (đã sắp + đều).
  2. Sắp = thứ tự khóa. Ổn định / tại chỗ / OO tốt–xấu — ba câu hỏi khi nêu thuật toán.
  3. Interchange: đổi ngay với mọi jj sau. Selection: một min, một swap. Đừng nộp nhầm (Ch.3 SapXep = Interchange).
  4. Insertion / Bubble (+cờ) tốt O(n)O(n) khi đã tăng. Cả bốn đơn giản xấu O(n2)O(n^2).
  5. Merge luôn O(nlogn)O(n\log n), phụ O(n)O(n), ổn định. Quick TB O(nlogn)O(n\log n), xấu O(n2)O(n^2). Heap luôn O(nlogn)O(n\log n), phụ O(1)O(1). Radix O(d(n+k))O(d(n+k)) trên số nguyên.

Câu thần chú

  • Nhị phân: L = M+1 / R = M-1, điều kiện L <= R, mảng đã sort.
  • Nội suy: pos = L + ((x-a[L])*(R-L))/(a[R]-a[L]); cấm chia khi a[R]=a[L]a[R]=a[L] hoặc xx ngoài đoạn.
  • Thấy min_idx → Selection; hai vòng if (a[i] > a[j]) swap → Interchange.
  • Insertion: while (j>=0 && a[j] > key) — thiếu j>=0 vỡ.
  • Bubble chỉ a[j]a[j+1].
  • Quick đề thi: pivot a[high], không đệ quy vào ô pivot.
  • Mỗi thuật toán: lưu đồ (thoi = if) + toán T(n)T(n) + ứng dụng một câu trước khi code.
  • Radix: LSD, counting ngược để ổn định.

Liên kết

  • Ch.1: OO, n(n1)/2n(n-1)/2, tốt/xấu; mảng SinhVien.
  • Ch.3: SapXep = Interchange trên DSLK; Merge hợp liên kết.
  • Ch.5: cây hoàn chỉnh của Heap; BST khác Heap.
  • Thực hành Phần 1: đếm ss/gan; nội suy; QL sinh viên.

Hết chương 2. Làm bài 8–13 và 17–19 trước khi sang DSLK.