二分查找(Binary Search)是计算机科学中最基础也最高效的查找算法之一,它能在O(log n)的时间复杂度内完成有序数据的查找操作。作为一名有多年算法教学经验的工程师,我发现很多初学者虽然能写出二分查找的代码,但对其中精妙之处和实际应用场景理解不深。今天我就通过LeetCode上的6道经典题目,带大家彻底掌握这个算法的核心思想。
二分查找的基本思想非常简单:每次都将搜索范围缩小一半。对于一个有序数组,我们首先比较中间元素与目标值:
这种分治策略使得算法的时间复杂度从线性查找的O(n)降低到O(log n),对于大规模数据查找效率提升显著。
注意:二分查找的前提是数据必须是有序的,如果是无序数组需要先排序才能使用二分查找
在实现二分查找时,有几个关键细节需要特别注意:
这些细节处理不当会导致死循环或者错误结果。在下面的具体题目解析中,我会逐一讲解这些问题的解决方案。
LeetCode 704是最基础的二分查找题目,要求在一个有序数组中查找目标值,存在则返回索引,不存在则返回-1。
cpp复制class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
};
初学者常犯的错误包括:
调试技巧:
这道题要求在有序数组中查找目标值,如果存在返回索引,不存在则返回应该插入的位置。这是二分查找的一个典型变种。
cpp复制class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left; // 关键变化在这里
}
};
唯一区别在于返回值处理:
这是因为当循环结束时,left指向的就是目标值应该插入的位置。这个性质在解决其他二分查找变种问题时非常有用。
这种变种在实际开发中很常见,比如:
这道题要求在有序数组中查找目标值的起始和结束位置。如果目标值不存在,返回[-1, -1]。这是二分查找的一个高级变种,需要找到目标值的左边界和右边界。
cpp复制class Solution {
int searchLeft(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
public:
vector<int> searchRange(vector<int>& nums, int target) {
int leftIdx = searchLeft(nums, target);
if (leftIdx == nums.size() || nums[leftIdx] != target) {
return {-1, -1};
}
int rightIdx = searchLeft(nums, target + 1) - 1;
return {leftIdx, rightIdx};
}
};
这种实现方式:
相比线性扫描的O(n)时间复杂度,二分查找版本在大数据量时优势明显。
这道题要求计算非负整数x的平方根,结果只保留整数部分。我们可以将这个问题转化为在[0, x]范围内查找最大的整数n使得n² ≤ x。
cpp复制class Solution {
public:
int mySqrt(int x) {
if (x <= 1) return x;
int left = 0, right = x;
while (left < right) {
int mid = left + (right - left) / 2;
if (x / mid >= mid) { // 等价于mid*mid <= x,但防止溢出
left = mid + 1;
} else {
right = mid;
}
}
return left - 1;
}
};
这道题判断一个数是否是完全平方数,可以看作是平方根问题的变种。
cpp复制class Solution {
public:
bool isPerfectSquare(int num) {
if (num <= 1) return true;
long left = 0, right = num;
while (left <= right) {
long mid = left + (right - left) / 2;
long square = mid * mid;
if (square == num) {
return true;
} else if (square < num) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
};
这道题给定一个m×n的矩阵,行列都是非递增顺序排列,要求统计其中负数的个数。最直观的解法是双重循环遍历整个矩阵。
cpp复制class Solution {
public:
int countNegatives(vector<vector<int>>& grid) {
int count = 0;
for (const auto& row : grid) {
for (int num : row) {
if (num < 0) count++;
}
}
return count;
}
};
由于每行都是有序的,我们可以对每行使用二分查找找到第一个负数的位置,然后计算该行的负数个数。
cpp复制class Solution {
public:
int countNegatives(vector<vector<int>>& grid) {
int count = 0;
for (const auto& row : grid) {
// 使用二分查找找到第一个负数的位置
int left = 0, right = row.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (row[mid] >= 0) {
left = mid + 1;
} else {
right = mid;
}
}
count += row.size() - left;
}
return count;
}
};
在实际面试中,面试官通常会期望你从暴力解法开始,然后逐步优化,最后可能还会讨论最优解法。
二分查找不仅仅用于算法题,在实际工程中也有广泛应用:
设计测试用例时应考虑:
在实现二分查找时,我通常会添加详细的日志输出,打印每次循环的left、right、mid值,这能帮助快速定位问题。
掌握了基础二分查找后,可以进一步学习以下内容:
二分查找的思想还可以扩展到:
在实际工程中,很多看似复杂的问题都可以转化为二分查找问题。关键是要培养将问题抽象为"在有序数据中查找满足某条件的边界"的能力。