Buổi 01 — Complexity Analysis & Array

Navigation

Roadmap-FAANG | → 02-Linked-List-Dummy-Node Giai đoạn: Big Tech Foundation | Độ khó: ⭐⭐


1. Analogy & Context

Ví dụ thực tế

Tìm tên trong danh bạ điện thoại.

  • — Linear Search: Lật từng trang một từ đầu đến cuối. 1000 trang = 1000 bước.
  • — Binary Search: Mở giữa sách. Tên cần tìm ở phần trước hay sau? Loại đi một nửa. 1000 trang = chỉ 10 bước.
  • — Hash lookup: Có index ở cuối sách “N = trang 743”. Trực tiếp đến trang đó.

Insight: Cùng một bài toán, thuật toán khác nhau tạo ra sự khác biệt giữa “instant” và “server timeout”.

Array là gì dưới góc nhìn hardware? Array là vùng nhớ liên tiếp (contiguous memory). Phần tử thứ nằm ở địa chỉ base_address + i * element_size. Đây chính là lý do random access — CPU tính thẳng địa chỉ, không cần traverse.


2. The FAANG Standard

Tại sao Senior Engineer phải master Complexity?

Ở FAANG, mọi code review đều có câu hỏi: “What’s the time and space complexity?” Không trả lời được → fail code review, dù code chạy đúng.

Array là foundation của mọi data structure. Heap, Hash Table, Segment Tree đều build trên array. Nếu không hiểu sâu array, mọi DS nâng cao đều sẽ bị “học vẹt”.

Điểm phân biệt Senior vs Junior:

  • Junior: “It works” ✓
  • Senior: “It works, time , space , và đây là tại sao không thể tốt hơn” ✓✓✓

3. Visual Thinking

3.1 Big O Hierarchy

graph LR
    A["O(1)<br/>Constant"] -->|faster| B["O(log N)<br/>Logarithmic"]
    B --> C["O(N)<br/>Linear"]
    C --> D["O(N log N)<br/>Linearithmic"]
    D --> E["O(N²)<br/>Quadratic"]
    E --> F["O(2ᴺ)<br/>Exponential"]
    F --> G["O(N!)<br/>Factorial"]

    style A fill:#22c55e,color:#fff
    style B fill:#86efac,color:#000
    style C fill:#fde047,color:#000
    style D fill:#fb923c,color:#fff
    style E fill:#f87171,color:#fff
    style F fill:#dc2626,color:#fff
    style G fill:#7f1d1d,color:#fff

3.2 Array Memory Layout

Index:   [0]   [1]   [2]   [3]   [4]
Value:   [10]  [20]  [30]  [40]  [50]
Addr:   0x100 0x104 0x108 0x10C 0x110
         ↑                         ↑
      base_addr              base + 4*4

3.3 Two-Pass Array Pattern

flowchart LR
    subgraph Pass1["Pass 1: Build prefix/state"]
        A[arr[0]] --> B[arr[1]] --> C[arr[2]] --> D[arr[n-1]]
    end
    subgraph Pass2["Pass 2: Answer queries"]
        E[arr[n-1]] --> F[arr[n-2]] --> G["..."] --> H[arr[0]]
    end
    Pass1 --> Pass2

4. TypeScript Template

4.1 Basic Array Operations

/**
 * Demonstrates O(1) vs O(N) array operations.
 * Understanding these fundamentals prevents common interview mistakes.
 */
 
// ✅ O(1) — Random Access
function getElement<T>(arr: T[], index: number): T | undefined {
  return arr[index]; // Direct memory address calculation
}
 
// ✅ O(N) — Linear Search (unsorted array)
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) — Prefix Sum (classic array pattern)
/**
 * Build prefix sum array để answer range sum queries in O(1).
 * @example prefixSum([1,2,3,4]) → [0,1,3,6,10]
 * Query sum(l, r) = prefix[r+1] - prefix[l]
 */
function buildPrefixSum(nums: number[]): number[] {
  const prefix = new Array(nums.length + 1).fill(0);
  for (let i = 0; i < nums.length; i++) {
    prefix[i + 1] = prefix[i] + nums[i];
  }
  return prefix;
}
 
