Back to blog

Big O Notation & Phân Tích Độ Phức Tạp: Hướng Dẫn Toàn Diện

algorithmsdata-structuresbig-ocomplexitycomputer-science
Big O Notation & Phân Tích Độ Phức Tạp: Hướng Dẫn Toàn Diện

Giới Thiệu

Tưởng tượng bạn đang ở thư viện với 1 triệu cuốn sách. Mất bao lâu để tìm một cuốn sách cụ thể?

  • Thư viện không sắp xếp: Có thể phải kiểm tra mọi cuốn → chậm
  • Sắp xếp theo tác giả: Dùng binary search → nhanh
  • Hệ thống số hóa: Tra cứu tức thì → instant

Đây chính là Big O notation: đo lường cách thuật toán scale khi input size tăng. Đây là ngôn ngữ developers dùng để thảo luận về hiệu suất thuật toán, và nó cần thiết cho việc viết code nhanh, scalable và ace technical interviews.

Những Gì Bạn Sẽ Học

✅ Hiểu Big O notation và tại sao nó quan trọng
✅ Thành thạo common time complexities: O(1), O(log n), O(n), O(n log n), O(n²), O(2ⁿ)
✅ Tính Big O cho loops, recursion, và nested structures
✅ Phân tích space complexity và memory usage
✅ Hiểu best, average, và worst-case scenarios
✅ Học time vs space trade-offs
✅ Áp dụng complexity analysis trong TypeScript và Python
✅ Thành thạo interview techniques cho việc thảo luận algorithm efficiency


1. Big O Notation Là Gì?

Big O notation mô tả cách runtime hoặc space requirements của thuật toán tăng khi input size tăng. Nó trả lời: "Thuật toán này scale như thế nào?"

Khái Niệm Chính

Input Size (n): Kích thước data bạn đang làm việc với

  • Array 10 items → n = 10
  • Array 1 triệu items → n = 1,000,000

Big O (O): Mô tả worst-case upper bound của growth

  • O(n) có nghĩa "at most linear growth"
  • O(1) có nghĩa "constant, không tăng"

Đọc Big O Notation

O(1)      → "O of 1" hoặc "constant time"
O(n)      → "O of n" hoặc "linear time"
O(log n)  → "O of log n" hoặc "logarithmic time"
O(n²)     → "O of n squared" hoặc "quadratic time"
O(2ⁿ)     → "O of 2 to the n" hoặc "exponential time"

2. Common Time Complexities

O(1) - Constant Time

Định nghĩa: Operation mất cùng thời gian bất kể input size.

Ví dụ:

  • Truy cập array element theo index
  • Lấy value từ hash table/object
  • Phép toán số học đơn giản

TypeScript:

// O(1) - Constant time
function getFirstElement<T>(arr: T[]): T | undefined {
    return arr[0];  // Luôn một operation, bất kể array size
}
 
// O(1) - Hash table lookup
function getUserAge(users: Map<string, number>, userId: string): number | undefined {
    return users.get(userId);  // Instant lookup
}

Python:

# O(1) - Constant time
def get_first_element(arr: list) -> any:
    return arr[0] if arr else None
 
# O(1) - Dictionary lookup
def get_user_age(users: dict, user_id: str) -> int:
    return users.get(user_id)

O(log n) - Logarithmic Time

Định nghĩa: Thuật toán chia problem làm đôi mỗi iteration.

Ví dụ: Binary search trong sorted array

TypeScript:

// O(log n) - Binary search
function binarySearch(arr: number[], target: number): number {
    let left = 0;
    let right = arr.length - 1;
    
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        
        if (arr[mid] === target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    return -1;
}
 
// Với 1,000,000 items, chỉ cần ~20 comparisons!

Python:

# O(log n) - Binary search
def binary_search(arr: list[int], target: int) -> int:
    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 -1

O(n) - Linear Time

Định nghĩa: Thuật toán xử lý mỗi element một lần.

Ví dụ: Single loop qua array, linear search

TypeScript:

// O(n) - Linear search
function linearSearch<T>(arr: T[], target: T): number {
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] === target) {
            return i;
        }
    }
    return -1;
}
 
