Back to blog

Sorting & Binary Search: Tìm Hiểu Chi Tiết Các Thuật Toán Thiết Yếu

algorithmssortingbinary-searchinterview-prepdsa
Sorting & Binary Search: Tìm Hiểu Chi Tiết Các Thuật Toán Thiết Yếu

Introduction

Sorting và searching là hai phép toán cơ bản nhất trong computer science. Mọi database query, mọi kết quả search engine, mọi autocomplete suggestion đều dựa vào các thuật toán sorting và searching hiệu quả phía dưới.

Trong coding interview, bài toán sorting kiểm tra khả năng hiểu divide and conquer, comparison-based logic, và stability của bạn. Bài toán binary search kiểm tra khả năng loại bỏ một nửa search space một cách có hệ thống — một trong những kỹ thuật thuật toán mạnh nhất mà bạn sẽ từng học.

Bài viết này bao gồm Phần 1: Sorting Algorithms và Phần 2: Binary Search từ series DSA của chúng ta. Kết thúc bài, bạn sẽ hiểu khi nào dùng mỗi thuật toán, tại sao chúng hoạt động, và cách implement chúng từ đầu.

What You'll Learn

✅ Hiểu comparison-based vs non-comparison sorting
✅ Implement Bubble Sort, Selection Sort, Insertion Sort, Merge Sort, Quick Sort
✅ Phân tích time và space complexity cho từng thuật toán sorting
✅ Nắm vững standard binary search và các biến thể
✅ Áp dụng binary search cho rotated array, search space, và answer validation
✅ Giải 8 bài interview kinh điển step-by-step

Prerequisites


Phần 1: Sorting Algorithms

1. Tại Sao Sorting Quan Trọng

Sorting không chỉ đơn giản là sắp xếp các phần tử theo thứ tự. Nhiều thuật toán yêu cầu input đã sorted làm điều kiện tiên quyết:

Phụ thuộc vào SortingThuật toán
Binary searchYêu cầu sorted array
Two pointersThường cần sorted input
Merge intervalsSort theo start time trước
Closest pairSort rồi scan neighbors
Kruskal's MSTSort edges theo weight
Median findingSorted order cho O(1) median

"Nếu brute force của bạn là O(n²) và input chưa sorted, bản năng đầu tiên nên là: mình có thể sort nó trước không?"

Sort một array tốn O(n log n), gần như "miễn phí" so với O(n²) brute force. Nhiều bài O(n²) trở thành O(n log n) chỉ bằng cách sort trước.


2. Phân Loại Sorting Algorithm

Các thuộc tính chính để so sánh:

Thuộc tínhĐịnh nghĩa
StableCác phần tử bằng nhau giữ nguyên thứ tự tương đối ban đầu
In-placeDùng O(1) bộ nhớ phụ (sort ngay trong array)
AdaptiveChạy nhanh hơn trên input gần như đã sorted
OnlineCó thể sort phần tử khi chúng đến (không cần tất cả data trước)

3. Thuật Toán Sorting Cơ Bản — O(n²)

Đây là các thuật toán đơn giản nhưng chậm. Bạn nên biết chúng cho interview, nhưng hiếm khi dùng trong production.

3.1 Bubble Sort

Ý tưởng: Lặp đi lặp lại qua array, swap các phần tử kề nhau nếu chúng sai thứ tự. Phần tử lớn nhất chưa sorted sẽ "nổi lên" đúng vị trí sau mỗi pass.

// TypeScript - Bubble Sort
function bubbleSort(arr: number[]): number[] {
  const n = arr.length;
  for (let i = 0; i < n - 1; i++) {
    let swapped = false;
    for (let j = 0; j < n - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        swapped = true;
      }
    }
    // Tối ưu: nếu không swap, array đã sorted
    if (!swapped) break;
  }
  return arr;
}
 
console.log(bubbleSort([5, 3, 8, 1, 2])); // [1, 2, 3, 5, 8]
# Python - Bubble Sort
def bubble_sort(arr: list[int]) -> list[int]:
    n = len(arr)
    for i in range(n - 1):
        swapped = False
        for j in range(n - 1 - i):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
        # Tối ưu: nếu không swap, array đã sorted
        if not swapped:
            break
    return arr
 
print(bubble_sort([5, 3, 8, 1, 2]))  # [1, 2, 3, 5, 8]
Thuộc tínhGiá trị
Time (Best)O(n) — đã sorted, với optimization
Time (Average/Worst)O(n²)
SpaceO(1) in-place
Stable✅ Có
Adaptive✅ Có (với cờ swapped)

Khi nào dùng: Gần như không bao giờ trong thực tế. Tốt cho việc dạy và hiểu sorting cơ bản.

3.2 Selection Sort

Ý tưởng: Tìm phần tử nhỏ nhất trong phần chưa sorted và swap nó lên đầu. Lặp lại cho mỗi vị trí.

// TypeScript - Selection Sort
function selectionSort(arr: number[]): number[] {
  const n = arr.length;
  for (let i = 0; i < n - 1; i++) {
    let minIdx = i;
    for (let j = i + 1; j < n; j++) {
      if (arr[j] < arr[minIdx]) {
        minIdx = j;
      }
    }
    if (minIdx !== i) {
      [arr[i], arr[minIdx]] = [arr[minIdx], arr[i]];
    }
  }
  return arr;
}
# Python - Selection Sort
def selection_sort(arr: list[int]) -> list[int]:
    n = len(arr)
    for i in range(n - 1):
        min_idx = i
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
        if min_idx != i:
            arr[i], arr[min_idx] = arr[min_idx], arr[i]
    return arr
Thuộc tínhGiá trị
Time (Mọi trường hợp)O(n²) — luôn quét toàn bộ phần chưa sorted
SpaceO(1) in-place
Stable❌ Không (swap có thể thay đổi thứ tự tương đối)
Adaptive❌ Không

Khi nào dùng: Khi memory write tốn kém (giảm thiểu swap — tối đa n swap).

