ĐỘ PHỨC TẠP THUẬT TOÁN (TIME & SPACE COMPLEXITY)

Để đánh giá một thuật toán "tốt" hay "xấu", người ta không dựa vào thời gian chạy tính bằng giây (vì thời gian này phụ thuộc vào cấu hình phần cứng của từng máy tính). Thay vào đó, người ta đo đếm số lượng phép tính cơ bản mà thuật toán thực hiện theo kích thước dữ liệu đầu vào $N$.

1. Phân loại Độ phức tạp

  1. Độ phức tạp thời gian (Time Complexity): Hàm số $T(N)$ biểu diễn số lượng phép tính máy tính phải thực hiện khi kích thước đầu vào là $N$.

  2. Độ phức tạp bộ nhớ (Space Complexity): Hàm số $S(N)$ biểu diễn lượng bộ nhớ bổ sung mà thuật toán cần cấp phát thêm trong quá trình vận hành.

2. Ký hiệu Big-O ($O$) – Đánh giá Trường hợp Xấu nhất (Worst-case)

Trong thực tế kỹ thuật, người ta quan tâm nhất đến giới hạn trên (Upper Bound) của khối lượng tính toán – tức là thuật toán sẽ tệ đến mức nào trong kịch bản xấu nhất. Ký hiệu Big-O biểu diễn tốc độ tăng trưởng của hàm số khi $N$ tiến đến vô cùng ($N \to \infty$).

Định nghĩa Toán học của Big-O:

Ta nói $T(N) = O(f(N))$ nếu tồn tại các hằng số dương $c$ và $N_0$ sao cho:

$$T(N) \le c \cdot f(N) \quad \forall N \ge N_0$$

Bảng các độ phức tạp Big-O kinh điển (Xếp theo chiều tăng dần về thời gian thực thi):

Ký hiệu Big-O Tên gọi Ví dụ thuật toán / Thao tác
$O(1)$ Hằng số (Constant) Truy cập phần tử mảng qua chỉ số arr[i], phép cộng/trừ cơ bản
$O(\log N)$ Lôgarit (Logarithmic) Tìm kiếm nhị phân (Binary Search)
$O(N)$ Tuyến tính (Linear) Duyệt mảng 1 vòng lặp để tìm Max/Min
$O(N \log N)$ Tuyến tính - Lôgarit Merge Sort, Quick Sort (trung bình)
$O(N^2)$ Bậc hai (Quadratic) 2 vòng lặp lồng nhau (Bubble Sort, Selection Sort)
$O(2^N)$ Mũ (Exponential) Đệ quy Fibonacci thuần túy, duyệt tất cả tập con
$O(N!)$ Giai thừa (Factorial) Duyệt tất cả hoán vị (Bài toán người giao hàng - TSP)

3. Quy tắc Rút gọn Ký hiệu Big-O

Khi phân tích một hàm số $T(N)$ phức tạp, ta áp dụng 2 quy tắc toán học:

  1. Bỏ qua hằng số nhân: $O(c \cdot f(N)) = O(f(N))$.

    • Ví dụ: Nếu thuật toán thực hiện $T(N) = 3N + 5$ phép tính, độ phức tạp vẫn là $O(N)$.

  2. Chỉ giữ lại cấp tăng trưởng cao nhất: Nếu $T(N) = f(N) + g(N)$ và $g(N)$ tăng trưởng chậm hơn $f(N)$, thì $O(T(N)) = O(f(N))$.

    • Ví dụ: $T(N) = N^2 + 100N + 5000 \Rightarrow O(N^2)$.

4. Mã nguồn Minh họa Phân tích Độ phức tạp

# 1. Độ phức tạp O(1) - Thời gian không đổi bất kể size của arr
def get_first_element(arr):
    return arr[0]  # Đúng 1 phép tính


# 2. Độ phức tạp O(N) - 1 vòng lặp chạy N lần
def print_all_elements(arr):
    for item in arr:  # Chạy N lần
        print(item)


# 3. Độ phức tạp O(N^2) - 2 vòng lặp lồng nhau
def print_all_pairs(arr):
    n = len(arr)
    for i in range(n):  # Vòng lặp ngoài: N lần
        for j in range(n):  # Vòng lặp trong: N lần
            print(arr[i], arr[j])  # Tổng số lần thực hiện: N * N = N^2

 

BÀI TẬP 

Bài tập 1: Phân tích Lý thuyết

Cho đoạn mã giả sau, hãy xác định độ phức tạp thời gian $O(f(N))$ trong kịch bản xấu nhất:

def complex_algorithm(arr):
    n = len(arr)

    # Đoạn 1
    for i in range(n):
        print(arr[i])

    # Đoạn 2
    for i in range(n):
        for j in range(n):
            if arr[i] == arr[j]:
                print("Match")

Bài tập 2: Thực hành Lập trình (Python)

Viết một hàm Python has_duplicate(arr) kiểm tra xem trong mảng arr có tồn tại ít nhất hai phần tử trùng nhau hay không.

  1. Yêu cầu 1: Viết hàm theo cách đơn giản nhất (dùng 2 vòng lặp lồng nhau). Xác định độ phức tạp thời gian $O(?)$ và bộ nhớ $O(?)$.

  2. Yêu cầu 2: Tìm cách tối ưu hàm trên để độ phức tạp thời gian đạt $O(N)$ bằng cách sử dụng cấu trúc dữ liệu set (Bảng băm).