Skip to content

Độ 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độ 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ánkí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?

1
2
3
4
5
for i in range(n):
    if i % 2 == 0:
        S = S + i
    else:
        P = P * i

Đá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ẻ.

1
2
3
4
5
for i in range(n):
    if i % 2 == 0:
        S = S + i
    else:
        P = P * i

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à:

\[ T(n) = p . c(n) \]

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ã:

1
2
3
for i in range(n):
    for j in range(n):
        S = S + a[i][j]

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\(n\) lần và số lần lặp của for j\(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))\)\(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ố

  1. Độ phức tạp của mỗi câu lệnh gán, đọcghi\(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)\).

  2. \(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)\).

    Câu hỏi 6

    Đoạn mã hoán vị hai biến ab sau có độ phức tạp là?

    1
    2
    3
    t = a
    a = b
    b = t
    



    Đáp án

    Độ phức tạp của đoạn mã trên là \(3.O(1) = O(1)\).

Quy tắc cộng

  1. Đố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.

    Câu hỏi 7

    Đoạn mã tính tổng từ 1 đến n sau có độ phức tạp là gì?

    for i in range(n):
        S = S + i
    



    Đáp án

    Dòng lệnh 2 có độ phức tạp là \(O(1)\) và lệnh này được thực hiện \(n\) lần.

    Do đó, độ phức tạp là \(n.O(1) = O(n)\).

  2. Đố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ì?

    1
    2
    3
    4
    5
    6
    7
    8
    n = 10
    sum_even, sum_odd = 0, 0
    
    for i in range(0, n, 2):      # Đoạn mã F1 gồm dòng lệnh 4 và 5
        sum_even = tong_even + i
    
    for i in range(1, n, 2):      # Đoạn mã F2 gồm dòng lệnh 7 và 8
        sum_odd = sum_odd  + i
    



    Đáp án

    Vòng lặp F1\(\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\(\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)\).

  3. Đối với cấu trúc điều kiện if có dạng

    if điều_kiện:
        khối_lệnh_1
    else:
        khối_lệnh_2
    

    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ủa khối_lệnh_1 hoặc khối_lệnh_2.

    Câu hỏi 9

    Đoạn mã tính tổng dương và tổng âm theo điều kiện của x sau có độ phức tạp là gì?

    1
    2
    3
    4
    if x > 0:
        sum_positive = sum_positive + x
    else:
        sum_negative = sum_negative + x
    



    Đáp án

    Dòng lệnh 2 và 4 đều có độ phức tạp là \(O(1)\), nên độ phức tạp của thuật toán này là \(O(1)\).

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\)\(f_2\).

\[ O(f_1(n)).O(f_2(n)) = O(f_1(n).f_2(n)) \]

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.

def print_prime_numbers(n):
    for i in range(2, n):      # Vòng lặp F1
        prime = True

        for j in range(2, i):  # Vòng lặp F2
            if i % j == 0:
                prime = False

        if prime == True:
            print(i)



Đáp án

Vòng lặp F1\(n – 1\) lần lặp, nên độ phức tạp là \(O(n)\).

Vòng lặp F2\(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

  1. Xác định phép toán tích cực.
  2. Tính số lần thực hiện phép toán tích cực.
  3. Á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ự

1
2
3
4
5
6
def linear_search(A, k):
    for i in range(len(A)):
        if A[i] == k:
            return i

    return -1

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

def binary_search(A, k):
    left = 0
    right = len(A) - 1

    while left <= right:
        mid = (left + right) // 2

        if A[mid] == k:
            return mid
        elif A[mid] < k:
            left = mid + 1
        else:
            right = mid - 1

    return -1

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

1
2
3
4
5
6
7
def bubble_sort(A):
    n = len(A)

    for i in range(n - 1):
        for j in range(n - 1 - i):
            if A[j] > A[j + 1]:
                A[j], A[j + 1] = A[j + 1], A[j]

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

Hình 2. Sơ đồ tóm tắt độ phức tạp thuật toán

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

  1. 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. 

  2. 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\)