Nhập môn

Tìm kiếm nhị phân (binary search): giải thích dễ hiểu, code C++ và Python

Tìm kiếm nhị phân là cách tìm một giá trị trong mảng đã sắp xếp: mỗi lần so với phần tử ở giữa, rồi bỏ đi một nửa chắc chắn không chứa giá trị cần tìm. Với mảng 1 triệu phần tử, cách này chỉ cần tối đa 20 lần so sánh, trong khi tìm lần lượt từ đầu có thể phải xét cả 1 triệu phần tử.

Ý chính
  • Chỉ dùng được khi mảng đã sắp xếp, hoặc khi điều kiện cần tìm có tính đơn điệu.
  • Mỗi bước loại đi một nửa, nên độ phức tạp là O(log n).
  • Trong C++ và Java, tính mid = lo + (hi - lo) / 2 để không bị tràn số.

Tìm kiếm nhị phân là gì?

Tìm kiếm nhị phân (binary search) là thuật toán tìm vị trí của một giá trị x trong mảng đã sắp xếp. Thay vì xét lần lượt từng phần tử như tìm kiếm tuần tự (linear search), ta nhìn vào phần tử ở giữa đoạn đang xét:

  • Nếu phần tử giữa bằng x: đã tìm thấy.
  • Nếu phần tử giữa nhỏ hơn x: x chỉ có thể nằm ở nửa bên phải, bỏ nửa bên trái.
  • Nếu phần tử giữa lớn hơn x: x chỉ có thể nằm ở nửa bên trái, bỏ nửa bên phải.

Lặp lại cho tới khi tìm thấy x, hoặc đoạn đang xét không còn phần tử nào, tức là x không có trong mảng.

Ý tưởng qua trò đoán số

Bạn nghĩ một số từ 1 đến 100. Mình đoán, còn bạn chỉ trả lời “lớn hơn”, “nhỏ hơn” hoặc “đúng”. Cách đoán tốt nhất là luôn đoán số ở giữa: 50, rồi 25 hoặc 75, rồi cứ thế chia đôi. Mỗi câu trả lời loại được một nửa số khả năng, nên chỉ cần tối đa 7 lần đoán, vì 27 = 128 ≥ 100. Tìm kiếm nhị phân làm đúng như vậy, chỉ khác là “đoán” trên một mảng đã sắp xếp.

Ví dụ từng bước

Tìm x = 23 trong mảng 10 phần tử đã sắp xếp, vị trí đánh số từ 0: a = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91].

0 1 2 3 4 5 6 7 8 9 Bước 1 2 5 8 12 16 23 38 56 72 91 lo hi mid 16 < 23: bỏ nửa trái, lo = 5 Bước 2 2 5 8 12 16 23 38 56 72 91 lo hi mid 56 > 23: bỏ nửa phải, hi = 6 Bước 3 2 5 8 12 16 23 38 56 72 91 lo=mid hi 23 = 23: tìm thấy ở vị trí 5
0 1 2 3 4 5 6 7 8 9 2 5 8 12 16 23 38 56 72 91 lo hi mid Bước 1: 16 < 23: bỏ nửa trái, lo = 5 2 5 8 12 16 23 38 56 72 91 lo hi mid Bước 2: 56 > 23: bỏ nửa phải, hi = 6 2 5 8 12 16 23 38 56 72 91 lo=mid hi Bước 3: 23 = 23: tìm thấy ở vị trí 5
Hình 1. Ba bước tìm 23. Ô xám là phần đã bị loại, ô cam là phần tử ở giữa, ô xanh là chỗ tìm thấy.
Bước lo hi mid a[mid] So với 23 Việc tiếp theo
1 0 9 4 16 16 < 23 lo = mid + 1 = 5
2 5 9 7 56 56 > 23 hi = mid − 1 = 6
3 5 6 5 23 bằng nhau Tìm thấy ở vị trí 5

Chỉ cần 3 lần so sánh. Tìm tuần tự từ đầu mảng phải xét 6 phần tử, từ vị trí 0 đến vị trí 5.

Code tìm kiếm nhị phân bằng C++ và Python

Hàm trả về vị trí của x, hoặc -1 nếu x không có trong mảng. lo và hi là hai đầu của đoạn đang xét; vòng lặp dừng khi lo > hi, tức là đoạn đã rỗng.

#include <iostream>
#include <vector>
using namespace std;

// Trả về vị trí của x trong mảng a (đã sắp xếp tăng dần), không có thì trả về -1
int binarySearch(const vector<int>& a, int x) {
    int lo = 0, hi = (int)a.size() - 1;
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;  // tránh tràn số khi lo + hi quá lớn
        if (a[mid] == x) return mid;
        if (a[mid] < x) lo = mid + 1;  // x nằm ở nửa bên phải
        else hi = mid - 1;             // x nằm ở nửa bên trái
    }
    return -1;
}

int main() {
    vector<int> a = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
    cout << binarySearch(a, 23) << "\n";  // 5
    cout << binarySearch(a, 7) << "\n";   // -1
    return 0;
}

Chạy chương trình sẽ in ra 5 và -1. Trong Python, số nguyên không bị tràn, nên viết (lo + hi) // 2 là được. Trong C++ và Java thì nên viết lo + (hi - lo) / 2, lý do ở phần lỗi hay gặp.

Độ phức tạp