3.3 Insertion Sort

Ý tưởng: Xây dựng sorted array từng phần tử một. Lấy mỗi phần tử và chèn nó vào đúng vị trí trong phần đã sorted phía trước.

Hãy nghĩ như xếp bài — bạn lấy từng lá bài và trượt nó vào đúng chỗ trong tay.

// TypeScript - Insertion Sort
function insertionSort(arr: number[]): number[] {
  const n = arr.length;
  for (let i = 1; i < n; i++) {
    const key = arr[i];
    let j = i - 1;
    // Dịch các phần tử lớn hơn key sang phải
    while (j >= 0 && arr[j] > key) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = key;
  }
  return arr;
}
# Python - Insertion Sort
def insertion_sort(arr: list[int]) -> list[int]:
    n = len(arr)
    for i in range(1, n):
        key = arr[i]
        j = i - 1
        # Dịch các phần tử lớn hơn key sang phải
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
    return arr
Thuộc tínhGiá trị
Time (Best)O(n) — đã sorted
Time (Average/Worst)O(n²)
SpaceO(1) in-place
Stable✅ Có
Adaptive✅ Có
Online✅ Có

Khi nào dùng: Array nhỏ (< 20 phần tử), data gần sorted, hoặc làm base case cho hybrid sort (TimSort dùng insertion sort cho partition nhỏ).


4. Thuật Toán Sorting Hiệu Quả — O(n log n)

Đây là những "ngựa chiến" của sorting thực tế. Hãy hiểu sâu các thuật toán này.

4.1 Merge Sort

Ý tưởng: Chia array làm đôi, đệ quy sort mỗi nửa, rồi merge hai nửa đã sorted lại. Đây là thuật toán divide and conquer kinh điển.

// TypeScript - 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);
}
 
function merge(left: number[], right: number[]): number[] {
  const result: number[] = [];
  let i = 0, j = 0;
 
  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) {
      result.push(left[i]);
      i++;
    } else {
      result.push(right[j]);
      j++;
    }
  }
 
  // Thêm các phần tử còn lại
  while (i < left.length) result.push(left[i++]);
  while (j < right.length) result.push(right[j++]);
 
  return result;
}
 
console.log(mergeSort([38, 27, 43, 3, 9, 82, 10]));
// [3, 9, 10, 27, 38, 43, 82]
# Python - Merge Sort
def merge_sort(arr: list[int]) -> list[int]:
    if len(arr) <= 1:
        return arr
 
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
 
    return merge(left, right)
 
def merge(left: list[int], right: list[int]) -> list[int]:
    result = []
    i = j = 0
 
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
 
    # Thêm các phần tử còn lại
    result.extend(left[i:])
    result.extend(right[j:])
    return result
 
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))
# [3, 9, 10, 27, 38, 43, 82]

Bước merge hoạt động thế nào:

Left:  [3, 27, 38, 43]    Right: [9, 10, 82]
        ↑ i=0                      ↑ j=0
 
Step 1: 3 < 9   → lấy 3    Result: [3]
Step 2: 27 > 9  → lấy 9    Result: [3, 9]
Step 3: 27 > 10 → lấy 10   Result: [3, 9, 10]
Step 4: 27 < 82 → lấy 27   Result: [3, 9, 10, 27]
Step 5: 38 < 82 → lấy 38   Result: [3, 9, 10, 27, 38]
Step 6: 43 < 82 → lấy 43   Result: [3, 9, 10, 27, 38, 43]
Step 7: thêm 82             Result: [3, 9, 10, 27, 38, 43, 82]
Thuộc tínhGiá trị
Time (Mọi trường hợp)O(n log n) — luôn chia và merge
SpaceO(n) — cần array tạm để merge
Stable✅ Có (phần tử bằng nhau giữ thứ tự)
Adaptive❌ Không (luôn O(n log n))

Khi nào dùng: Khi cần stability, sorting linked list, external sorting (data quá lớn cho memory), hoặc khi cần guarantee O(n log n).

Tại sao O(n log n)?

  • log n level đệ quy (chia đôi array mỗi lần)
  • O(n) công việc ở mỗi level (merge tất cả phần tử)
  • Tổng: O(n) × O(log n) = O(n log n)

4.2 Quick Sort

Ý tưởng: Chọn một phần tử pivot, partition array sao cho tất cả phần tử nhỏ hơn pivot sang trái và lớn hơn sang phải, rồi đệ quy sort hai phần.

// TypeScript - Quick Sort (Lomuto partition)
function quickSort(arr: number[], low: number = 0, high: number = arr.length - 1): number[] {
  if (low < high) {
    const pivotIdx = partition(arr, low, high);
    quickSort(arr, low, pivotIdx - 1);
    quickSort(arr, pivotIdx + 1, high);
  }
  return arr;
}
 
