Độ phức tạp thuật toán¶
Tóm lược nội dung
Bài này trình bày những khái niệm liên quan đến độ phức tạp của thuật toán, trong đó có:
- Giải thích ý nghĩa của Big O.
- Một số kỹ thuật xác định nhanh độ phức tạp.
Đặt vấn đề¶
Giả sử mảng gồm 100 phần tử đã sắp xếp tăng dần.
Khi cần kiểm tra xem giá trị nào đó có tồn tại trong mảng hay không, ta nên sử dụng thuật toán tìm kiếm tuần tự hay tìm kiếm nhị phân? Căn cứ vào đâu mà ta cho rằng thuật toán này nhanh hơn thuật toán kia?
Một cách khái quát, khi nhiều thuật toán giải quyết cùng một bài toán, thì thuật toán nào tốt hơn?
Khái niệm¶
Ta luôn muốn chương trình của mình thực thi một thuật toán tốt, theo nghĩa chạy nhanh và chiếm ít bộ nhớ. Có nhiều yếu tố dùng để đánh giá thuật toán tốt hay chưa tốt. Một trong số đó có tên gọi là độ phức tạp của thuật toán.
Độ phức tạp thuật toán
Độ phức tạp thuật toán là cách thức đánh giá thuật toán dựa trên lượng tài nguyên cần có để thực hiện thuật toán.
Theo đó, độ phức tạp thuật toán được phân thành hai loại: độ phức tạp không gian và độ phức tạp thời gian, ứng với hai dạng tài nguyên chính là bộ nhớ và thời gian.
-
Độ phức tạp không gian:
Dùng để xác định dung lượng bộ nhớ cần thiết để thực hiện thuật toán dựa trên kích thước dữ liệu đầu vào.
-
Độ phức tạp thời gian:
Dùng để xác định thời gian thực hiện thuật toán dựa trên kích thước dữ liệu đầu vào.
Cũng có tài liệu nêu rằng, độ phức tạp của thuật toán là độ đo đánh giá hiệu quả của thuật toán, chứ không phải cách thức đo.
Với sự phát triển của phần cứng ngày nay, các máy tính thường có đủ bộ nhớ để chạy chương trình. Do đó, độ phức tạp không gian có thể ít được đề cập hơn so với độ phức tạp thời gian. Và bài này chỉ trình bày về độ phức tạp thời gian của thuật toán, từ nay gọi tắt là độ phức tạp thuật toán.
Câu hỏi 1
Có bao nhiêu loại độ phức tạp thuật toán?
Đáp án
Có hai loại là không gian và thời gian. Đáp án: C.
Ý tưởng xác định độ phức tạp¶
Ý tưởng xác định độ phức tạp
Độ phức tạp phải được tính dựa trên sự tương quan giữa số lượng phép toán và kích thước của dữ liệu đầu vào.
Thời gian để thực hiện một chương trình được tính theo đơn vị giây (hoặc milli-giây), phụ thuộc vào nhiều yếu tố khác nhau như ngôn ngữ lập trình, trình biên dịch, hệ điều hành, tốc độ CPU, v.v... Vì thế, khi đánh giá thuật toán của chương trình, ta không dựa vào thời gian thực, mà cần một đơn vị tính khác, độc lập với những yếu tố trên. Cụ thể hơn, thuật toán cần được đánh giá trên cơ sở là bản thân thuật toán và dữ liệu đầu vào.
Phép toán tích cực
Phép toán tích cực là phép toán được thực hiện thường xuyên nhất trong thuật toán.
Theo ý tưởng này, độ phức tạp thời gian được xác định bằng cách ước lượng số phép toán tích cực cần thực hiện.
Câu hỏi 2
Phép toán tích cực trong đoạn mã sau nằm ở dòng nào?
Đáp án
Ta thấy phép toán so sánh bằng == (dòng 2) luôn được thực hiện dù i
chẵn hay lẻ, trong khi phép gán cho biến S
(dòng 3) chỉ thực hiện khi i
chẵn và phép gán cho biến P
(dòng 5) chỉ thực hiện khi i
lẻ.
Như vậy, phép toán tích cực trong vòng lặp trên là phép toán so sánh.
Phép toán tích cực bao gồm các phép toán sau:
- Phép so sánh
- Phép gán
- Phép toán số học
Big O¶
Ký hiệu Big O¶
Có ba loại trường hợp khác nhau khi thực hiện thuật toán:
- Trường hợp tốt nhất
- Trường hợp trung bình
- Trường hợp xấu nhất1
Ký hiệu Big O
Độ phức tạp thời gian của thuật toán được ký hiệu là \(O(g(n))\). Trong đó, \(g(n)\) là một hàm số theo \(n\), với \(n\) là kích thước của dữ liệu đầu vào.
Mỗi trường hợp có một ký hiệu riêng. Trong đó, ký hiệu \(O\), đọc là Big-Oh, có thể xem là phổ biến nhất và thường áp dụng cho trường hợp xấu nhất.
Cách xác định Big O¶
Gọi \(p\) là thời gian thực hiện một phép toán tích cực,
\(n\) là kích thước của dữ liệu đầu vào,
\(c(n)\) là hàm số theo \(n\) cho biết số lần thực hiện phép toán tích cực,
thì thời gian thực hiện thuật toán được định nghĩa là:
Ví dụ 1:
Áp dụng cho đoạn mã ở câu hỏi 2:
Giả sử mỗi phép toán tích cực được thực hiện trong một đơn vị thời gian, nghĩa là \(p = 1\);
và phép toán tích cực được thực hiện \(c(n) = n\) lần.
Như vậy, thời gian thực hiện đoạn mã là: \(T(n) = p . c(n) = 1 . n = n\)
Ví dụ 2:
Cho đoạn mã:
Phép toán tích cực trong đoạn mã này là phép cộng dồn cho biến S
.
Vì số lần lặp của for i
là \(n\) lần và số lần lặp của for j
là \(n\) lần, nên phép toán tích cực được thực hiện \(c(n) = n^2\) lần.
Như vậy, thời gian thực hiện đoạn mã là \(T(n) = p . c(n) = 1 . n^2 = n^2\)
Cách xác định Big O
Xét hàm \(g(n)\) không âm với mọi số nguyên \(n \ge 0\).
Nếu \(\exists n_0 \in \mathrm{Z}, \exists c \gt 0: \forall n \ge n_0\), \(g(n) \le c.f(n)\) thì ta nói rằng “g(n) là Big-Oh f(n)”, viết là \(g(n) = O(f(n))\).
Theo đó, Big O của các đoạn mã trên như sau:
- Trong ví dụ 1, vì \(T(n) = n\) nên \(g(n) = O(n)\)
- Trong ví dụ 2, vì \(T(n) = n^2\) nên \(g(n) = O(n^2)\)
Câu hỏi 3
Giả sử một đoạn mã có thời gian thực hiện là \(T(n) = 3n^2 + 7n + 5\).
Bạn hãy cho biết Big O của đoạn mã này.
Đáp án
Ta có: \(3n^2 + 7n + 5 = n^2(3 + \frac{7}{n} + \frac{5}{n^2}) \le 5n^2\) khi \(n > 7\)
Chọn \(c = 5, n_0 = 7, f(n) = n^2\) thì \(T(n) \le 5.f(n)\).
Vậy \(T(n) = O(n^2)\), hay Big O của đoạn mã là \(O(n^2)\).
Ý nghĩa của Big O¶
Khi phân tích độ phức tạp, ta thường tập trung vào trường hợp xấu nhất, là trường hợp cho thấy được giới hạn cận trên của thuật toán. Ký hiệu Big O cũng thường được sử dụng để chỉ trường hợp xấu nhất này.
Do đó, Big O được dùng để khái quát độ phức tạp của các thuật toán. Dựa vào Big O, ta có thể phân loại các thuật toán khi kích thước dữ liệu vào là như nhau. Nói cách khác,
Ý nghĩa của Big O
Với hai thuật toán có độ phức tạp lần lượt là \(O(f_1(n))\) và \(O(f_2(n))\):
Khi \(n\) đủ lớn, nếu \(f_1(n) < f_2(n)\) thì thuật toán có \(O(f_1(n))\) nhanh hơn thuật toán có \(O(f_2(n))\).
Câu hỏi 4
Bảng sau đây so sánh độ phức tạp của hai thuật toán tìm kiếm tuần tự và nhị phân. Dữ liệu đầu vào là mảng gồm \(n\) phần tử đã sắp xếp tăng dần.
Nội dung so sánh | Kích thước dữ liệu đầu vào | Tìm kiếm tuần tự | Tìm kiếm nhị phân |
---|---|---|---|
Số lần thực hiện phép toán tích cực | \(n = 10\) | 10 | 4 |
Số lần thực hiện phép toán tích cực | \(n = 100\) | 100 | 7 |
Độ phức tạp | \(O(n)\) | \(O(log n)\) 2 |
Dựa vào bảng này, bạn hãy cho biết thuật toán nào nhanh hơn?
Đáp án
Dễ thấy, \(log n < n\) nên thuật toán tìm kiếm nhị phân nhanh hơn thuật toán tìm kiếm tuần tự.
Một số Big O phổ biến¶
Bảng 1. Một số Big O phổ biến và tên gọi của chúng.
Big O | Tên gọi về thời gian thực thi |
---|---|
\(O(1)\) | Constant Time |
\(O(logn)\) | Logarithmic Time |
\(O(n)\) | Linear Time |
\(O(n logn)\) | Log Linear Time |
\(O(n^2)\) | Quadratic Time |
\(O(n^p)\) | Polynomial Time |
\(O(c^n)\) | Exponential Time |
\(O(n!)\) | Factorial Time |
Biểu đồ dưới đây biểu diễn các Big O trên, trừ \(O(n^p)\) nhằm tránh bị rối. Trong đó, các Big O được minh họa từ màu xanh, nghĩa là thuật toán chạy nhanh, chuyển dần cho đến màu đỏ, nghĩa là chạy rất chậm. Cũng xin lưu ý, biểu đồ chỉ là phác thảo mang tính minh họa, giúp người học dễ hình dung về các độ phức tạp, chứ không chính xác một cách chi tiết.
Hình 1. Biểu đồ minh họa sự tương quan giữa một số Big O
Quy tắc xác định độ phức tạp¶
Việc xác định độ phức tạp của một thuật toán bất kỳ là tương đối... phức tạp (!!!). Tuy nhiên, ta có thể áp dụng những quy tắc sau đây để tính Big O.
Quy tắc hằng số¶
-
Độ phức tạp của mỗi câu lệnh gán, đọc và ghi là \(O(1)\). Đây là độ phức tạp hằng số và không phụ thuộc dữ liệu đầu vào.
Câu hỏi 5
Dòng lệnh
x = 5
có độ phức tạp là gì?Đáp án
x = 5
là lệnh gán, độ phức tạp là \(O(1)\). -
\(O(c.f(n)) = O(f(n))\), với \(c\) là hằng số. Nói cách khác, ta có thể bỏ đi hằng số \(c\).
Hệ quả là, khi có nhiều lệnh gán, thì độ phức tạp vẫn được tính là \(O(1)\).
Quy tắc cộng¶
-
Đối với thuật toán có một vòng lặp, độ phức tạp được tính dựa trên số lần lặp.
-
Đối với thuật toán gồm hai đoạn mã tuần tự F1 và F2, nếu F1 có độ phức tạp \(O(f_1(n))\) và F2 có độ phức tạp \(O(f_2(n))\) thì độ phức tạp của cả thuật toán được xác định bằng đoạn mã có độ phức tạp lớn hơn.
\[ O(f_1(n)) + O(f_2(n)) = O(max(f_1(n), f_2(n))) \]Theo đó, quy tắc này còn được gọi là quy tắc lấy max.
Câu hỏi 8
Đoạn mã tính tổng các số chẵn và tổng các số lẻ từ 0 đến n sau có độ phức tạp là gì?
Đáp án
Vòng lặp F1 có \(\frac{n}{2}\) lần lặp, nên độ phức tạp là \(O(n)\). (Do hệ số \(\frac{1}{2}\) trong \(\frac{n}{2}\) được lược bỏ).
Tương tự, vòng lặp F2 có \(\frac{n}{2}\) lần lặp, nên độ phức tạp là \(O(n)\).
Thuật toán này có hai vòng lặp tuần tự F1 và F2 nên có độ phức tạp là \(max(O(n), O(n)) = O(n)\).
-
Đối với cấu trúc điều kiện if có dạng
vì độ phức tạp của câu lệnh kiểm tra
điều_kiện
thường là \(O(1)\) nên độ phức tạp sẽ là độ phức tạp lớn nhất củakhối_lệnh_1
hoặckhối_lệnh_2
.
Quy tắc nhân¶
Đối với thuật toán gồm hai cấu trúc lặp lồng nhau F1 và F2, nếu F1 có độ phức tạp \(O(f_1(n))\) và F2 có độ phức tạp \(O(f_2(n))\) thì độ phức tạp của thuật toán được xác định bằng độ phức tạp của tích \(f_1\) và \(f_2\).
Câu hỏi 10
Chương trình sau in ra các số nguyên tố nhỏ hơn n. Bạn hãy cho biết độ phức tạp của chương trình này.
Đáp án
Vòng lặp F1 có \(n – 1\) lần lặp, nên độ phức tạp là \(O(n)\).
Vòng lặp F2 có \(i – 1\) lần lặp, nên độ phức tạp là \(O(i)\). Vì \(i = 1, 2, ..., n\) nên độ phức tạp của vòng lặp F2 sẽ là \(O(n)\).
Thuật toán này có hai vòng lặp, trong đó F2 lồng trong F1, nên độ phức tạp là \(O(n) × O(n) = O(n^2)\).
Độ phức tạp của những thuật toán tìm kiếm và sắp xếp¶
Tóm tắt trình tự xác định độ phức tạp
- Xác định phép toán tích cực.
- Tính số lần thực hiện phép toán tích cực.
- Áp dụng các quy tắc ở mục trên để xác định độ phức tạp.
Thuật toán tìm kiếm tuần tự¶
Câu hỏi 11
Bạn hãy cho biết phép toán tích cực của chương trình trên nằm ở dòng số mấy?
Đáp án
Chủ thớt không tin rằng bạn không nhìn thấy phép toán tích cực, là phép so sánh, đã được làm nổi bật sẵn, ở dòng 3.
Câu hỏi 12
Dựa vào phép toán tích cực đó, bạn hãy cho biết độ phức tạp của thuật toán tìm kiếm tuần tự.
Đáp án
Trong trường hợp xấu nhất, phần tử có giá trị bằng k
nằm ở vị trí cuối cùng của mảng, phép toán tích cực được thực hiện \(n\) lần.
Vậy độ phức tạp của thuật toán tìm kiếm tuần tự là \(n.O(1) = O(n)\).
Thuật toán tìm kiếm nhị phân¶
Câu hỏi 13
Bạn hãy cho biết phép toán tích cực của chương trình trên nằm ở dòng số mấy?
Đáp án
Phép toán tích cực, là phép so sánh, cũng đã được tô sẵn ở dòng 8.
Câu hỏi 14
Bạn hãy đoán xem độ phức tạp của thuật toán tìm kiếm nhị phân là gì.
Đáp án
Giả sử phép toán tích cực này phải được thực hiện \(t\) lần thì mới tìm thấy k
.
- Lần 1: Số phần tử còn lại cần xét là \(\frac{n}{2}=\frac{n}{2^1}\).
- Lần 2: Số phần tử còn lại cần xét là \(\frac{n}{4}=\frac{n}{2^2}\).
- Lần 3: Số phần tử còn lại cần xét là \(\frac{n}{8}=\frac{n}{2^3}\).
- ...
Suy ra, đối với lần \(t\), số phần tử còn lại cần xét là \(\frac{n}{2^t}\).
Mà ở lần \(t\), là lần tìm thấy k
, số phần tử còn lại là \(1\).
Ta có:
\(\frac{n}{2^t} = 1 \Rightarrow 2^t = n \Rightarrow t = log_2(n)\)
Vậy độ phức tạp của thuật toán tìm kiếm nhị phân là \(O(log n)\).
Thuật toán sắp xếp nổi bọt¶
Câu hỏi 15
Bạn hãy cho biết phép toán tích cực của chương trình trên nằm ở dòng số mấy?
Đáp án
Phép toán tích cực là phép so sánh ở dòng 6.
Mẹo vặt
Với các đoạn mã gồm nhiều vòng lặp lồng nhau, phép toán tích cực thường nằm ở vòng lặp trong cùng.
Câu hỏi 16
Bạn hãy dựa vào quy tắc nhân để tính độ phức tạp của thuật toán sắp xếp nổi bọt.
Đáp án
Xét vòng lặp của biến j
, trong trường hợp i
= 0, phép so sánh sẽ được thực hiện \(n – 1\) lần. Theo quy tắc hằng số, ta có thể bỏ qua \(-1\). Như vậy, độ phức tạp của vòng lặp này là \(O(n)\).
Xét vòng lặp của i
, thân của vòng lặp này có thể được thực hiện \(n\) lần.
Do đó, theo quy tắc nhân, độ phức tạp của thuật toán sắp xếp nổi bọt là \(O(n).O(n) = O(n.n) = O(n^2)\).
Sơ đồ tóm tắt nội dung¶
Some English words¶
Vietnamese | Tiếng Anh |
---|---|
độ phức tạp thuật toán | algorithmic complexity |
độ phức tạp không gian | space complexity |
độ phức tạp thời gian | time complexity |
giây, milli-giây | second, millisecond |
ký hiệu \(O\) | Big O notation |
phép toán tích cực | dominant operation |
-
Trường hợp tốt nhất: tạm hiểu là thuật toán hoàn tất nhiệm vụ trong thời gian ngắn nhất có thể.
Trường hợp trung bình: tạm hiểu là khả năng thực hiện trung bình của thuật toán đối với một lượng lớn dữ liệu đầu vào.
Trường hợp xấu nhất: là trường hợp mà số lần thực hiện phép toán tích cực nhiều nhất. ↩ -
Trong tin học, \(log n\) thường được hiểu là hàm logarit cơ số 2 của \(n\). Chẳng hạn ở ví dụ 5, \(log_2{100} = 7\). ↩