Bài 26.322 phút đọc
Độ phức tạp và cách chọn
Sau bài này bạn sẽ làm được
- Đọc và viết ký hiệu O lớn cho các đoạn mã đơn giản
- So sánh tìm tuyến tính, tìm nhị phân và bảng băm
- Chọn đúng cách tìm cho từng tình huống
- Biết hằng số ẩn quan trọng thế nào khi n nhỏ
Ký hiệu O lớn nói về cách chi phí tăng khi dữ liệu lớn lên, và nó cố tình bỏ qua mọi hằng số. Điều đó làm nó rất hữu ích để so sánh thuật toán, và rất dễ gây hiểu nhầm khi dữ liệu nhỏ.
#Ký hiệu O lớn
O lớn
Viết
f(n) = O(g(n)) nghĩa là tồn tại hằng số c và n0 sao cho f(n) <= c * g(n) với mọi n >= n0. Nói cách khác, g là cận trên của tốc độ tăng, bỏ qua hằng số nhân và bỏ qua các giá trị n nhỏ.| Độ phức tạp | Tên gọi | n = 1000 tốn | n = 1000000 tốn |
|---|---|---|---|
| O(1) | Hằng số | 1 | 1 |
| O(log n) | Lô ga rít | 10 | 20 |
| O(n) | Tuyến tính | 1 nghìn | 1 triệu |
| O(n log n) | Tuyến tính lô ga rít | 10 nghìn | 20 triệu |
| O(n bình phương) | Bình phương | 1 triệu | 1 nghìn tỷ |
| O(2 mũ n) | Mũ | Không tưởng | Không tưởng |
| O(n giai thừa) | Giai thừa | Không tưởng | Không tưởng |
#Đọc độ phức tạp từ mã
doc-ma.c
/* O(1): số thao tác không phụ thuộc n */
int dau_tien(const int *a, size_t n) { return n ? a[0] : -1; }
/* O(n): một vòng lặp chạy n lần */
long tong(const int *a, size_t n)
{
long s = 0;
for (size_t i = 0; i < n; ++i) s += a[i];
return s;
}
/* O(n^2): hai vòng lồng nhau, mỗi vòng n lần */
int co_cap_bang_nhau(const int *a, size_t n)
{
for (size_t i = 0; i < n; ++i)
for (size_t j = i + 1; j < n; ++j)
if (a[i] == a[j]) return 1;
return 0;
}
/* O(log n): mỗi bước chia đôi phạm vi */
size_t tim_nhi_phan(const int *a, size_t n, int x);
/* O(n log n): vòng ngoài n lần, mỗi lần gọi một việc O(log n) */
void chen_het_vao_cay(BST *t, const int *a, size_t n)
{
for (size_t i = 0; i < n; ++i) bst_chen(t, a[i]);
}
/* O(n + m): hai vòng lặp NỐI TIẾP, không lồng nhau */
void in_hai_mang(const int *a, size_t n, const int *b, size_t m)
{
for (size_t i = 0; i < n; ++i) printf("%d ", a[i]);
for (size_t j = 0; j < m; ++j) printf("%d ", b[j]);
}#Ba cách tìm đặt cạnh nhau
| Tuyến tính | Nhị phân | Bảng băm | |
|---|---|---|---|
| Độ phức tạp tìm | O(n) | O(log n) | O(1) trung bình |
| Xấu nhất | O(n) | O(log n) | O(n) |
| Cần dữ liệu đã sắp | Không | Có | Không |
| Chi phí chuẩn bị | 0 | O(n log n) để sắp | O(n) để dựng bảng |
| Bộ nhớ phụ | 0 | 0 | O(n) |
| Chèn một phần tử | O(1) | O(n) vì phải dịch | O(1) khấu hao |
| Xóa một phần tử | O(n) | O(n) | O(1) khấu hao |
| Duyệt theo thứ tự | O(n log n) phải sắp | O(n), rất nhanh | Không làm được |
| Truy vấn theo khoảng | O(n) | O(log n + k) | Không làm được |
| n = 1 000 000 tốn | 500 nghìn phép | 20 phép | 1 phép |
terminal
./do-ba-cach 1000000 1000000
1000000 phan tu, 1000000 lan tim
chuan bi tim tong
tuyen tinh 0.000 s 412.4 s 412.4 s
sap + nhi phan 0.089 s 0.142 s 0.231 s
bang bam 0.104 s 0.038 s 0.142 s# Nhưng chỉ tìm 10 lần thì bức tranh đảo ngược
./do-ba-cach 1000000 10
1000000 phan tu, 10 lan tim
chuan bi tim tong
tuyen tinh 0.000 s 0.0041 s 0.0041 s
sap + nhi phan 0.089 s 0.0000 s 0.0890 s
bang bam 0.104 s 0.0000 s 0.1040 s#Hằng số ẩn quan trọng thế nào
O lớn bỏ qua hằng số, và với n nhỏ thì hằng số quyết định tất cả. Đây là sai lầm phổ biến nhất khi áp dụng phân tích tiệm cận.
hang-so.c
/* Thuật toán A: O(n^2) với hằng số 1
Thuật toán B: O(n log n) với hằng số 100
n = 10 : A = 100, B = 3320 -> A nhanh hơn 33 lần
n = 100 : A = 10000, B = 66400 -> A nhanh hơn 6.6 lần
n = 1000 : A = 1000000, B = 996000 -> hòa
n = 10000: A = 100000000, B = 13300000 -> B nhanh hơn 7.5 lần
Điểm hòa vốn ở khoảng n = 1000. Dưới ngưỡng đó, thuật toán
"tệ hơn về tiệm cận" lại nhanh hơn thật. */terminal
./do-insertion-vs-quick
n insertion (O(n^2)) quick (O(n log n)) ben nao nhanh hon
8 0.09 us 0.41 us insertion
16 0.28 us 0.72 us insertion
32 1.02 us 1.38 us insertion
64 3.81 us 2.94 us quick
128 14.90 us 6.12 us quick
1024 912.00 us 54.80 us quick| Nguồn của hằng số ẩn | Ảnh hưởng |
|---|---|
| Truy cập bộ nhớ tuần tự hay ngẫu nhiên | Chênh nhau 10 tới 100 lần, xem Bài 21.1 |
| Lời gọi hàm qua con trỏ | Chậm hơn 3 tới 15 lần vì không nội tuyến được |
| Cấp phát động | Mỗi lần malloc tốn hàng chục tới hàng trăm chu kỳ |
| Nhánh dự đoán sai | Mỗi lần sai tốn 15 tới 20 chu kỳ |
| Phép chia và chia dư | Chậm hơn phép cộng 20 tới 40 lần |
#Chọn cách nào cho tình huống nào
| Tình huống | Chọn | Vì sao |
|---|---|---|
| Tìm một lần trong mảng chưa sắp | Tuyến tính | Sắp xếp tốn O(n log n), không bù lại được |
| Tìm nhiều lần, chỉ cần khớp chính xác | Bảng băm | O(1) và dựng bảng chỉ tốn O(n) |
| Tìm nhiều lần, cần cả truy vấn theo khoảng | Mảng đã sắp hoặc cây | Bảng băm không làm được truy vấn khoảng |
| Dữ liệu thay đổi liên tục, cần thứ tự | Cây cân bằng | Mảng đã sắp chèn tốn O(n) |
| Dữ liệu thay đổi liên tục, không cần thứ tự | Bảng băm | Chèn và xóa đều O(1) khấu hao |
| n dưới 50 và không đổi nhiều | Mảng, tìm tuyến tính | Hằng số nhỏ nhất, mã đơn giản nhất |
| Cần bảo đảm ở trường hợp xấu nhất | Cây cân bằng | Bảng băm có thể tụt về O(n) khi bị tấn công |
Bảng tra nhanh cho cả Phần 10 và 11
| Cấu trúc | Tìm | Chèn | Xóa | Duyệt theo thứ tự |
|---|---|---|---|---|
| Mảng chưa sắp | O(n) | O(1) ở cuối | O(n) | Phải sắp trước |
| Mảng đã sắp | O(log n) | O(n) | O(n) | O(n) |
| Danh sách liên kết | O(n) | O(1) nếu có con trỏ | O(1) nếu có con trỏ | O(n) |
| Cây tìm kiếm cân bằng | O(log n) | O(log n) | O(log n) | O(n) |
| Bảng băm | O(1) TB | O(1) TB | O(1) TB | Không làm được |
| Heap | O(n) | O(log n) | O(log n) chỉ ở gốc | Không làm được |
Tự làm thử
- Viết bảng thời gian ước tính cho sáu độ phức tạp với
ntừ 10 tới một tỷ, giả sử một tỷ thao tác mỗi giây. - Với năm đoạn mã bạn từng viết, xác định độ phức tạp của từng đoạn và giải thích cách đếm.
- Đo thời gian ba cách tìm trên một triệu phần tử với số lần tìm bằng 1, 10, 100 và một triệu.
- Tìm điểm hòa vốn giữa insertion sort và quick sort trên máy bạn, rồi so với ngưỡng 16 mà nhiều thư viện dùng.
- Viết một hàm O(n) nhưng có hằng số lớn, và một hàm O(n log n) có hằng số nhỏ, rồi tìm
nmà chúng đổi ngôi.
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
- O lớn mô tả tốc độ tăng của chi phí, bỏ qua hằng số nhân và bỏ qua các giá trị
nnhỏ. - Đọc độ phức tạp bằng cách đếm tổng số lần thân vòng lặp trong cùng chạy, không phải nhân số vòng lặp với nhau.
- Tìm tuyến tính không cần chuẩn bị, tìm nhị phân cần sắp trước, bảng băm cần dựng bảng. Điểm hòa vốn thường chỉ vài chục lần tìm.
- Hằng số ẩn quyết định tất cả khi
nnhỏ, và bộ nhớ đệm là nguồn chênh lệch lớn nhất mà O lớn hoàn toàn mù trước nó. - Dùng O lớn để loại thuật toán không khả thi, nhưng phải đo để chọn giữa hai ứng viên hợp lý.