function partition(arr: number[], low: number, high: number): number {
  const pivot = arr[high]; // Chọn phần tử cuối làm pivot
  let i = low - 1;        // Ranh giới phần tử nhỏ hơn
 
  for (let j = low; j < high; j++) {
    if (arr[j] <= pivot) {
      i++;
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
  }
 
  // Đặt pivot vào đúng vị trí
  [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
  return i + 1;
}
 
console.log(quickSort([8, 3, 1, 7, 0, 10, 2]));
// [0, 1, 2, 3, 7, 8, 10]
# Python - Quick Sort (Lomuto partition)
def quick_sort(arr: list[int], low: int = 0, high: int = None) -> list[int]:
    if high is None:
        high = len(arr) - 1
 
    if low < high:
        pivot_idx = partition(arr, low, high)
        quick_sort(arr, low, pivot_idx - 1)
        quick_sort(arr, pivot_idx + 1, high)
 
    return arr
 
def partition(arr: list[int], low: int, high: int) -> int:
    pivot = arr[high]  # Chọn phần tử cuối làm pivot
    i = low - 1        # Ranh giới phần tử nhỏ hơn
 
    for j in range(low, high):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
 
    # Đặt pivot vào đúng vị trí
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1
 
print(quick_sort([8, 3, 1, 7, 0, 10, 2]))
# [0, 1, 2, 3, 7, 8, 10]

Trace partition cho [8, 3, 1, 7, 0, 10, 2], pivot = 2:

Ban đầu: [8, 3, 1, 7, 0, 10, 2]  pivot=2, i=-1
 
j=0: 8 > 2  → bỏ qua
j=1: 3 > 2  → bỏ qua
j=2: 1 ≤ 2  → i=0, swap(0,2) → [1, 3, 8, 7, 0, 10, 2]
j=3: 7 > 2  → bỏ qua
j=4: 0 ≤ 2  → i=1, swap(1,4) → [1, 0, 8, 7, 3, 10, 2]
j=5: 10 > 2 → bỏ qua
 
Đặt pivot: swap(i+1=2, high=6) → [1, 0, 2, 7, 3, 10, 8]
                                          ↑ pivot tại index 2
Thuộc tínhGiá trị
Time (Best/Average)O(n log n)
Time (Worst)O(n²) — đã sorted + pivot tệ
SpaceO(log n) — recursion stack
Stable❌ Không
Adaptive❌ Không

Chiến lược chọn pivot:

Chiến lượcƯu điểmNhược điểm
Phần tử cuốiĐơn giảnO(n²) trên sorted input
Phần tử ngẫu nhiênTránh worst caseThêm random call
Median-of-threePerformance thực tế tốtPhức tạp hơn chút

Khi nào dùng: Lựa chọn mặc định cho in-memory sorting. Hầu hết standard library sort dùng Quick Sort variant (hoặc hybrid sort như IntroSort).

4.3 Merge Sort vs Quick Sort

Khía cạnhMerge SortQuick Sort
Worst caseO(n log n) ✅O(n²) ❌
Average caseO(n log n)O(n log n)
SpaceO(n) ❌O(log n) ✅
Stable✅ Có❌ Không
Cache performanceKém (không in-place)Xuất sắc (in-place)
Tốt nhất choLinked list, external sortArray, in-memory sort
Được dùng bởiPython (Timsort), Java (objects)C++ (IntroSort), Java (primitives)

Interview tip: Nếu được hỏi "thuật toán sort nào tốt nhất?", câu trả lời luôn là "tùy trường hợp." Hãy đề cập các trade-off ở trên.


5. Tổng Hợp So Sánh Sorting Algorithm

Thuật toánBestAverageWorstSpaceStableIn-Place
Bubble SortO(n)O(n²)O(n²)O(1)✅✅
Selection SortO(n²)O(n²)O(n²)O(1)❌✅
Insertion SortO(n)O(n²)O(n²)O(1)✅✅
Merge SortO(n log n)O(n log n)O(n log n)O(n)✅❌
Quick SortO(n log n)O(n log n)O(n²)O(log n)❌✅
Heap SortO(n log n)O(n log n)O(n log n)O(1)❌✅

Điểm chính cần nhớ:

  • O(n log n) là lower bound lý thuyết cho comparison-based sorting
  • Merge Sort = guarantee O(n log n), stable, nhưng tốn O(n) space
  • Quick Sort = nhanh nhất thực tế, in-place, nhưng worst case O(n²)
  • Insertion Sort = tốt nhất cho array nhỏ hoặc gần sorted
  • Sort thực tế là hybrid: TimSort (Python, Java) kết hợp Merge Sort + Insertion Sort

Built-in Sort: Các Ngôn Ngữ Dùng Gì

// TypeScript/JavaScript - TimSort (stable, O(n log n))
const arr = [3, 1, 4, 1, 5, 9];
arr.sort((a, b) => a - b);  // Luôn cung cấp comparator cho số!
// Không có comparator, JS sort theo string: [1, 1, 3, 4, 5, 9]
# Python - TimSort (stable, O(n log n))
arr = [3, 1, 4, 1, 5, 9]
arr.sort()              # Sort in-place
sorted_arr = sorted(arr)  # Trả về list mới đã sorted
 
# Custom sort
intervals = [(2, 3), (1, 5), (1, 2)]
intervals.sort(key=lambda x: (x[0], x[1]))
# [(1, 2), (1, 5), (2, 3)]

Interview tip: Trong interview thật, dùng built-in sort trừ khi được yêu cầu implement từ đầu. Biết cách chúng hoạt động, nhưng đừng phát minh lại bánh xe.


Phần 2: Binary Search

6. Binary Search Cơ Bản

Binary search là một trong những kỹ thuật mạnh nhất trong computer science. Ý tưởng cốt lõi rất đơn giản: nếu search space của bạn đã sorted (hoặc có monotonic property), bạn có thể loại bỏ một nửa với mỗi lần so sánh.

Linear Search:  [1, 3, 5, 7, 9, 11, 13, 15]  → Kiểm tra từng cái: O(n)
Binary Search:  [1, 3, 5, 7, 9, 11, 13, 15]  → Kiểm tra giữa, loại nửa: O(log n)

6.1 O(log n) Nhanh Cỡ Nào?

Kích thước Array (n)Linear Search O(n)Binary Search O(log n)
100100 bước7 bước
10,00010,000 bước14 bước
1,000,0001,000,000 bước20 bước
1,000,000,0001 tỷ bước30 bước

Binary search trên 1 tỷ phần tử chỉ mất tối đa 30 bước. Đó là sức mạnh của logarithmic time.

// TypeScript - Standard 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 - left) / 2); // Tránh overflow
 
    if (arr[mid] === target) {
      return mid;      // Tìm thấy
    } else if (arr[mid] < target) {
      left = mid + 1;  // Target ở nửa phải
    } else {
      right = mid - 1; // Target ở nửa trái
    }
  }
 
  return -1; // Không tìm thấy
}
 
