Độ phức tạp thuật toán (Big O Notation)
Big O là gì?
Big O Notation là cách đo lường hiệu suất của thuật toán dựa trên:
- Thời gian (Time Complexity): Thuật toán chạy nhanh hay chậm?
- Không gian (Space Complexity): Thuật toán dùng bao nhiêu bộ nhớ?
Big O mô tả trường hợp xấu nhất (worst case) và cho biết thuật toán sẽ “scale” như thế nào khi dữ liệu tăng lên.
Các độ phức tạp phổ biến
| Big O | Tên gọi | Ví dụ |
|---|---|---|
| O(1) | Constant | Truy cập phần tử mảng |
| O(log n) | Logarithmic | Binary Search |
| O(n) | Linear | Duyệt mảng |
| O(n log n) | Linearithmic | Merge Sort, Quick Sort |
| O(n²) | Quadratic | Nested loops |
| O(2ⁿ) | Exponential | Fibonacci đệ quy |
| O(n!) | Factorial | Permutations |
O(1) - Constant Time
Thời gian thực thi không đổi dù dữ liệu lớn đến đâu.
Python
def get_first(arr):
return arr[0] # O(1)
def add_numbers(a, b):
return a + b # O(1)
# Hash table lookup
my_dict = {"name": "John", "age": 30}
print(my_dict["name"]) # O(1)O(log n) - Logarithmic Time
Mỗi bước chia đôi dữ liệu cần xử lý.
Python
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1O(n) - Linear Time
Thời gian tăng tỷ lệ thuận với kích thước dữ liệu.
Python
def find_max(arr):
max_val = arr[0]
for num in arr: # Duyệt n phần tử
if num > max_val:
max_val = num
return max_valO(n²) - Quadratic Time
Thường do nested loops - vòng lặp lồng nhau.
Python
def bubble_sort(arr):
n = len(arr)
for i in range(n): # O(n)
for j in range(n - 1): # O(n)
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
# Tổng: O(n × n) = O(n²)O(n²) trở lên thường không chấp nhận được với dữ liệu lớn. Hãy tìm cách tối ưu!
Space Complexity
Bộ nhớ sử dụng theo kích thước input.
Python
# O(1) Space - In-place
def reverse_inplace(arr):
left, right = 0, len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1
# O(n) Space - Tạo mảng mới
def reverse_copy(arr):
return arr[::-1] # Tạo bản sao mớiQuy tắc tính Big O
1. Bỏ qua hằng số (Drop Constants)
Khi phân tích Big O, ta bỏ qua các hằng số nhân vì chúng không ảnh hưởng đến tốc độ tăng của thuật toán.
Giải thích: O(2n), O(3n), O(100n) đều tăng tuyến tính theo n, nên đều quy về O(n).
Python
def example(arr):
# Duyệt mảng 1 lần: n phép
for x in arr:
print(x)
# Duyệt mảng lần 2: n phép
for x in arr:
print(x * 2)
# Tổng: 2n phép → O(2n) → O(n)2. Giữ lại số hạng lớn nhất (Drop Non-Dominant Terms)
Khi có nhiều số hạng, chỉ giữ lại số hạng tăng nhanh nhất vì nó sẽ “thống trị” khi n lớn.
Giải thích: Với n = 1000: n² = 1,000,000 nhưng n chỉ = 1000. Số hạng n “không đáng kể” so với n².
| Biểu thức | Đơn giản |
|---|---|
| O(n² + n) | O(n²) |
| O(n³ + n² + n) | O(n³) |
| O(n + log n) | O(n) |
| O(2ⁿ + n²) | O(2ⁿ) |
Python
def example(arr):
n = len(arr)
# Phần 1: O(n²)
for i in range(n):
for j in range(n):
print(i, j)
# Phần 2: O(n)
for i in range(n):
print(i)
# Tổng: O(n² + n) → O(n²)3. Các phép toán liên tiếp = Cộng (Add for Sequential)
Khi có nhiều đoạn code chạy tuần tự (không lồng nhau), ta cộng độ phức tạp.
Giải thích: Nếu duyệt mảng A kích thước a, rồi duyệt mảng B kích thước b → O(a + b).
Python
def process_two_arrays(arr1, arr2):
# Duyệt arr1: O(a) với a = len(arr1)
for x in arr1:
print(x)
# Duyệt arr2: O(b) với b = len(arr2)
for y in arr2:
print(y)
# Tổng: O(a) + O(b) = O(a + b)
# LƯU Ý: Không phải O(n) vì 2 mảng có kích thước khác nhau!4. Vòng lặp lồng nhau = Nhân (Multiply for Nested)
Khi có vòng lặp lồng bên trong vòng lặp khác, ta nhân độ phức tạp.
Giải thích: Vòng ngoài chạy a lần, mỗi lần vòng trong chạy b lần → Tổng: a × b lần.
Python
def nested_loops(arr1, arr2):
# Với mỗi phần tử của arr1 (a lần)
for x in arr1:
# Duyệt toàn bộ arr2 (b lần)
for y in arr2:
print(x, y)
# Tổng: O(a × b)
# Nếu a = b = n thì O(n²)Phân biệt O(a + b) và O(a × b):
- Tuần tự (sequential):
for Arồifor B→ O(a + b) - Lồng nhau (nested):
for A { for B }→ O(a × b)
Tóm tắt
| Cần nhớ |
|---|
| O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) |
| Nested loops → O(n²) |
| Chia đôi mỗi bước → O(log n) |
| Tối ưu từ O(n²) → O(n) thường dùng Hash Table |
Tiếp theo: Mảng (Array)