二分查找刷题总结
推荐使用闭区间的方式去做二分查找的题目
如果数量比较少,那么建议使用顺序遍历的方式
二分结束时一定有: i指向首个大于 target 的元素,j指向首个小于 target 的元素。易得当数组不包含 target 时,插入索引为i
74. 搜索二维矩阵
问题描述:从一个二维数组中搜索到target
1、mod是列值
2、这里要考虑,只有一行或者只有一列的情况,这种情况,要恒定为0
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int left = 0;
int right = matrix.length * matrix[0].length - 1;
if (right == 0) {
return matrix[0][0] == target;
}
int mod = matrix[0].length;
while(left <= right) {
int mid = left + (right - left) / 2;
int row = mid / mod;
int col = mid % mod;
if (matrix[row][col] > target) {
right = mid - 1;
} else if (matrix[row][col] < target) {
left = mid + 1;
} else {
return true;
}
}
return false;
}
}
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int top = 0, bottom = m - 1;
// 首先确定在哪一行
while (top <= bottom) {
int midRow = top + (bottom - top) / 2;
if (matrix[midRow][0] <= target && target <= matrix[midRow][n-1]) {
// 在这一行,进行列二分
int left = 0, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (matrix[midRow][mid] == target) {
return true;
} else if (matrix[midRow][mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
} else if (target < matrix[midRow][0]) {
bottom = midRow - 1;
} else {
top = midRow + 1;
}
}
return false;
}
}
162. 寻找峰值
寻找最大值,这个也可以理解,嗯
大的一侧为什么一定有峰值?注意题目条件,在题目描述中出现了 nums[-1] = nums[n] = -∞,这就代表着 只要数组中存在一个元素比相邻元素大,那么沿着它一定可以找到一个峰值
沿着大的方向走,肯定会存在一个峰值
判断逻辑:不用 mid 和 left 比,而是和 mid+1 比,核心是「通过上坡 / 下坡确定峰值方向」,保证二分的有效性;
边界控制:循环条件 l < r,收缩规则 l=mid+1/r=mid,始终保证「峰值在 [l, r] 区间内」;
结果推导:循环终止时 l==r,区间只剩一个元素,这个元素就是峰值,因此返回 l 即可。
class Solution {
public int findPeakElement(int[] nums) {
int l = 0, r = nums.length - 1;
while (l < r) {
int mid = l + ((r - l) >> 1);
if (nums[mid] < nums[mid + 1]) {
l = mid + 1;
} else {
r = mid;
}
}
return l;
}
}
33. 搜索旋转排序数组
我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的
当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:
- 如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [nums[l],nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
- 如果 [mid, r] 是有序数组,且 target 的大小满足 [nums[mid+1],nums[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。

public int search(int[] nums, int target) {
int lo = 0, hi = nums.length - 1, mid = 0;
while (lo <= hi) {
mid = lo + (hi - lo) / 2;
if (nums[mid] == target) {
return mid;
}
if (nums[mid] >= nums[lo]) { // left到mid是有序数据
if (target >= nums[lo] && target < nums[mid]) {
hi = mid - 1;
} else {
lo = mid + 1;
}
} else { // mid --> right是有序数组
if (target > nums[mid] && target <= nums[hi]) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
}
return -1;
}
34. 在排序数组中查找元素的第一个和最后一个位置
ACE了
class Solution {
public int[] searchRange(int[] nums, int target) {
return new int[] { left(nums, target), right(nums, target) };
}
public int left(int[] nums, int target) {
int i = binarySearch(nums, target);
if (i == nums.length || nums[i] != target) {
return -1;
}
return i;
}
public int right(int[] nums, int target) {
int i = binarySearch(nums, target + 1);
int j = i - 1;
if (j == -1 || nums[j] != target) {
return -1;
}
return j;
}
public int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
right = mid - 1;
}
}
return left;
}
}
153. 寻找旋转排序数组中的最小值
第二次做,思路还是错误的,这里要利用它的特点,如果mid>small,那么直接left = mid + 1, 否则 right = mid - 1;
解题总结:
1、边界条件判断
2、如果单调的,直接返回第一个元素即可
3、动态调整small,当前最small为nums[0]
4、如果大于small,那么缩小左边的边界
5、如果小于small,那么缩小右边的边界,然后更新small的值
6、最后循环结束的时候,已经记录到了small。
class Solution {
public int findMin(int[] nums) {
int l = 0;
int r = nums.length - 1;
if (nums.length == 1) {
return nums[0];
}
if (nums[0] < nums[nums.length - 1]) {
return nums[0];
}
int small = nums[0];
while(l <= r) {
int m = (l + r) / 2;
if (nums[m] >= small) {
l = m + 1;
} else {
r = m - 1;
small = nums[m];
}
}
return nums[l];
}
}
4. 寻找两个正序数组的中位数
这个题目确实是LeetCode的经典难题(Hard级别),核心难点在于要求O(log(m+n))的时间复杂度(如果只是合并数组找中位数,O(m+n)的复杂度很简单,但不符合最优要求)。我们先从“为什么难”开始,一步步拆解代码的逻辑。
一、先明确问题:中位数的定义
中位数是把一个有序数组分成等长的两半后,“中间”的那个数:
- 如果总长度是奇数:中位数 = 第
(len/2 + 1)小的数(比如总长度5,第3小的数); - 如果总长度是偶数:中位数 = (第
len/2小的数 + 第len/2 + 1小的数)/ 2(比如总长度4,第2小+第3小的平均数)。
比如:nums1=[1,3],nums2=[2] → 合并后[1,2,3] → 总长度3(奇数)→ 第2小的数是2(中位数);
nums1=[1,2],nums2=[3,4] → 合并后[1,2,3,4] → 总长度4(偶数)→ (2+3)/2=2.5(中位数)。
二、核心思路:把“找中位数”转化为“找第k小的数”
代码的关键是 getKthElement 函数(找两个有序数组中第k小的数),因为:
- 奇数长度:找第
(totalLen/2 + 1)小的数; - 偶数长度:找第
totalLen/2和totalLen/2 + 1小的数,再取平均。
为什么要这么转化?因为“找第k小”可以用二分法实现O(log(m+n))的复杂度,而直接合并数组是O(m+n),不符合最优要求。
三、拆解 getKthElement 函数(核心中的核心)
3.1 二分法的核心思想:“排除不可能的数”
找第k小的数,我们不需要合并数组,而是通过每次排除掉k/2个不可能是第k小的数,把问题规模缩小,直到k=1(直接取两个数组当前首元素的最小值)。
举个例子理解:
nums1=[1,4,7,9],nums2=[2,3,5,8],找第5小的数(k=5)。
- 第一步:k=5 → 取k/2=2(整除)→ 看nums1的第2个元素(nums1[1]=4)、nums2的第2个元素(nums2[1]=3);
- 比较4和3:3更小 → 说明nums2的前2个元素(2,3)都不可能是第5小的数(因为最多只有1+2=3个数比3小,3最多是第4小);
- 排除nums2的前2个元素 → 问题变成:nums1=[1,4,7,9],nums2=[5,8],找第5-2=3小的数(k=3);
- 第二步:k=3 → k/2=1 → 看nums1的第1个元素(1)、nums2的第1个元素(5);
- 1更小 → 排除nums1的前1个元素 → 问题变成:nums1=[4,7,9],nums2=[5,8],找第3-1=2小的数(k=2);
- 第三步:k=2 → k/2=1 → 看nums1的第1个元素(4)、nums2的第1个元素(5);
- 4更小 → 排除nums1的前1个元素 → 问题变成:nums1=[7,9],nums2=[5,8],找第2-1=1小的数(k=1);
- k=1 → 取min(7,5)=5 → 第5小的数是5(验证:合并数组[1,2,3,4,5,7,8,9],第5小确实是5)。
3.2 代码逐行拆解 getKthElement
public int getKthElement(int[] nums1, int[] nums2, int k) {
int length1 = nums1.length, length2 = nums2.length;
int index1 = 0, index2 = 0; // 两个数组的“当前起始位置”(表示前面的数已经被排除了)
int kthElement = 0;
while (true) {
// 边界情况1:nums1已经排除完所有元素 → 直接取nums2的第k个元素
if (index1 == length1) {
return nums2[index2 + k - 1];
}
// 边界情况2:nums2已经排除完所有元素 → 直接取nums1的第k个元素
if (index2 == length2) {
return nums1[index1 + k - 1];
}
// 边界情况3:k=1 → 直接取两个数组当前首元素的最小值
if (k == 1) {
return Math.min(nums1[index1], nums2[index2]);
}
// 正常情况:计算要比较的“候选位置”
int half = k / 2; // 每次排除k/2个元素
// 候选位置 = 当前起始位置 + half - 1(因为数组索引从0开始)
// 注意:要防止候选位置超过数组长度(比如nums1只剩1个元素,half=2)
int newIndex1 = Math.min(index1 + half, length1) - 1;
int newIndex2 = Math.min(index2 + half, length2) - 1;
// 两个候选值
int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
// 比较候选值,排除更小的那个数组的前半部分
if (pivot1 <= pivot2) {
// 排除nums1中index1到newIndex1的元素 → 共(newIndex1 - index1 + 1)个
k -= (newIndex1 - index1 + 1);
// 更新nums1的起始位置(排除后的下一个位置)
index1 = newIndex1 + 1;
} else {
// 排除nums2中index2到newIndex2的元素
k -= (newIndex2 - index2 + 1);
// 更新nums2的起始位置
index2 = newIndex2 + 1;
}
}
}
3.3 边界情况的解释
- 边界1/2:比如nums1=[1,2],nums2=[3,4,5],k=4 → 排除nums1的2个元素后,nums1已经空了 → 直接取nums2的第4-2=2个元素(nums2[0+2-1]=nums2[1]=4);
- 边界3:k=1时,两个数组的当前首元素中更小的那个,就是第1小的数(比如nums1=[4], nums2=[5] → min(4,5)=4)。
3.4 关键细节:为什么 newIndex1 = Math.min(index1 + half, length1) - 1?
比如nums1=[1,3](length1=2),index1=0,half=3(k=6)→ index1+half=3,超过length1=2 → 所以newIndex1取length1-1=1(nums1的最后一个元素),避免数组越界。
四、拆解主函数 findMedianSortedArrays
主函数的逻辑很简单,就是把“找中位数”转化为“找第k小的数”:
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int length1 = nums1.length, length2 = nums2.length;
int totalLength = length1 + length2;
if (totalLength % 2 == 1) { // 奇数长度
int midIndex = totalLength / 2;
// 第(midIndex + 1)小的数(比如totalLength=5,midIndex=2 → 第3小)
double median = getKthElement(nums1, nums2, midIndex + 1);
return median;
} else { // 偶数长度
int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
// 第(midIndex1 + 1)小 和 第(midIndex2 + 1)小 的平均数
double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
return median;
}
}
举个例子验证:
nums1=[1,2],nums2=[3,4] → totalLength=4(偶数)→ midIndex1=1,midIndex2=2 → 找第2小和第3小的数(2和3)→ (2+3)/2=2.5,正确。
五、整体流程总结(用一个例子串起来)
以nums1=[1,3],nums2=[2]为例:
- totalLength=3(奇数)→ midIndex=1 → 找第2小的数;
- 调用getKthElement(nums1, nums2, 2):
- index1=0,index2=0,k=2;
- half=1 → newIndex1=0(0+1-1),newIndex2=0(0+1-1);
- pivot1=1,pivot2=2 → 1<=2 → 排除nums1的前1个元素(k=2-1=1,index1=1);
- 现在k=1 → 返回min(nums1[1]=3, nums2[0]=2) → 2;
- 主函数返回2 → 正确。
六、为什么这个算法是O(log(m+n))?
每次循环都会把k缩小一半(排除k/2个元素),直到k=1。而k的最大值是m+n,所以循环次数是log2(m+n),也就是时间复杂度O(log(m+n)),符合最优要求。
七、常见疑问解答
1. 为什么排除的是“更小的候选值所在数组的前半部分”?
比如pivot1 <= pivot2 → 说明nums1的前half个元素都比pivot2小,这些元素最多只能是第k-1小的数(因为nums2的前half个元素已经比pivot1大),所以不可能是第k小的数,直接排除。
2. 为什么k要减去排除的元素个数?
因为排除的元素都是比第k小的数更小的数,所以剩下的问题是“在剩下的元素中找第k-排除个数小的数”。
3. 有没有更简单的方法?
有,但复杂度更高:把两个数组合并成一个有序数组,然后直接取中位数(O(m+n))。但题目隐含要求最优复杂度(O(log(m+n))),所以必须用二分法。
八、简化记忆
- 中位数 = 奇数取第(len/2+1)小,偶数取(len/2和len/2+1)小的平均;
- 找第k小:每次排除k/2个不可能的数,直到k=1;
- 排除规则:比较两个数组的第k/2个元素,排除更小的那个数组的前k/2个元素;
- 边界:数组空了直接取另一个数组的第k个元素,k=1取最小值。
这个题目确实需要多刷几遍,建议先手动模拟2-3个例子(比如上面的例子),再对照代码走一遍,就能慢慢理解二分排除的逻辑了。

浙公网安备 33010602011771号