console.log(binarySearch([1, 3, 5, 7, 9, 11], 7));  // 3
console.log(binarySearch([1, 3, 5, 7, 9, 11], 6));  // -1
# Python - Standard Binary Search
def binary_search(arr: list[int], target: int) -> int:
    left, right = 0, len(arr) - 1
 
    while left <= right:
        mid = left + (right - left) // 2  # Tránh overflow
 
        if arr[mid] == target:
            return mid       # Tìm thấy
        elif arr[mid] < target:
            left = mid + 1   # Target ở nửa phải
        else:
            right = mid - 1  # Target ở nửa trái
 
    return -1  # Không tìm thấy
 
print(binary_search([1, 3, 5, 7, 9, 11], 7))   # 3
print(binary_search([1, 3, 5, 7, 9, 11], 6))   # -1

Ví dụ trace — tìm 7 trong [1, 3, 5, 7, 9, 11]:

Step 1: left=0, right=5, mid=2 → arr[2]=5 < 7  → left=3
Step 2: left=3, right=5, mid=4 → arr[4]=9 > 7  → right=3
Step 3: left=3, right=3, mid=3 → arr[3]=7 = 7  → return 3 ✓

6.3 Các Bug Binary Search Thường Gặp

Binary search nổi tiếng là dễ viết sai. Đây là những lỗi phổ biến nhất:

BugCode saiCode đúng
Integer overflowmid = (left + right) / 2mid = left + (right - left) / 2
Vòng lặp vô hạnleft = mid hoặc right = midleft = mid + 1 hoặc right = mid - 1
Lệch 1while (left < right) vs while (left <= right)Tùy biến thể (xem bên dưới)
Sai biênSearch vượt quá array boundsLuôn kiểm tra left <= right

Standard binary search chỉ là khởi đầu. Sức mạnh thực sự của binary search đến từ các biến thể. Hãy nắm vững ba pattern cốt lõi.

7.1 Pattern 1: Tìm Giá Trị Chính Xác

Đây là standard binary search đã thấy ở trên. Dùng while (left <= right) với left = mid + 1 và right = mid - 1.

Template: Trả về index khi arr[mid] === target, hoặc -1 nếu không tìm thấy.

Đây là pattern binary search quan trọng nhất cho interview. Thay vì tìm bất kỳ occurrence nào, tìm vị trí đầu tiên hoặc cuối cùng thỏa mãn điều kiện.

Tìm Vị Trí Đầu Tiên ≥ Target (Lower Bound):

// TypeScript - Tìm vị trí đầu tiên arr[mid] >= target
function lowerBound(arr: number[], target: number): number {
  let left = 0;
  let right = arr.length; // Lưu ý: right = length (không phải length-1)
 
  while (left < right) {  // Lưu ý: < chặt (không phải <=)
    const mid = Math.floor(left + (right - left) / 2);
    if (arr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid;  // Lưu ý: right = mid (không phải mid-1)
    }
  }
 
  return left; // Vị trí đầu tiên arr[left] >= target
}
 
// Ví dụ: [1, 3, 3, 5, 7]
// lowerBound(arr, 3) → 1 (index đầu tiên có giá trị >= 3)
// lowerBound(arr, 4) → 3 (index đầu tiên có giá trị >= 4, tức là 5)
// lowerBound(arr, 8) → 5 (vượt quá cuối, target > tất cả phần tử)
# Python - Tìm vị trí đầu tiên arr[mid] >= target
def lower_bound(arr: list[int], target: int) -> int:
    left, right = 0, len(arr)  # right = len(arr), không phải len(arr)-1
 
    while left < right:  # < chặt (không phải <=)
        mid = left + (right - left) // 2
        if arr[mid] < target:
            left = mid + 1
        else:
            right = mid  # right = mid (không phải mid-1)
 
    return left  # Vị trí đầu tiên arr[left] >= target
 
# Python cũng có built-in:
import bisect
bisect.bisect_left([1, 3, 3, 5, 7], 3)   # 1
bisect.bisect_right([1, 3, 3, 5, 7], 3)  # 3

Tìm Vị Trí Cuối Cùng ≤ Target (Upper Bound):

// TypeScript - Tìm vị trí cuối cùng arr[mid] <= target
function upperBound(arr: number[], target: number): number {
  let left = 0;
  let right = arr.length;
 
  while (left < right) {
    const mid = Math.floor(left + (right - left) / 2);
    if (arr[mid] <= target) {
      left = mid + 1;
    } else {
      right = mid;
    }
  }
 
  return left - 1; // Vị trí cuối cùng arr[left-1] <= target
}
 
// Ví dụ: [1, 3, 3, 5, 7]
// upperBound(arr, 3) → 2 (index cuối có giá trị <= 3)
// upperBound(arr, 4) → 2 (index cuối có giá trị <= 4, tức là 3)

Khi nào dùng cái nào:

CầnHàmTrả về
Occurrence đầu tiên của targetlowerBound(arr, target) rồi kiểm tra arr[result] === target
Occurrence cuối cùng của targetupperBound(arr, target) rồi kiểm tra arr[result] === target
Đếm số lần xuất hiện targetupperBound(target) - lowerBound(target) + 1
Vị trí chènlowerBound(arr, target)

7.3 Pattern 3: Binary Search on Answer (Search Space Reduction)

Đây là pattern cao cấp và mạnh nhất. Thay vì search trong array, bạn binary search trên answer space.

Insight chính: Nếu bạn có thể định nghĩa hàm canAchieve(x) trả về true cho mọi giá trị ≥ ngưỡng nào đó và false bên dưới (monotonic), bạn có thể binary search tìm ngưỡng đó.

Answer space:  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
canAchieve:    [F, F, F, F, T, T, T, T, T, T]
                              ↑ Tìm boundary này