function rangeSum(prefix: number[], left: number, right: number): number {
  return prefix[right + 1] - prefix[left];
}

4.2 Kadane’s Algorithm — Maximum Subarray

/**
 * Maximum Subarray Sum — LeetCode #53 (Classic FAANG problem)
 *
 * Key insight: Tại mỗi vị trí i, ta có 2 lựa chọn:
 *   1. Extend subarray hiện tại: currentSum + nums[i]
 *   2. Start fresh từ nums[i]
 * → Chọn cái nào lớn hơn.
 *
 * Time: O(N) | Space: O(1)
 */
function maxSubArray(nums: number[]): number {
  let maxSum = nums[0];
  let currentSum = nums[0];
 
  for (let i = 1; i < nums.length; i++) {
    // Extend or start fresh — core of Kadane's
    currentSum = Math.max(nums[i], currentSum + nums[i]);
    maxSum = Math.max(maxSum, currentSum);
  }
 
  return maxSum;
}

4.3 Product Except Self — No Division Trick

/**
 * Product of Array Except Self — LeetCode #238
 *
 * Approach: Two-pass prefix & suffix product.
 * Pass 1 (left→right): result[i] = product of all elements to the LEFT of i
 * Pass 2 (right→left): multiply result[i] by product of all elements to the RIGHT
 *
 * Time: O(N) | Space: O(1) (output array không tính)
 */
function productExceptSelf(nums: number[]): number[] {
  const n = nums.length;
  const result = new Array(n).fill(1);
 
  // Pass 1: prefix products
  let leftProduct = 1;
  for (let i = 0; i < n; i++) {
    result[i] = leftProduct;
    leftProduct *= nums[i];
  }
 
  // Pass 2: multiply by suffix products
  let rightProduct = 1;
  for (let i = n - 1; i >= 0; i--) {
    result[i] *= rightProduct;
    rightProduct *= nums[i];
  }
 
  return result;
}

5. Complexity Deep-Dive

5.1 Time Complexity Comparison

OperationArrayLinked ListHash Table
Access by indexN/A
Search (unsorted) avg
Search (sorted)N/A
Insert at end amortized avg
Insert at beginningN/A
Delete to find avg

5.2 Phân tích Prefix Sum

Brute force range sum:
  For each query (l, r): iterate l → r → O(N) per query
  Q queries: O(Q * N) total

Với Prefix Sum:
  Build: O(N) một lần
  Each query: O(1)
  Q queries: O(N + Q) total → khi Q >> 1, lợi thế là rõ rệt

Khi nào dùng Prefix Sum?

  • Nhiều range sum/average queries trên static array
  • “Sum of subarray” hoặc “range query” trong đề
  • Có thể extend sang 2D prefix sum cho matrix problems

5.3 Amortized Analysis — Dynamic Array

Tại sao Array.push() amortized?

Dynamic array double size khi full. Với push operations:

  • operations: each
  • Resize events: 1 + 2 + 4 + … + = total copy work
  • Amortized per operation:

6. Edge Cases & Pitfalls

Các bẫy thường gặp — Senior vẫn mắc

1. Off-by-one error (phổ biến nhất)

// ❌ Sai — miss phần tử cuối
for (let i = 0; i < arr.length - 1; i++) { ... }
 
// ✅ Đúng
for (let i = 0; i < arr.length; i++) { ... }

2. Empty array — không check

// ❌ Crash khi arr = []
function maxElement(arr: number[]): number {
  let max = arr[0]; // undefined nếu arr rỗng!
  ...
}
 
// ✅ Guard clause
function maxElement(arr: number[]): number {
  if (arr.length === 0) throw new Error("Empty array");
  let max = arr[0];
  ...
}

3. Integer overflow trong prefix sum

// ❌ Có thể overflow với large inputs (TypeScript dùng float64 nên hiếm hơn)
// Nhưng trong Java/C++: int có thể overflow khi N = 10^5, value = 10^4
// → Dùng long/BigInt
 
