Big-O
Nội dung bài · 6 mục
Cửa hàng có 5 sản phẩm thì cách tìm nào cũng nhanh. Lên 5 triệu sản phẩm, có cách vẫn trả lời ngay, có cách chạy mãi không xong. Big-O cho bạn biết trước một cách giải chậm đi thế nào khi dữ liệu lớn lên.
Khái niệm
📈 Big-O: cách mô tả số bước của một thuật toán tăng thế nào khi số phần tử n tăng, bỏ qua các hằng số.
Big-O không đo giây, vì mỗi máy chạy nhanh chậm khác nhau. Nó đếm số bước và chỉ xem số bước tăng theo kiểu nào: dữ liệu gấp đôi thì số bước gấp đôi, gấp bốn, hay gần như không đổi.
| Big-O | Đọc là | Ví dụ | 1.000 phần tử |
|---|---|---|---|
| O(1) | hằng số | list[2], dict[key] (trung bình) |
1 bước |
| O(log n) | logarit | tìm nhị phân trên dãy đã sắp xếp | ~10 bước |
| O(n) | tuyến tính | duyệt list tìm một tên | 1.000 bước |
| O(n log n) | n log n | merge sort | ~10.000 bước |
| O(n²) | bình phương | hai vòng lặp lồng nhau | 1.000.000 bước |
Ví dụ
List<string> names = new List<string>
{
"Bút bi", "Vở", "Balo", "Thước", "Máy tính"
};
// O(1): lấy theo vị trí, luôn chỉ một bước
Console.WriteLine(names[2]); // Balo
// O(n): tìm theo tên, xấu nhất phải xem hết list
int steps = 0;
foreach (string name in names)
{
steps++;
if (name == "Máy tính")
{
break;
}
}
Console.WriteLine(steps); // 5names[2]nhảy thẳng tới ô thứ ba, không cần nhìn các ô khác.- Tìm "Máy tính" phải so từng tên từ đầu. List 5 triệu tên thì xấu nhất là 5 triệu lần so.
- Big-O thường tính theo trường hợp xấu nhất: tên cần tìm nằm cuối, hoặc không có. Chỗ nào ghi "trung bình" là nói trường hợp thường gặp.
Vòng lặp lồng nhau
Vòng ngoài chạy n lần. Mỗi lần đó, vòng trong lại chạy n lần, nên tổng
cộng là n × n bước: O(n²).
Console.WriteLine(CountSteps(10)); // 100
int CountSteps(int n)
{
int steps = 0;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
steps++;
}
}
return steps;
}▶ Mô phỏng: Đếm bước của hai vòng lặp lồng nhau
Console.WriteLine(CountSteps(4));
int CountSteps(int n)
{
int steps = 0;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
steps++;
}
}
return steps;
}Xem từng bước
- i = 0: vòng trong chạy lại từ j = 0. steps = 1.
- i = 0, j = 1: steps = 2.
- i = 0, j = 2: steps = 3.
- i = 0, j = 3: steps = 4.
- i = 1: vòng trong chạy lại từ j = 0. steps = 5.
- i = 1, j = 1: steps = 6.
- i = 1, j = 2: steps = 7.
- i = 1, j = 3: steps = 8.
- i = 2: vòng trong chạy lại từ j = 0. steps = 9.
- i = 2, j = 1: steps = 10.
- i = 2, j = 2: steps = 11.
- i = 2, j = 3: steps = 12.
- i = 3: vòng trong chạy lại từ j = 0. steps = 13.
- i = 3, j = 1: steps = 14.
- i = 3, j = 2: steps = 15.
- i = 3, j = 3: steps = 16.
- Trả về 16 = 4 × 4. Gấp đôi n thì số bước gấp bốn: O(n²).
- In 16. Thử đổi n ở ô "Tự thử" để thấy số bước tăng theo n × n.
Thử ngay
Thêm hai dòng sau vào dưới dòng gọi CountSteps(10):
Console.WriteLine(CountSteps(100));
Console.WriteLine(CountSteps(1000));Đoán trước khi chạy: mỗi lượt n tăng 10 lần, từ 10 lên 100 rồi 1000.
Số bước tăng bao nhiêu lần?
Xem kết quả
100
10000
1000000Mỗi lượt tăng 100 lần, vì n tăng 10 lần thì n × n tăng 10 × 10 lần. Với
O(n²), dữ liệu lớn gấp 10 là chờ lâu gấp 100.
Lỗi hay gặp
Gọi Contains của List trong vòng lặp. Code chỉ có một vòng lặp,
nhưng mỗi lần gọi, Contains lại duyệt cả list, nên tổng cộng là O(n²).
// SAI — O(n²): Contains duyệt lại list mỗi vòng
List<string> seen = new List<string>();
foreach (string name in names)
{
if (seen.Contains(name))
{
Console.WriteLine("Trùng: " + name);
}
seen.Add(name);
}// ĐÚNG — O(n): ContainsKey của Dictionary là O(1)
Dictionary<string, bool> seen =
new Dictionary<string, bool>();
foreach (string name in names)
{
if (seen.ContainsKey(name))
{
Console.WriteLine("Trùng: " + name);
}
seen[name] = true;
}Vì sao ContainsKey chỉ tốn một bước là chuyện của chương Bảng băm.
Tóm tắt
- Big-O mô tả số bước tăng thế nào khi dữ liệu tăng, không đo giây.
- Lấy theo vị trí là O(1), duyệt tìm là O(n), hai vòng lồng nhau là O(n²).
- Thường tính theo trường hợp xấu nhất và bỏ qua hằng số.
- Cẩn thận với method có vòng lặp ẩn bên trong, như
List.Contains.
Bình luận
Đang tải bình luận…