// TypeScript - Binary Search on Answer template
function binarySearchOnAnswer(low: number, high: number): number {
  while (low < high) {
    const mid = Math.floor(low + (high - low) / 2);
    if (canAchieve(mid)) {
      high = mid;     // mid có thể là answer, search bên trái
    } else {
      low = mid + 1;  // mid quá nhỏ, search bên phải
    }
  }
  return low;
}
# Python - Binary Search on Answer template
def binary_search_on_answer(low: int, high: int) -> int:
    while low < high:
        mid = low + (high - low) // 2
        if can_achieve(mid):
            high = mid      # mid có thể là answer, search bên trái
        else:
            low = mid + 1   # mid quá nhỏ, search bên phải
    return low

Chúng ta sẽ thấy pattern này trong các bài interview bên dưới.


8. Bài Interview: Sorting

Problem 1: Sort Colors (Dutch National Flag)

LeetCode 75: Cho một array với các phần tử 0, 1, 2, sort nó in-place trong một pass.

Insight chính: Kỹ thuật ba con trỏ — duy trì ranh giới cho 0s, 1s, và 2s.

// TypeScript - Sort Colors (One-pass, O(n) time, O(1) space)
function sortColors(nums: number[]): void {
  let low = 0;      // Ranh giới: mọi thứ trước low là 0
  let mid = 0;      // Phần tử hiện tại
  let high = nums.length - 1; // Ranh giới: mọi thứ sau high là 2
 
  while (mid <= high) {
    if (nums[mid] === 0) {
      [nums[low], nums[mid]] = [nums[mid], nums[low]];
      low++;
      mid++;
    } else if (nums[mid] === 1) {
      mid++;
    } else {
      // nums[mid] === 2
      [nums[mid], nums[high]] = [nums[high], nums[mid]];
      high--;
      // Không tăng mid — phần tử swap cần kiểm tra lại
    }
  }
}
 
const colors = [2, 0, 2, 1, 1, 0];
sortColors(colors);
console.log(colors); // [0, 0, 1, 1, 2, 2]
# Python - Sort Colors (One-pass, O(n) time, O(1) space)
def sort_colors(nums: list[int]) -> None:
    low, mid, high = 0, 0, len(nums) - 1
 
    while mid <= high:
        if nums[mid] == 0:
            nums[low], nums[mid] = nums[mid], nums[low]
            low += 1
            mid += 1
        elif nums[mid] == 1:
            mid += 1
        else:
            nums[mid], nums[high] = nums[high], nums[mid]
            high -= 1
            # Không tăng mid — phần tử swap cần kiểm tra lại
 
colors = [2, 0, 2, 1, 1, 0]
sort_colors(colors)
print(colors)  # [0, 0, 1, 1, 2, 2]

Trace:

[2, 0, 2, 1, 1, 0]  low=0, mid=0, high=5
 ↑mid         ↑high
nums[0]=2 → swap(0,5) → [0, 0, 2, 1, 1, 2]  high=4
 
[0, 0, 2, 1, 1, 2]  low=0, mid=0, high=4
 ↑mid      ↑high
nums[0]=0 → swap(0,0) → [0, 0, 2, 1, 1, 2]  low=1, mid=1
 
nums[1]=0 → swap(1,1) → [0, 0, 2, 1, 1, 2]  low=2, mid=2
 
nums[2]=2 → swap(2,4) → [0, 0, 1, 1, 2, 2]  high=3
 
nums[2]=1 → mid=3
 
nums[3]=1 → mid=4  → mid > high → XONG ✓
Kết quả: [0, 0, 1, 1, 2, 2]

Problem 2: Merge Intervals

LeetCode 56: Cho một tập hợp các intervals, merge tất cả intervals chồng nhau.

Insight chính: Sort theo start time, rồi merge các interval kề nhau mà overlap.

// TypeScript - Merge Intervals
function mergeIntervals(intervals: number[][]): number[][] {
  if (intervals.length <= 1) return intervals;
 
  // Sort theo start time
  intervals.sort((a, b) => a[0] - b[0]);
 
  const merged: number[][] = [intervals[0]];
 
  for (let i = 1; i < intervals.length; i++) {
    const current = intervals[i];
    const last = merged[merged.length - 1];
 
    if (current[0] <= last[1]) {
      // Chồng nhau — mở rộng điểm cuối
      last[1] = Math.max(last[1], current[1]);
    } else {
      // Không chồng — thêm interval mới
      merged.push(current);
    }
  }
 
  return merged;
}
 
console.log(mergeIntervals([[1,3],[2,6],[8,10],[15,18]]));
// [[1,6],[8,10],[15,18]]
# Python - Merge Intervals
def merge_intervals(intervals: list[list[int]]) -> list[list[int]]:
    if len(intervals) <= 1:
        return intervals
 
    # Sort theo start time
    intervals.sort(key=lambda x: x[0])
 
    merged = [intervals[0]]
 
    for current in intervals[1:]:
        last = merged[-1]
 
        if current[0] <= last[1]:
            # Chồng nhau — mở rộng điểm cuối
            last[1] = max(last[1], current[1])
        else:
            # Không chồng — thêm interval mới
            merged.append(current)
 
    return merged
 
print(merge_intervals([[1,3],[2,6],[8,10],[15,18]]))
# [[1,6],[8,10],[15,18]]

Time: O(n log n) cho sorting + O(n) cho merging = O(n log n)
Space: O(n) cho kết quả

Problem 3: Kth Largest Element

LeetCode 215: Tìm phần tử lớn thứ k trong array chưa sorted.

Insight chính: Dùng Quick Select — biến thể của Quick Sort chỉ đệ quy vào một partition.

// TypeScript - Kth Largest Element (Quick Select)
function findKthLargest(nums: number[], k: number): number {
  // Lớn thứ k = nhỏ thứ (n - k) (0-indexed)
  const targetIdx = nums.length - k;
  return quickSelect(nums, 0, nums.length - 1, targetIdx);
}
 