// O(n) - Sum all elements
function sum(arr: number[]): number {
    let total = 0;
    for (const num of arr) {
        total += num;
    }
    return total;
}

O(n log n) - Linearithmic Time

Định nghĩa: Common trong efficient sorting algorithms.

Ví dụ: Merge sort, Quick sort

TypeScript:

// O(n log n) - Merge sort
function mergeSort(arr: number[]): number[] {
    if (arr.length <= 1) {
        return arr;
    }
    
    const mid = Math.floor(arr.length / 2);
    const left = mergeSort(arr.slice(0, mid));
    const right = mergeSort(arr.slice(mid));
    
    return merge(left, right);
}
 
// Native sort cũng là O(n log n)
console.log([...arr].sort((a, b) => a - b));

O(n²) - Quadratic Time

Định nghĩa: Nested loops qua cùng dataset.

Ví dụ: Bubble sort, checking all pairs

TypeScript:

// O(n²) - Bubble sort
function bubbleSort(arr: number[]): number[] {
    const n = arr.length;
    
    for (let i = 0; i < n; i++) {           // Outer loop: n times
        for (let j = 0; j < n - i - 1; j++) {  // Inner loop: n times
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
            }
        }
    }
    
    return arr;
}

Python:

# O(n²) - Bubble sort
def bubble_sort(arr: list[int]) -> list[int]:
    n = len(arr)
    
    for i in range(n):
        for j in range(n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    
    return arr

O(2ⁿ) - Exponential Time

Định nghĩa: Thuật toán nhân đôi với mỗi additional input.

Ví dụ: Recursive Fibonacci (naive)

TypeScript:

// O(2ⁿ) - Naive recursive Fibonacci
function fibonacci(n: number): number {
    if (n <= 1) return n;
    
    return fibonacci(n - 1) + fibonacci(n - 2);
}
 
// Với n=30: ~2 triệu calls!

Growth Rate Comparison

Input Size (n)O(1)O(log n)O(n)O(n log n)O(n²)O(2ⁿ)
101310331001,024
1001710066410,0001.27×10³⁰
1,0001101,0009,9661,000,000∞

Key Takeaway: O(1) và O(log n) scale cực tốt. O(n²) và O(2ⁿ) trở nên unusable cho large inputs.


3. Cách Tính Big O

Quy Tắc 1: Drop Constants

Constants không ảnh hưởng đến growth rate.

// O(2n) → đơn giản hóa thành O(n)
function printTwice(arr: number[]): void {
    arr.forEach(x => console.log(x));  // O(n)
    arr.forEach(x => console.log(x));  // O(n)
    // Total: O(2n) → O(n)
}

Quy Tắc 2: Dominant Term Wins

Giữ lại term tăng nhanh nhất.

// O(n² + n) → đơn giản hóa thành O(n²)
function example(arr: number[]): void {
    // Loop đơn: O(n)
    for (let i = 0; i < arr.length; i++) {
        console.log(arr[i]);
    }
    
    // Nested loops: O(n²)
    for (let i = 0; i < arr.length; i++) {
        for (let j = 0; j < arr.length; j++) {
            console.log(arr[i], arr[j]);
        }
    }
    
    // Total: O(n²)
}

Quy Tắc 3: Different Inputs = Different Variables

// O(n + m) - KHÔNG PHẢI O(n)
function printBothArrays(arr1: number[], arr2: number[]): void {
    arr1.forEach(x => console.log(x));  // O(n)
    arr2.forEach(x => console.log(x));  // O(m)
    // Total: O(n + m)
}
 
// O(n × m) - KHÔNG PHẢI O(n²)
function printPairs(arr1: number[], arr2: number[]): void {
    for (const x of arr1) {       // O(n)
        for (const y of arr2) {   // O(m)
            console.log(x, y);
        }
    }
    // Total: O(n × m)
}

4. Space Complexity

Space complexity đo memory usage khi input tăng.

O(1) - Constant Space

// O(1) space - Chỉ vài variables
function sumArray(arr: number[]): number {
    let total = 0;  // O(1) space
    for (const num of arr) {
        total += num;
    }
    return total;
}

O(n) - Linear Space

// O(n) space - Tạo new array
function doubleArray(arr: number[]): number[] {
    const result: number[] = [];  // O(n) space
    for (const num of arr) {
        result.push(num * 2);
    }
    return result;
}

Recursion Stack Space

// O(n) space do call stack
function factorial(n: number): number {
    if (n <= 1) return 1;
    return n * factorial(n - 1);  // Mỗi call dùng stack space
}
 
// Maximum stack depth: n → O(n) space

5. Built-In Operations Complexity

TypeScript/JavaScript

OperationTime ComplexityGhi chú
arr[i]O(1)Array access
arr.push(x)O(1)Add to end
arr.pop()O(1)Remove from end
arr.unshift(x)O(n)Add to start
arr.shift()O(n)Remove from start
arr.sort()O(n log n)Sort array
map.get(key)O(1)Hash table lookup
set.has(x)O(1)Set membership

Python

OperationTime ComplexityGhi chú
arr[i]O(1)List access
arr.append(x)O(1)Add to end
arr.pop()O(1)Remove from end
arr.insert(0, x)O(n)Insert at start
sorted(arr)O(n log n)Sort list
dict[key]O(1)Dictionary lookup
x in setO(1)Set membership

6. Time vs Space Trade-offs

Thường bạn có thể trade space cho time (hoặc ngược lại).

Ví Dụ: Two Sum

// Brute force: O(n²) time, O(1) space
function twoSumBrute(arr: number[], target: number): [number, number] | null {
    for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[i] + arr[j] === target) {
                return [i, j];
            }
        }
    }
    return null;
}
 
