二分查找力扣题(leetcode)
4. 寻找两个正序数组的中位数
难度:困难
题目:
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
示例 1:
C++
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
C++
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
提示:
- \(nums1.length == m\)
- \(nums2.length == n\)
- \(0 <= m <= 1000\)
- \(0 <= n <= 1000\)
- \(1 <= m + n <= 2000\)
- \(-10^6 <= nums1[i], nums2[i] <= 10^6\)
代码:
C++
#include <vector>
using namespace std;
class Solution
{
public:
// 临时数组(足够容纳题目最大2000个元素)
int tmp[2010];
// 合并两个有序数组(核心修正:长度计算+返回完整合并结果)
vector<int> merge_sort(int q1[], int l1, int r1, int q2[], int l2, int r2)
{
int i = l1, j = l2, k = 0;
// 归并核心逻辑
while(i <= r1 && j <= r2)
{
if(q1[i] <= q2[j]) tmp[k++] = q1[i++];
else tmp[k++] = q2[j++];
}
while(i <= r1) tmp[k++] = q1[i++];
while(j <= r2) tmp[k++] = q2[j++];
// 核心修正1:长度计算错误 → 正确计算总元素数
int size = k; // k是实际合并的元素个数(最可靠)
// 核心修正2:vector构造时用实际长度,不要-1
vector<int> vec(tmp, tmp + size);
return vec;
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2)
{
// 处理空数组边界情况
int* p1 = nums1.empty() ? nullptr : &nums1[0];
int len1 = nums1.size();
int* p2 = nums2.empty() ? nullptr : &nums2[0];
int len2 = nums2.size();
// 合并两个数组
vector<int> mergeVec;
if (len1 == 0) mergeVec = nums2; // nums1空,直接用nums2
else if (len2 == 0) mergeVec = nums1; // nums2空,直接用nums1
else mergeVec = merge_sort(p1, 0, len1-1, p2, 0, len2-1);
// 计算中位数
int total = mergeVec.size();
if (total % 2 == 1)
return (double)mergeVec[total / 2];// 奇数:取中间元素
else
{
// 偶数:取中间两个数的平均值
int mid1 = mergeVec[total / 2 - 1];
int mid2 = mergeVec[total / 2];
return (mid1 + mid2) / 2.0;
}
}
};
// //模板
// int bSearch(int l, int r, int x)
// {
// while(l < r)
// {
// int mid = (l+r) >> 1;
// if(q[mid] >= x) r = mid;
// else l = mid + 1;
// }
// return l;
// }
// double bSearch(double l, double r, double x)
// {
// const int eps = 1e-6;
// while(l - r > eps)
// {
// if(check(mid) > x) r = mid;
// else l = mid;
// }
// return l;
// }
// void merge_sort(int q[], int l, int r)
// {
// //终止条件
// if(l >= r) return;
// //拆分成子问题
// int mid = (l+r)>>1;
// //递归处理子问题
// merge_sort(q, l, mid), merge_sort(q, mid+1, r);
// //合并子问题
// int i = l, j = mid+1, k = 0;
// while(i <= mid && j <= r)
// {
// if(q[i] <= q[j]) tmp[k++] = q[i++];
// else tmp[k++] = q[j++];
// }
// while(i <= mid) tmp[k++] = q[i++];
// while(j <= r) tmp[k++] = q[j++];
// //复制回原数组
// for(int i = l, j = 0; i <= r; i++,j++) q[i] = tmp[j];
// }
相似题目
33. 搜索旋转排序数组
难度:中等
题目:
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 向左旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 下标 3 上向左旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
C++
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
C++
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
C++
输入:nums = [1], target = 0
输出:-1
提示:
- \(1 <= nums.length <= 5000\)
- \(-10^4 <= nums[i] <= 10^4\)
- \(nums\) 中的每个值都 独一无二
- 题目数据保证
nums在预先未知的某个下标上进行了旋转 - \(-10^4 <= target <= 10^4\)
代码:
C++
class Solution
{
public:
int bSearch(vector<int>& nums, int l, int r, int x)
{
while(l < r)
{
int mid = (l + r) >> 1;
if(nums[mid] >= x) r = mid;
else l = mid + 1;
}
// 循环结束后l=r,需要验证是否等于x(避免找不到的情况)
return (nums[l] == x) ? l : -1;
}
int bSearchRotatePoint(vector<int>& nums, int l, int r)
{
while(l < r)
{
int mid = (l + r) >> 1;
// 旋转点在右段:mid比最后一个元素大,说明最小元素在mid右边
if(nums[mid] > nums[r]) l = mid + 1;
else r = mid; // 否则在mid或左边
}
return l;//循环结束l=r,就是旋转点
}
int search(vector<int>& nums, int target)
{
int n = nums.size();
if(n == 0) return -1;
// 第一步:用二分找旋转点(最小元素的下标)
int rotate_idx = bSearchRotatePoint(nums, 0, n-1);
// 第二步:分两段用你的模板找target
if(target == nums[rotate_idx]) return rotate_idx; // 刚好是旋转点
// 情况1:target在左段([0, rotate_idx-1])
if(rotate_idx > 0 && target >= nums[0] && target <= nums[rotate_idx-1])
return bSearch(nums, 0, rotate_idx - 1, target);
// 情况2:target在右段([rotate_idx, n-1])
else if(rotate_idx < n-1 && target >= nums[rotate_idx] && target <= nums[n-1])
return bSearch(nums, rotate_idx, n - 1, target);
// 情况3:只有一段(数组没旋转,或target不在数组中)
else
{
// 数组没旋转时,直接用模板找整个数组
if(rotate_idx == 0) return bSearch(nums, 0, n-1, target);
// 否则target不存在
else return -1;
}
}
};
相似题目
34. 在排序数组中查找元素的第一个和最后一个位置
难度:中等
题目:
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
C++
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
C++
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
C++
输入:nums = [], target = 0
输出:[-1,-1]
提示:
- \(0 <= nums.length <= 10^5\)
- \(-10^9 <= nums[i] <= 10^9\)
- \(nums\) 是一个非递减数组
- \(-10^9 <= target <= 10^9\)
代码:
C++
#include <vector>
using namespace std;
class Solution
{
public:
int bSearch(vector<int>& q, int l, int r, int x)
{
while (l < r)
{
int mid = (l + r) >> 1;
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
return (q[l] == x) ? l : -1;
}
bool searchMatrix(vector<vector<int>>& matrix, int target)
{
// 边界1:空矩阵直接返回false
if (matrix.empty() || matrix[0].empty()) return false;
int m = matrix.size(); // 行数
int n = matrix[0].size(); // 列数
// 二分查找target
for (int i = 0; i < m; i++)
{
// 优化:如果当前行的第一个元素>target,后续行都更大,直接break
if (matrix[i][0] > target) break;
// 优化:如果当前行的最后一个元素<target,跳过当前行
if (matrix[i][n-1] < target) continue;
// 通过二分法,查找当前行是否有target
int pos = bSearch(matrix[i], 0, n-1, target);
if (pos != -1) return true;// 找到target
}
// 所有行都没找到
return false;
}
};
相似题目
第一个错误的版本简单
蜡烛之间的盘子中等
240. 搜索二维矩阵 II
难度:中等
题目:
编写一个高效的算法来搜索 *m* x *n* 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
示例 1:
C++
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
C++
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
C++
输入:nums = [], target = 0
输出:[-1,-1]
提示:
- \(0 <= nums.length <= 10^5\)
- \(-10^9 <= nums[i] <= 10^9\)
- \(nums\) 是一个非递减数组
- \(-10^9 <= target <= 10^9\)
代码:
C++
#include <vector>
using namespace std;
class Solution
{
public:
int bSearch(vector<int>& q, int l, int r, int x)
{
while (l < r)
{
int mid = (l + r) >> 1;
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
return (q[l] == x) ? l : -1;
}
bool searchMatrix(vector<vector<int>>& matrix, int target)
{
// 边界1:空矩阵直接返回false
if (matrix.empty() || matrix[0].empty()) return false;
int m = matrix.size(); // 行数
int n = matrix[0].size(); // 列数
// 二分查找target
for (int i = 0; i < m; i++)
{
// 优化:如果当前行的第一个元素>target,后续行都更大,直接break
if (matrix[i][0] > target) break;
// 优化:如果当前行的最后一个元素<target,跳过当前行
if (matrix[i][n-1] < target) continue;
// 通过二分法,查找当前行是否有target
int pos = bSearch(matrix[i], 0, n-1, target);
if (pos != -1) return true;// 找到target
}
// 所有行都没找到
return false;
}
};
相似题目
第一个错误的版本简单
蜡烛之间的盘子中等
287. 寻找重复数
难度:中等
题目:
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
示例 1:
C++
输入:nums = [1,3,4,2,2]
输出:2
示例 2:
C++
输入:nums = [3,1,3,4,2]
输出:3
示例 3:
C++
输入:nums = [3,3,3,3,3]
输出:3
提示:
- \(0 <= nums.length <= 10^5\)
- \(-10^9 <= nums[i] <= 10^9\)
- \(nums\) 是一个非递减数组
- \(-10^9 <= target <= 10^9\)
代码:
C++
class Solution
{
public:
int bSearch(vector<int>& q, int l, int r)
{
while(l < r)
{
int mid = (l + r) >> 1;
// 统计数组中 <= mid 的数的个数
int cnt = 0;
for(int num : q)
{
if(num <= mid) cnt++;
}
// 核心判断:cnt>mid说明重复数在左区间[1,mid]
if(cnt > mid) r = mid;
else l = mid + 1; // 否则在右区间[mid+1, r]
}
return l; // 最终收敛到重复数
}
int findDuplicate(vector<int>& nums)
{
// 数值范围是[1, nums.size()-1](因为数组长度是n+1,数值是1~n)
return bSearch(nums, 1, nums.size() - 1);
}
};
「核心公理」(解题的关键)
题目条件:数组有 n+1 个数,数值都在 [1, n] 之间,且只有一个数重复。
👉 正常情况下(无重复):数值 ≤ mid 的数,最多有 mid 个。
👉 异常情况(有重复):如果数值 ≤ mid 的数 超过 mid 个 ,说明重复数一定藏在 [1, mid] 里;反之则藏在 [mid+1, n] 里。
✅ 「升序的 1~n 无重复数组」是「≤mid 的数 = mid」的标准情况(也是最多的情况);
✅ 只要数组里有重复数,就会让某个 mid 对应的「≤mid 的数的个数」超过 mid ------ 这就是我们二分找重复数的核心依据。
相似题目
缺失的第一个正数困难
只出现一次的数字简单
环形链表 II中等
丢失的数字简单
错误的集合简单
300. 最长递增子序列
难度:中等
题目:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
C++
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
C++
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
C++
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
- \(1 <= nums.length <= 2500\)
- \(-10^4 <= nums[i] <= 10^4\)
代码:
C++
class Solution
{
public:
// 复用你的二分模板:找数组q中[l, r]里第一个 >= x的位置
int bSearch(vector<int>& q, int l, int r, int x)
{
while(l < r)
{
int mid = (l + r) >> 1; // 和你的模板完全一致
if(q[mid] >= x) r = mid;
else l = mid + 1;
}
return l; // 最终l=r,就是第一个>=x的位置
}
int lengthOfLIS(vector<int>& nums)
{
if(nums.empty()) return 0;
vector<int> tails; // 维护最长递增子序列的最小末尾数组
for(int num : nums)
{
// 情况1:当前数比tails最后一个元素大,直接追加(延长子序列)
if(tails.empty() || num > tails.back())
{
tails.push_back(num);
}
else// 情况2:用二分找第一个>=num的位置,替换它(贪心优化)
{
int pos = bSearch(tails, 0, tails.size()-1, num);
tails[pos] = num;
}
}
return tails.size(); // tails的长度就是最长递增子序列的长度
}
};
相似题目
递增的三元子序列中等
最长数对链中等
最长理想子序列中等
一步一步的实操演示 + 通俗解释 ,把「贪心 + 二分」的每一个操作落地到具体例子上,让你清楚看到这些方法是怎么 "用起来" 的 ------ 以示例 nums = [10,9,2,5,3,7,101,18] 为例,全程还原解题过程:
第一步:初始化核心数组
先创建空数组 tails,它的作用是「记录不同长度递增子序列的最小末尾」,初始时 tails = []。
第二步:遍历数组,逐个处理每个数
我们按顺序处理 nums 中的每个数,每一步都严格执行「贪心 + 二分」规则:
处理第 1 个数:10
- 当前
tails为空 → 直接追加; - 操作:
tails.push_back(10)→tails = [10]; - 含义:长度为 1 的最长递增子序列,最小末尾是 10。
处理第 2 个数:9
- 规则判断:9 ≤ tails 最后一个元素(10)→ 触发二分;
- 二分操作(找 tails 中第一个≥9 的位置):
- 二分区间
l=0, r=0(tails 长度 1)→ 直接返回 pos=0;
- 二分区间
- 替换操作:
tails[0] = 9→tails = [9]; - 含义:长度为 1 的最长递增子序列,最小末尾更新为 9(比 10 更小,后续更容易接数)。
处理第 3 个数:2
- 规则判断:2 ≤ tails 最后一个元素(9)→ 触发二分;
- 二分操作(找 tails 中第一个≥2 的位置):
- 二分区间
l=0, r=0→ 返回 pos=0;
- 二分区间
- 替换操作:
tails[0] = 2→tails = [2]; - 含义:长度为 1 的最长递增子序列,最小末尾更新为 2(最优)。
处理第 4 个数:5
- 规则判断:5 > tails 最后一个元素(2)→ 直接追加;
- 操作:
tails.push_back(5)→tails = [2,5]; - 含义:
- 长度 1 的最小末尾:2;
- 长度 2 的最小末尾:5。
处理第 5 个数:3
- 规则判断:3 ≤ tails 最后一个元素(5)→ 触发二分;
- 二分操作(找 tails 中第一个≥3 的位置):
- 二分区间
l=0, r=1:- mid=(0+1)>>1=0 → tails[0]=2 < 3 → l=1;
- 循环结束,返回 pos=1;
- 二分区间
- 替换操作:
tails[1] = 3→tails = [2,3]; - 含义:长度 2 的最长递增子序列,最小末尾更新为 3(比 5 更小,后续更容易接数)。
处理第 6 个数:7
- 规则判断:7 > tails 最后一个元素(3)→ 直接追加;
- 操作:
tails.push_back(7)→tails = [2,3,7]; - 含义:
- 长度 1:2;长度 2:3;长度 3:7。
处理第 7 个数:101
- 规则判断:101 > tails 最后一个元素(7)→ 直接追加;
- 操作:
tails.push_back(101)→tails = [2,3,7,101]; - 含义:长度 4 的最长递增子序列,最小末尾是 101。
处理第 8 个数:18
- 规则判断:18 ≤ tails 最后一个元素(101)→ 触发二分;
- 二分操作(找 tails 中第一个≥18 的位置):
- 二分区间 l=0, r=3:
- mid=(0+3)>>1=1 → tails[1]=3 < 18 → l=2;
- mid=(2+3)>>1=2 → tails[2]=7 < 18 → l=3;
- 循环结束,返回 pos=3;
- 二分区间 l=0, r=3:
- 替换操作:
tails[3] = 18→tails = [2,3,7,18]; - 含义:长度 4 的最长递增子序列,最小末尾更新为 18(比 101 更优)。
第三步:最终结果
遍历结束后,tails 的长度是 4 → 这就是最长递增子序列的长度(和示例答案一致)。
关键方法的落地总结
| 方法 | 具体怎么用 |
|---|---|
| 贪心策略 | 用 tails 记录「不同长度子序列的最小末尾」,优先让末尾更小(为后续延长留空间) |
| 追加规则 | 当前数 > tails 最后一个元素 → 直接 push_back,延长子序列长度 |
| 二分模板 | 当前数 ≤ tails 最后一个元素 → 调用模板找「第一个≥当前数的位置」,替换该位置的值 |
| 结果输出 | 最终 tails 的长度 = 最长递增子序列的长度 |
核心疑问解答(为什么这样做能得到正确长度?)
你可能会问:tails 里的元素不是真正的最长子序列(比如最终 tails 是 [2,3,7,18],而实际最长子序列是 [2,3,7,101]),为什么长度是对的?
→ 因为我们的目标是「求长度」,不是「找具体子序列」。tails 的核心作用是记录 "能达到的最长长度",替换操作只是优化末尾值,不影响长度的统计 ------ 只要能追加元素,就说明长度能 + 1,这就够了。