function quickSelect(arr: number[], low: number, high: number, target: number): number {
  if (low === high) return arr[low];
 
  // Random pivot để tránh worst case
  const randomIdx = low + Math.floor(Math.random() * (high - low + 1));
  [arr[randomIdx], arr[high]] = [arr[high], arr[randomIdx]];
 
  const pivotIdx = partition(arr, low, high);
 
  if (pivotIdx === target) {
    return arr[pivotIdx];
  } else if (pivotIdx < target) {
    return quickSelect(arr, pivotIdx + 1, high, target);
  } else {
    return quickSelect(arr, low, pivotIdx - 1, target);
  }
}
 
// Tái sử dụng hàm partition từ Quick Sort (Mục 4.2)
 
console.log(findKthLargest([3, 2, 1, 5, 6, 4], 2)); // 5
console.log(findKthLargest([3, 2, 3, 1, 2, 4, 5, 5, 6], 4)); // 4
# Python - Kth Largest Element (Quick Select)
import random
 
def find_kth_largest(nums: list[int], k: int) -> int:
    target_idx = len(nums) - k  # Lớn thứ k = nhỏ thứ (n-k)
    return quick_select(nums, 0, len(nums) - 1, target_idx)
 
def quick_select(arr: list[int], low: int, high: int, target: int) -> int:
    if low == high:
        return arr[low]
 
    # Random pivot để tránh worst case
    random_idx = random.randint(low, high)
    arr[random_idx], arr[high] = arr[high], arr[random_idx]
 
    pivot_idx = qs_partition(arr, low, high)
 
    if pivot_idx == target:
        return arr[pivot_idx]
    elif pivot_idx < target:
        return quick_select(arr, pivot_idx + 1, high, target)
    else:
        return quick_select(arr, low, pivot_idx - 1, target)
 
def qs_partition(arr: list[int], low: int, high: int) -> int:
    pivot = arr[high]
    i = low - 1
    for j in range(low, high):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1
 
print(find_kth_largest([3, 2, 1, 5, 6, 4], 2))  # 5

Time: O(n) trung bình, O(n²) worst case
Space: O(1) — in-place partitioning

Tại sao O(n)? Trung bình, mỗi lần đệ quy làm việc trên nửa số phần tử: n + n/2 + n/4 + ... = 2n = O(n).


Problem 4: Search in Rotated Sorted Array

LeetCode 33: Tìm target trong array đã sorted rồi bị rotated tại một pivot.

Insight chính: Ít nhất một nửa của array luôn sorted. Xác định nửa nào, rồi kiểm tra xem target có nằm trong nửa đó không.

Sorted ban đầu: [0, 1, 2, 4, 5, 6, 7]
Rotated tại idx 4: [4, 5, 6, 7, 0, 1, 2]
                     ↑ sorted ↑  ↑ sorted ↑
// TypeScript - Search in Rotated Sorted Array
function searchRotated(nums: number[], target: number): number {
  let left = 0;
  let right = nums.length - 1;
 
  while (left <= right) {
    const mid = Math.floor(left + (right - left) / 2);
 
    if (nums[mid] === target) return mid;
 
    // Xác định nửa nào sorted
    if (nums[left] <= nums[mid]) {
      // Nửa trái sorted
      if (nums[left] <= target && target < nums[mid]) {
        right = mid - 1; // Target trong nửa trái sorted
      } else {
        left = mid + 1;  // Target trong nửa phải
      }
    } else {
      // Nửa phải sorted
      if (nums[mid] < target && target <= nums[right]) {
        left = mid + 1;  // Target trong nửa phải sorted
      } else {
        right = mid - 1; // Target trong nửa trái
      }
    }
  }
 
  return -1;
}
 
console.log(searchRotated([4, 5, 6, 7, 0, 1, 2], 0)); // 4
console.log(searchRotated([4, 5, 6, 7, 0, 1, 2], 3)); // -1
# Python - Search in Rotated Sorted Array
def search_rotated(nums: list[int], target: int) -> int:
    left, right = 0, len(nums) - 1
 
    while left <= right:
        mid = left + (right - left) // 2
 
        if nums[mid] == target:
            return mid
 
        # Xác định nửa nào sorted
        if nums[left] <= nums[mid]:
            # Nửa trái sorted
            if nums[left] <= target < nums[mid]:
                right = mid - 1  # Target trong nửa trái sorted
            else:
                left = mid + 1   # Target trong nửa phải
        else:
            # Nửa phải sorted
            if nums[mid] < target <= nums[right]:
                left = mid + 1   # Target trong nửa phải sorted
            else:
                right = mid - 1  # Target trong nửa trái
 
    return -1
 
print(search_rotated([4, 5, 6, 7, 0, 1, 2], 0))  # 4

Trace tìm target=0 trong [4, 5, 6, 7, 0, 1, 2]:

Step 1: left=0, right=6, mid=3 → nums[3]=7 ≠ 0
        nums[0]=4 <= nums[3]=7 → nửa trái sorted [4,5,6,7]
        0 không trong [4, 7) → left = 4
 
Step 2: left=4, right=6, mid=5 → nums[5]=1 ≠ 0
        nums[4]=0 <= nums[5]=1 → nửa trái sorted [0,1]
        0 trong [0, 1) → right = 4
 
Step 3: left=4, right=4, mid=4 → nums[4]=0 = 0 → return 4 ✓

Problem 5: Find First and Last Position

LeetCode 34: Tìm vị trí bắt đầu và kết thúc của giá trị target trong sorted array.

Insight chính: Dùng hai binary search — một tìm occurrence đầu, một tìm occurrence cuối.

// TypeScript - Find First and Last Position
function searchRange(nums: number[], target: number): number[] {
  return [findFirst(nums, target), findLast(nums, target)];
}
 