// Optimized: O(n) time, O(n) space
function twoSumHash(arr: number[], target: number): [number, number] | null {
    const seen = new Map<number, number>();
    
    for (let i = 0; i < arr.length; i++) {
        const complement = target - arr[i];
        if (seen.has(complement)) {
            return [seen.get(complement)!, i];
        }
        seen.set(arr[i], i);
    }
    
    return null;
}

Trade-off: Chúng ta dùng extra space (hash table) để đạt faster time.


7. Interview Tips

Cách Communicate Complexity

Tốt:

"Solution này có O(n) time complexity vì chúng ta iterate qua array một lần. Space complexity là O(1) vì chỉ dùng vài variables."

Không tốt:

"Nó khá nhanh."

Red Flags trong Interviews

  • O(n²) hoặc tệ hơn khi O(n) hoặc O(n log n) có thể
  • O(2ⁿ) không có memoization
  • Không xem xét edge cases

Optimization Strategy

  1. Bắt đầu với brute force: Có working solution trước
  2. Analyze complexity: "Đây là O(n²), có thể tốt hơn không?"
  3. Identify bottlenecks: Nested loops? Repeated work?
  4. Apply patterns: Hash tables cho lookups, two pointers, binary search
  5. Xem xét trade-offs: O(n) space có chấp nhận được cho O(n log n) → O(n) time?

Tổng Kết và Key Takeaways

✅ Big O đo scalability, không phải absolute speed
✅ Common complexities: O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)
✅ Drop constants và giữ dominant term: O(2n + 5) → O(n)
✅ Analyze loops: Nested loops nhân complexity
✅ Space complexity quan trọng: Xem xét memory usage
✅ Recursion dùng stack space: O(n) space cho depth n
✅ Built-in operations có complexity: Biết operations của ngôn ngữ
✅ Trade-offs tồn tại: Time vs space, simplicity vs efficiency
✅ Focus vào worst-case cho interviews và production

Bước Tiếp Theo

Giờ bạn đã hiểu complexity analysis, bạn đã sẵn sàng cho:

Tiếp tục DSA series:

Quay lại roadmap:

Practice Resources:


Thuộc chuỗi bài Data Structures & Algorithms Roadmap

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.