Tìm kiếm nhị phân
Cập nhật lần cuối
Tìm kiếm nhị phân (binary search) tìm một giá trị đích trong mảng **đã sắp xếp** bằng cách liên tục chia đôi vùng tìm kiếm. Thuật toán so sánh phần tử ở giữa với giá trị đích: nếu khớp thì dừng; nếu không, nửa không thể chứa giá trị đích bị loại bỏ và vùng tìm kiếm thu hẹp lại còn nửa kia. Mỗi lần so sánh loại đi một nửa số phần tử còn lại, đó là lý do thuật toán chạy trong O(log n): tìm trong một triệu giá trị đã sắp xếp chỉ cần tối đa khoảng 20 lần so sánh.
Hoạt ảnh ở trên hiển thị các con trỏ lo, mid và hi, đồng thời làm mờ nửa bị loại sau mỗi lần so sánh. Điều kiện tiên quyết bắt buộc: mảng phải được sắp xếp sẵn. Với dữ liệu chưa sắp xếp, bạn cần dùng tìm kiếm tuyến tính hoặc sắp xếp trước (xem sắp xếp trộn). Chính ý tưởng chia đôi này cũng là nền tảng của cây tìm kiếm nhị phân.
Độ phức tạp thời gian và không gian
| Trường hợp | Độ phức tạp | Ghi chú |
|---|---|---|
| Tốt nhất | O(1) | Phần tử ở giữa chính là giá trị đích ngay lần so sánh đầu tiên. |
| Trung bình | O(log n) | Mỗi lần so sánh chia đôi vùng tìm kiếm còn lại. |
| Xấu nhất | O(log n) | Vùng tìm kiếm thu hẹp còn một phần tử trước khi tìm thấy hoặc không thấy. |
| Không gian | O(1) | Phiên bản vòng lặp chỉ lưu các chỉ số lo, hi và mid. |
Từng bước thực hiện
| Bước | Điều gì xảy ra |
|---|---|
| 1 | Đặt lo là chỉ số đầu tiên và hi là chỉ số cuối cùng của mảng đã sắp xếp. |
| 2 | Tính chỉ số ở giữa: mid = (lo + hi) // 2. |
| 3 | Nếu a[mid] bằng giá trị đích, trả về mid (đã tìm thấy). |
| 4 | Nếu a[mid] **nhỏ hơn** giá trị đích, giá trị đích chỉ có thể nằm ở nửa phải: đặt lo = mid + 1. |
| 5 | Nếu a[mid] **lớn hơn** giá trị đích, tìm ở nửa trái: đặt hi = mid - 1. |
| 6 | Lặp lại từ bước 2 khi lo <= hi; nếu vùng tìm kiếm rỗng, giá trị đích không có trong mảng. |
Ví dụ minh họa
Tìm 5 trong [1, 2, 3, 5, 7, 8, 9]:
| Lượt | Vùng tìm kiếm (lo..hi) | mid | a[mid] | Thao tác |
|---|---|---|---|---|
| 1 | [1, 2, 3, 5, 7, 8, 9] (0..6) | 3 | 5 | a[3] = 5: tìm thấy giá trị đích tại chỉ số 3. |
Từng bước khi không tìm thấy
Tìm 4 trong cùng mảng đó cho thấy vùng tìm kiếm rỗng dần như thế nào:
| Lượt | Vùng tìm kiếm (lo..hi) | mid | a[mid] | Thao tác |
|---|---|---|---|---|
| 1 | 0..6 | 3 | 5 | 5 > 4: tìm ở nửa trái, hi = 2. |
| 2 | 0..2 | 1 | 2 | 2 < 4: tìm ở nửa phải, lo = 2. |
| 3 | 2..2 | 2 | 3 | 3 < 4, nên lo thành 3 và vùng tìm kiếm rỗng: không tìm thấy. |
Khi nào nên dùng tìm kiếm nhị phân
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Dữ liệu đã được sắp xếp (hoặc bạn tìm kiếm trên đó nhiều lần) | Dữ liệu chưa sắp xếp và chỉ tìm một lần (sắp xếp trước tốn O(n log n)) |
| Cấu trúc dữ liệu hỗ trợ truy cập ngẫu nhiên nhanh (mảng) | Bạn chỉ có thể truy cập tuần tự (danh sách liên kết) |
Tập dữ liệu lớn (O(log n) thể hiện rõ sức mạnh ở quy mô lớn) | Tập dữ liệu rất nhỏ (duyệt tuần tự cũng nhanh như vậy mà đơn giản hơn) |
Code Binary Search
Một cách cài đặt Binary Search gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C, Pseudocode. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.
Code Binary Search bằng Python
1def binary_search(a, target):2 lo, hi = 0, len(a) - 13 while lo <= hi:4 mid = (lo + hi) // 25 if a[mid] == target:6 return mid7 if a[mid] < target:8 lo = mid + 1 # search the right half9 else:10 hi = mid - 1 # search the left half11 return -112
13
14nums = [1, 2, 3, 5, 7, 8, 9] # must be sorted15print("Index of 5:", binary_search(nums, 5))16print("Index of 4:", binary_search(nums, 4))Code Binary Search bằng JavaScript
1function binarySearch(a, target) {2 let lo = 0;3 let hi = a.length - 1;4 while (lo <= hi) {5 const mid = Math.floor((lo + hi) / 2);6 if (a[mid] === target) return mid;7 if (a[mid] < target) {8 lo = mid + 1; // search the right half9 } else {10 hi = mid - 1; // search the left half11 }12 }13 return -1;14}15
16const nums = [1, 2, 3, 5, 7, 8, 9]; // must be sorted17console.log("Index of 5:", binarySearch(nums, 5));18console.log("Index of 4:", binarySearch(nums, 4));Code Binary Search bằng Java
1public class Main {2 static int binarySearch(int[] a, int target) {3 int lo = 0;4 int hi = a.length - 1;5 while (lo <= hi) {6 int mid = (lo + hi) / 2;7 if (a[mid] == target) return mid;8 if (a[mid] < target) {9 lo = mid + 1; // search the right half10 } else {11 hi = mid - 1; // search the left half12 }13 }14 return -1;15 }16
17 public static void main(String[] args) {18 int[] nums = {1, 2, 3, 5, 7, 8, 9}; // must be sorted19 System.out.println("Index of 5: " + binarySearch(nums, 5));20 System.out.println("Index of 4: " + binarySearch(nums, 4));21 }22}Code Binary Search bằng C++
1#include <iostream>2#include <vector>3
4int binarySearch(const std::vector<int>& a, int target) {5 int lo = 0;6 int hi = static_cast<int>(a.size()) - 1;7 while (lo <= hi) {8 int mid = lo + (hi - lo) / 2;9 if (a[mid] == target) return mid;10 if (a[mid] < target) {11 lo = mid + 1; // search the right half12 } else {13 hi = mid - 1; // search the left half14 }15 }16 return -1;17}18
19int main() {20 std::vector<int> nums = {1, 2, 3, 5, 7, 8, 9}; // must be sorted21 std::cout << "Index of 5: " << binarySearch(nums, 5) << "\n";22 std::cout << "Index of 4: " << binarySearch(nums, 4) << "\n";23 return 0;24}Code Binary Search bằng C
1#include <stdio.h>2
3int binary_search(const int a[], int n, int target) {4 int lo = 0;5 int hi = n - 1;6 while (lo <= hi) {7 int mid = lo + (hi - lo) / 2;8 if (a[mid] == target) return mid;9 if (a[mid] < target) {10 lo = mid + 1; /* search the right half */11 } else {12 hi = mid - 1; /* search the left half */13 }14 }15 return -1;16}17
18int main(void) {19 int nums[] = {1, 2, 3, 5, 7, 8, 9}; /* must be sorted */20 int n = sizeof(nums) / sizeof(nums[0]);21 printf("Index of 5: %d\n", binary_search(nums, n, 5));22 printf("Index of 4: %d\n", binary_search(nums, n, 4));23 return 0;24}Code Binary Search bằng Pseudocode
1DECLARE nums : ARRAY[1:7] OF INTEGER2DECLARE n : INTEGER3n ← 74// The array must be sorted for binary search5nums[1] ← 16nums[2] ← 27nums[3] ← 38nums[4] ← 59nums[5] ← 710nums[6] ← 811nums[7] ← 912
13FUNCTION binarySearch(target : INTEGER) RETURNS INTEGER14 DECLARE lo : INTEGER15 DECLARE hi : INTEGER16 DECLARE mid : INTEGER17 lo ← 118 hi ← n19 WHILE lo <= hi DO20 mid ← (lo + hi) DIV 221 IF nums[mid] = target THEN22 RETURN mid23 ENDIF24 IF nums[mid] < target THEN25 // Target is larger, search the right half26 lo ← mid + 127 ELSE28 // Target is smaller, search the left half29 hi ← mid - 130 ENDIF31 ENDWHILE32 RETURN -133ENDFUNCTION34
35OUTPUT "Index of 5 is ", binarySearch(5)36OUTPUT "Index of 4 is ", binarySearch(4)Câu hỏi thường gặp về tìm kiếm nhị phân
Độ phức tạp thời gian của tìm kiếm nhị phân là bao nhiêu?
O(log n) trong trường hợp trung bình và xấu nhất, vì mỗi lần so sánh chia đôi vùng tìm kiếm còn lại, và O(1) trong trường hợp tốt nhất khi phần tử ở giữa đầu tiên chính là giá trị đích. Phiên bản vòng lặp dùng thêm O(1) bộ nhớ.Vì sao tìm kiếm nhị phân cần mảng đã sắp xếp?
Tìm kiếm nhị phân và tìm kiếm tuyến tính khác nhau như thế nào?
O(n)) và chạy được trên mọi mảng; tìm kiếm nhị phân chia đôi vùng tìm kiếm của mảng đã sắp xếp (O(log n)) nhưng cần dữ liệu đầu vào đã sắp xếp. Với vài phần tử thì khác biệt không đáng kể; ở quy mô lớn, tìm kiếm nhị phân thắng áp đảo.Tìm kiếm nhị phân cần bao nhiêu lần so sánh?
log2(n) + 1: 10 lần so sánh đủ cho 1.000 phần tử, 20 lần đủ cho 1.000.000 phần tử. Mức tăng theo logarit đó khiến nó trở thành cách tra cứu mặc định trên dữ liệu đã sắp xếp.Lỗi tràn số kinh điển trong tìm kiếm nhị phân là gì?
(lo + hi) / 2 có thể gây tràn số với kiểu số nguyên có kích thước cố định khi lo + hi vượt quá giá trị lớn nhất của kiểu. Cách an toàn là mid = lo + (hi - lo) / 2. Trong Python điều này không quan trọng (số nguyên có độ chính xác tùy ý), nhưng trong Java/C/C++ đây là một bug có thật và rất nổi tiếng.