function findFirst(nums: number[], target: number): number {
  let left = 0, right = nums.length - 1;
  let result = -1;
 
  while (left <= right) {
    const mid = Math.floor(left + (right - left) / 2);
    if (nums[mid] === target) {
      result = mid;
      right = mid - 1;  // Tiếp tục tìm bên trái cho occurrence sớm hơn
    } else if (nums[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
 
  return result;
}
 
function findLast(nums: number[], target: number): number {
  let left = 0, right = nums.length - 1;
  let result = -1;
 
  while (left <= right) {
    const mid = Math.floor(left + (right - left) / 2);
    if (nums[mid] === target) {
      result = mid;
      left = mid + 1;   // Tiếp tục tìm bên phải cho occurrence muộn hơn
    } else if (nums[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
 
  return result;
}
 
console.log(searchRange([5,7,7,8,8,10], 8)); // [3, 4]
console.log(searchRange([5,7,7,8,8,10], 6)); // [-1, -1]
# Python - Find First and Last Position
def search_range(nums: list[int], target: int) -> list[int]:
    return [find_first(nums, target), find_last(nums, target)]
 
def find_first(nums: list[int], target: int) -> int:
    left, right = 0, len(nums) - 1
    result = -1
 
    while left <= right:
        mid = left + (right - left) // 2
        if nums[mid] == target:
            result = mid
            right = mid - 1  # Tiếp tục tìm bên trái
        elif nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
 
    return result
 
def find_last(nums: list[int], target: int) -> int:
    left, right = 0, len(nums) - 1
    result = -1
 
    while left <= right:
        mid = left + (right - left) // 2
        if nums[mid] == target:
            result = mid
            left = mid + 1  # Tiếp tục tìm bên phải
        elif nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
 
    return result
 
print(search_range([5,7,7,8,8,10], 8))  # [3, 4]

Time: O(log n) — hai binary search
Space: O(1)

Problem 6: Find Peak Element

LeetCode 162: Tìm peak element (phần tử lớn hơn neighbors) trong O(log n).

Insight chính: Nếu arr[mid] < arr[mid+1], peak chắc chắn tồn tại bên phải. Nếu arr[mid] > arr[mid+1], peak tồn tại bên trái (hoặc tại mid).

// TypeScript - Find Peak Element
function findPeakElement(nums: number[]): number {
  let left = 0;
  let right = nums.length - 1;
 
  while (left < right) {
    const mid = Math.floor(left + (right - left) / 2);
 
    if (nums[mid] < nums[mid + 1]) {
      left = mid + 1;  // Peak ở bên phải
    } else {
      right = mid;     // Peak ở mid hoặc bên trái
    }
  }
 
  return left; // left === right, trỏ đến peak
}
 
console.log(findPeakElement([1, 2, 3, 1]));    // 2 (giá trị 3)
console.log(findPeakElement([1, 2, 1, 3, 5, 6, 4])); // 5 (giá trị 6)
# Python - Find Peak Element
def find_peak_element(nums: list[int]) -> int:
    left, right = 0, len(nums) - 1
 
    while left < right:
        mid = left + (right - left) // 2
 
        if nums[mid] < nums[mid + 1]:
            left = mid + 1   # Peak ở bên phải
        else:
            right = mid      # Peak ở mid hoặc bên trái
 
    return left  # left == right, trỏ đến peak
 
print(find_peak_element([1, 2, 3, 1]))    # 2
print(find_peak_element([1, 2, 1, 3, 5, 6, 4]))  # 5

Time: O(log n)
Space: O(1)

Tại sao hoạt động: Biên array là -∞, nên nếu ta đang đi "lên dốc" (tăng dần), ta chắc chắn sẽ gặp peak trước khi rơi xuống -∞ ở biên.

Problem 7: Minimum in Rotated Sorted Array

LeetCode 153: Tìm phần tử nhỏ nhất trong rotated sorted array (không có duplicate).

Insight chính: So sánh mid với right. Nếu arr[mid] > arr[right], minimum ở nửa phải. Ngược lại, nó ở nửa trái (bao gồm mid).

// TypeScript - Find Minimum in Rotated Sorted Array
function findMin(nums: number[]): number {
  let left = 0;
  let right = nums.length - 1;
 
  while (left < right) {
    const mid = Math.floor(left + (right - left) / 2);
 
    if (nums[mid] > nums[right]) {
      left = mid + 1;  // Minimum ở nửa phải
    } else {
      right = mid;     // Minimum ở mid hoặc nửa trái
    }
  }
 
  return nums[left];
}
 
console.log(findMin([3, 4, 5, 1, 2]));    // 1
console.log(findMin([4, 5, 6, 7, 0, 1, 2])); // 0
console.log(findMin([11, 13, 15, 17]));    // 11 (không rotated)
# Python - Find Minimum in Rotated Sorted Array
def find_min(nums: list[int]) -> int:
    left, right = 0, len(nums) - 1
 
    while left < right:
        mid = left + (right - left) // 2
 
        if nums[mid] > nums[right]:
            left = mid + 1   # Minimum ở nửa phải
        else:
            right = mid      # Minimum ở mid hoặc nửa trái
 
    return nums[left]
 
print(find_min([3, 4, 5, 1, 2]))      # 1
print(find_min([4, 5, 6, 7, 0, 1, 2]))  # 0

Problem 8: Koko Eating Bananas (Binary Search on Answer)

LeetCode 875: Koko có thể ăn với tốc độ k chuối/giờ. Cho các đống chuối và h giờ, tìm k tối thiểu.

Insight chính: Binary search trên answer k. Với mỗi candidate speed, kiểm tra xem Koko có thể ăn hết tất cả đống trong h giờ không.

// TypeScript - Koko Eating Bananas
function minEatingSpeed(piles: number[], h: number): number {
  let left = 1;
  let right = Math.max(...piles);
 
  while (left < right) {
    const mid = Math.floor(left + (right - left) / 2);
 
    if (canFinish(piles, mid, h)) {
      right = mid;     // Thử tốc độ chậm hơn
    } else {
      left = mid + 1;  // Cần tốc độ nhanh hơn
    }
  }
 
  return left;
}
 
function canFinish(piles: number[], speed: number, hours: number): boolean {
  let totalHours = 0;
  for (const pile of piles) {
    totalHours += Math.ceil(pile / speed);
  }
  return totalHours <= hours;
}
 
console.log(minEatingSpeed([3, 6, 7, 11], 8));   // 4
console.log(minEatingSpeed([30, 11, 23, 4, 20], 5)); // 30
console.log(minEatingSpeed([30, 11, 23, 4, 20], 6)); // 23
# Python - Koko Eating Bananas
import math
 
def min_eating_speed(piles: list[int], h: int) -> int:
    left, right = 1, max(piles)
 
    while left < right:
        mid = left + (right - left) // 2
 
        if can_finish(piles, mid, h):
            right = mid      # Thử tốc độ chậm hơn
        else:
            left = mid + 1   # Cần tốc độ nhanh hơn
 
    return left
 
def can_finish(piles: list[int], speed: int, hours: int) -> bool:
    total_hours = sum(math.ceil(pile / speed) for pile in piles)
    return total_hours <= hours
 
print(min_eating_speed([3, 6, 7, 11], 8))    # 4
print(min_eating_speed([30, 11, 23, 4, 20], 5))  # 30

Trace cho piles=[3,6,7,11], h=8:

Search space: [1, 11]
 
mid=6: ceil(3/6)+ceil(6/6)+ceil(7/6)+ceil(11/6) = 1+1+2+2 = 6 ≤ 8 ✓ → right=6
mid=3: ceil(3/3)+ceil(6/3)+ceil(7/3)+ceil(11/3) = 1+2+3+4 = 10 > 8 ✗ → left=4
mid=5: ceil(3/5)+ceil(6/5)+ceil(7/5)+ceil(11/5) = 1+2+2+3 = 8 ≤ 8 ✓ → right=5
mid=4: ceil(3/4)+ceil(6/4)+ceil(7/4)+ceil(11/4) = 1+2+2+3 = 8 ≤ 8 ✓ → right=4
 
left=4, right=4 → return 4

Time: O(n × log(max_pile)) — binary search × feasibility check
Space: O(1)


10. Binary Search Decision Framework

Dùng decision tree này để xác định pattern binary search nào cần áp dụng:

Từ khóa nhận biết trong interview:

SignalPattern
"tìm target trong sorted array"Pattern 1: Standard
"first occurrence", "leftmost", "minimum thỏa mãn"Pattern 2: Lower Bound
"last occurrence", "rightmost", "maximum thỏa mãn"Pattern 2: Upper Bound
"minimum speed/capacity/time để đạt X"Pattern 3: Search on Answer
"rotated sorted array"Modified Pattern 1
"peak element"Biến thể Pattern 2

11. Bài Tập Luyện Tập

Bài Sorting

Easy:

#BàiConcept chính
1Sort ColorsDutch National Flag
2Merge Sorted ArrayTwo pointers từ cuối
3Valid AnagramCounting sort variant
4Contains DuplicateSort + kiểm tra kề

Medium:

#BàiConcept chính
5Merge IntervalsSort + merge
6Kth Largest ElementQuick Select
7Top K Frequent ElementsBucket sort
8Sort ListMerge sort trên linked list
9Largest NumberCustom comparator

Hard:

#BàiConcept chính
10Count of Smaller NumbersMerge sort + counting

Easy:

#BàiConcept chính
1Binary SearchStandard binary search
2First Bad VersionLower bound
3Search Insert PositionLower bound
4Sqrt(x)Binary search on answer

Medium:

#BàiConcept chính
5Search in Rotated ArrayModified binary search
6Find First and Last PositionBoundary search
7Find Peak ElementBinary search trên peak
8Find Minimum in Rotated ArrayBinary search trên rotation
9Koko Eating BananasSearch on answer
10Capacity to Ship PackagesSearch on answer
11Search a 2D MatrixFlatten + binary search

Hard:

#BàiConcept chính
12Median of Two Sorted ArraysBinary search trên partition
13Split Array Largest SumSearch on answer
14Find in Rotated Array II (có duplicate)Xử lý duplicate

Cách tiếp cận khuyên dùng: Giải hết bài Easy trước, rồi Medium. Chỉ thử Hard khi bạn đã thoải mái với các pattern.


Summary and Key Takeaways

Sorting Algorithms:
✅ O(n log n) là lower bound cho comparison sort — Merge Sort và Quick Sort đạt được điều này
✅ Merge Sort: stable, guarantee O(n log n), nhưng tốn O(n) space
✅ Quick Sort: nhanh nhất thực tế, in-place O(log n) space, nhưng worst case O(n²)
✅ Insertion Sort: tốt nhất cho array nhỏ và data gần sorted

Binary Search Cơ Bản:
✅ Loại bỏ một nửa search space mỗi bước — O(log n) time
✅ Yêu cầu sorted hoặc monotonic input
✅ Luôn dùng mid = left + (right - left) / 2 để tránh overflow

Binary Search Patterns:
✅ Pattern 1 (Exact match): while (left <= right), return mid khi tìm thấy
✅ Pattern 2 (Boundary): while (left < right), tìm first/last thỏa điều kiện
✅ Pattern 3 (Search on answer): Binary search trên answer space với feasibility check

Interview Tips:
✅ "Sort trước" thường giảm bài O(n²) xuống O(n log n)
✅ Binary search on answer là pattern bị đánh giá thấp nhất — tìm "minimum X để đạt Y"
✅ Biết khi nào dùng left <= right vs left < right — tùy pattern
✅ Quick Select giải bài "kth element" trong O(n) trung bình


What's Next?

Bạn đã nắm vững hai công cụ thuật toán cơ bản nhất: sorting và binary search. Các kỹ thuật này xuất hiện khắp nơi — trong bài array, bài tree, graph algorithm, và system design.

Tiếp tục series DSA:

Next: Dynamic Programming & Greedy Algorithms — Optimal substructure, memoization, và greedy choice
Previous: Heaps & Priority Queues — Các phép toán min/max hiệu quả
Series Overview: 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.