Sau mỗi bước, đoạn đang xét chỉ còn khoảng một nửa. Từ n phần tử, sau k bước còn khoảng n / 2k phần tử, nên số bước tối đa vào khoảng log2 n:

  • Thời gian: O(log n).
  • Bộ nhớ: O(1) với bản dùng vòng lặp ở trên.
Số phần tử n Tìm tuần tự (tối đa) Tìm kiếm nhị phân (tối đa)
10 10 lần 4 lần
1.000 1.000 lần 10 lần
1.000.000 1.000.000 lần 20 lần
1.000.000.000 1.000.000.000 lần 30 lần

Số lần so sánh tối đa của tìm kiếm nhị phân là ⌊log2 n⌋ + 1. Mảng càng lớn thì cách biệt với tìm tuần tự càng lớn.

Học cùng giảng viên

Chủ đề này nằm trong Lecture 07 - Binary Search (Tìm kiếm nhị phân) của Khóa Blue 2.0. Xem đề cương

Hàm có sẵn: lower_bound và bisect

Khi làm bài, bạn thường không cần tự viết lại. C++ có lower_bound, upper_bound và binary_search trong thư viện <algorithm>; Python có module bisect.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> a = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
    // lower_bound: vị trí đầu tiên có giá trị >= x
    int i = lower_bound(a.begin(), a.end(), 23) - a.begin();
    int j = lower_bound(a.begin(), a.end(), 7) - a.begin();
    cout << i << " " << j << "\n";                          // 5 2
    cout << binary_search(a.begin(), a.end(), 23) << "\n";  // 1 (có 23)
    return 0;
}
  • lower_bound (C++) và bisect_left (Python): vị trí đầu tiên có giá trị ≥ x. Với x = 7, kết quả là 2 (phần tử 8). Đó cũng là chỗ chèn 7 vào mà mảng vẫn tăng dần.
  • upper_bound và bisect_right: vị trí đầu tiên có giá trị > x.
  • binary_search của C++ chỉ trả lời có hay không (true / false), không cho biết vị trí.

Lỗi hay gặp

Cẩn thận
  • Mảng chưa sắp xếp. Tìm kiếm nhị phân cho kết quả sai mà không báo lỗi gì. Hãy sắp xếp trước, hoặc đọc lại đề xem mảng đã tăng dần chưa.
  • Tràn số khi tính (lo + hi) / 2. Nếu lo + hi vượt quá 231 − 1, kiểu int của C++ hay Java bị tràn. Lỗi này từng nằm trong hàm tìm kiếm nhị phân của thư viện Java suốt nhiều năm. Viết lo + (hi - lo) / 2 là an toàn.
  • Lặp vô hạn. Viết lo = mid thay vì lo = mid + 1 thì khi đoạn còn 1 hoặc 2 phần tử, mid có thể bằng lo mãi và vòng lặp không bao giờ dừng.
  • Lệch biên. Đừng trộn hai kiểu viết, ví dụ dùng while (lo < hi) nhưng lại cập nhật hi = mid - 1. Chọn một kiểu và giữ đúng kiểu đó. Kiểu trong bài này là đoạn đóng [lo, hi]: while (lo <= hi), lo = mid + 1, hi = mid - 1.

Không chỉ để tìm trong mảng

Ý tưởng chia đôi dùng được cho mọi bài có điều kiện đơn điệu: nếu một giá trị thỏa điều kiện thì mọi giá trị lớn hơn nó cũng thỏa, hoặc ngược lại. Khi đó ta tìm kiếm nhị phân trên chính đáp án. Ví dụ: tìm số nguyên k lớn nhất có k2 ≤ N, hay thời gian nhỏ nhất để làm xong mọi việc. Dạng này rất hay gặp trong đề thi học sinh giỏi Tin học và trong phỏng vấn.

Luyện ngay trên Big-O Coding

Các bài dưới đây có dùng tìm kiếm nhị phân, làm trực tiếp trên trang luyện thuật toán của Big-O Coding:

Câu hỏi thường gặp

Tìm kiếm nhị phân có dùng được với mảng chưa sắp xếp không?

Không. Phải sắp xếp trước, mất O(n log n), nên chỉ đáng làm khi bạn cần tìm nhiều lần trên cùng một mảng. Nếu chỉ tìm một lần thì tìm tuần tự O(n) là đủ.

Tìm kiếm nhị phân nhanh hơn tìm tuần tự bao nhiêu?

Với 1 triệu phần tử, tìm kiếm nhị phân cần tối đa 20 lần so sánh, còn tìm tuần tự có thể cần tới 1 triệu lần.

Nên viết bằng đệ quy hay vòng lặp?

Cả hai đều mất O(log n) thời gian. Bản đệ quy tốn thêm O(log n) bộ nhớ cho các lần gọi hàm, nên khi làm bài người ta thường viết bằng vòng lặp như trong bài.

Mảng có nhiều phần tử bằng x thì hàm trả về vị trí nào?

Bản cơ bản trả về một vị trí bất kỳ trong số đó. Muốn lấy vị trí đầu tiên, dùng lower_bound (C++) hoặc bisect_left (Python). Vị trí ngay sau phần tử cuối cùng bằng x là upper_bound hoặc bisect_right.

Muốn học bài bản cùng giảng viên?

Luyện phỏng vấn Google, Meta, Amazon theo Interview Patterns: 16 buổi online live, lớp 7–15 người, C++/Python/Java. Học thử buổi 1 miễn phí.

Xem Khóa Blue 2.0

Nguồn tham khảo

Chia sẻ: Facebook LinkedIn
Nhắn Zalo tư vấnZalo Gọi 0937 401 483