// ✅ TypeScript: number là float64, safe up to 2^53 ≈ 9 * 10^15
// Với FAANG problems, thường đủ — nhưng luôn comment lý do

4. Mutating input array mà không nói

// ❌ Silently modifies input — interviewer sẽ hỏi
function process(arr: number[]): void {
  arr.sort(); // MUTATES INPUT!
}
 
// ✅ Explicit về mutation, hoặc tạo copy
function process(arr: readonly number[]): number[] {
  return [...arr].sort((a, b) => a - b); // Clone trước
}

5. Confusing i với arr[i] khi đặt tên

// ❌ Confusing
for (let i = 0; i < arr.length; i++) {
  if (arr[i] > arr[i+1]) { ... } // i là index hay value?
}
 
// ✅ Clear naming
for (let idx = 0; idx < arr.length; idx++) {
  const current = arr[idx];
  const next = arr[idx + 1];
  if (current > next) { ... }
}

7. Pattern Recognition

Signal Words → Array Patterns

Signal trong đềPatternTemplate
”subarray sum equals K”Prefix Sum + HashMapprefixSum, count[sum]++
”maximum subarray”Kadane’s AlgorithmcurrentSum = max(nums[i], currentSum + nums[i])
”rotate array k steps”Reverse trick hoặc Extra array3 reverses in-place
”find duplicate in [1,N]“Index as Hash / Floyd’s cyclearr[arr[i]] = visited marker
”merge sorted arrays”Two Pointersi, j trên 2 arrays
”product except self”Prefix + Suffix passTwo-pass, O(1) space

8. Top LeetCode Problems

Curated List — Bắt buộc giải trước khi qua buổi tiếp theo

#ProblemDifficultyPatternGhi chú
53Maximum SubarrayMediumKadane’sBài kinh điển, phải giải được trong 5 phút
238Product of Array Except SelfMediumPrefix + SuffixKhông dùng division — test Two-pass thinking
560Subarray Sum Equals KMediumPrefix Sum + HashMapKết hợp 2 kỹ thuật
152Maximum Product SubarrayMediumKadane’s variantTrack cả min và max (negative số)
41First Missing PositiveHardArray as HashDùng index làm marker — , space

Giải trình bài #41 (Hard) — Why it’s important

/**
 * First Missing Positive — LeetCode #41
 *
 * Key insight: Missing positive trong array độ dài N phải nằm trong [1, N+1].
 * Dùng chính array làm hash table: đặt nums[i] ở đúng vị trí index nums[i]-1.
 *
 * Time: O(N) | Space: O(1) — đây là điểm phân biệt Senior
 */
function firstMissingPositive(nums: number[]): number {
  const n = nums.length;
 
  // Phase 1: Place each number at its correct index
  // nums[i] should be at index nums[i] - 1
  for (let i = 0; i < n; i++) {
    while (
      nums[i] > 0 &&
      nums[i] <= n &&
      nums[nums[i] - 1] !== nums[i]  // Avoid infinite loop on duplicates
    ) {
      // Swap nums[i] to its correct position
      const correctIdx = nums[i] - 1;
      [nums[i], nums[correctIdx]] = [nums[correctIdx], nums[i]];
    }
  }
 
  // Phase 2: Find first position where nums[i] !== i + 1
  for (let i = 0; i < n; i++) {
    if (nums[i] !== i + 1) return i + 1;
  }
 
  return n + 1; // All 1..N are present
}

9. Self-Assessment Checklist

Trước khi chuyển sang buổi tiếp theo, hãy confirm bản thân có thể:

  • Giải thích được sự khác biệt giữa bằng ngôn ngữ người thường
  • Phân tích Amortized complexity của dynamic array
  • Code Prefix Sum từ scratch trong 3 phút
  • Implement Kadane’s algorithm không cần nhìn
  • Giải LeetCode #238 với space
  • Identify được 5 edge cases trước khi code bất kỳ bài array nào

→ Tiếp theo: 02-Linked-List-Dummy-Node — Dummy Node pattern & why it’s elegant