3.15-3.16-二分
35. 搜索插入位置 - 力扣(LeetCode)
模板题,令l = -1 , r = nums.size()可避免讨论,最后返回的是r
class Solution { public: int searchInsert(vector<int>& nums, int target) { int l = -1 , r = nums.size(); while(l != r - 1){ int mid = l + ((r - l) >> 1); if(nums[mid] < target) l = mid; else if(nums[mid] > target) r = mid; else return mid; } return r; } };
74. 搜索二维矩阵 - 力扣(LeetCode)
对行、列两次二分即可。
第一次二分查找行,得到行号d,此时进行讨论:
- d==m 返回false
- d<m 再二分列号,得到坐标
matrix[d][r]与target比较即可class Solution { public: bool searchMatrix(vector<vector<int>>& matrix, int target) { int m = matrix.size(); int n = matrix[0].size(); int u = -1 , d = m; while(u + 1 != d){ int mid = u + ((d - u) >> 1); if(matrix[mid][n - 1] < target) u = mid; else if(matrix[mid][n - 1] > target) d = mid; else return true; } if(d == m) return false; else{ int l = -1 , r = n; while(l + 1 != r){ int mid = l + ((r - l) >> 1); if(matrix[d][mid] < target) l = mid; else if(matrix[d][mid] > target) r = mid; else return true; } return matrix[d][r] == target; } } };
灵神方法:
方法一:二分查找
由于矩阵的每一行是递增的,且每行的第一个数大于前一行的最后一个数,如果把矩阵每一行拼在一起,我们可以得到一个递增数组。
例如示例 1,三行拼在一起得
a=[1,3,5,7,10,11,16,20,23,30,34,60]
由于这是一个有序数组,我们可以用二分查找判断 target 是否在 matrix 中。代码实现时,并不需要真的拼成一个长为 mn 的数组 a,而是将 a[i] 转换成矩阵中的行号和列号。例如示例 1,i=9 对应的 a[i]=30,由于矩阵有 n=4 列,所以 a[i] 在 ⌊i/n⌋=2 行,在 i mod n=1 列。
一般地,有
a[i]=matrix[⌊i/n⌋][imodn]class Solution { public: bool searchMatrix(vector<vector<int>>& matrix, int target) { int m = matrix.size(), n = matrix[0].size(); int left = -1, right = m * n; while (left + 1 < right) { int mid = left + (right - left) / 2; int x = matrix[mid / n][mid % n]; if (x == target) { return true; } (x < target ? left : right) = mid; } return false; } };复杂度分析
- 时间复杂度:O(log(mn)),其中 m 和 n 分别为 matrix 的行数和列数。
- 空间复杂度:O(1)。
方法二:排除法
该方法也适用于 240. 搜索二维矩阵 II。
class Solution { public: bool searchMatrix(vector<vector<int>>& matrix, int target) { int m = matrix.size(), n = matrix[0].size(); int i = 0, j = n - 1; while (i < m && j >= 0) { // 还有剩余元素 if (matrix[i][j] == target) { return true; // 找到 target } if (matrix[i][j] < target) { i++; // 这一行剩余元素全部小于 target,排除 } else { j--; // 这一列剩余元素全部大于 target,排除 } } return false; } };复杂度分析
- 时间复杂度:O(m+n),其中 m 和 n 分别为 matrix 的行数和列数。
- 空间复杂度:O(1)。
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
代码细节说明:
为什么L的初始值为-1,R的初始值为N?
首先,如果二分本来就没有结果。如对于 nums=
[1 2 2 3 3 4]target = 5,如果你要寻找第一个 >=5 的数,你会发现,整个过程都在执行L=mid,最后得到的结果中,R是等于下标6的,他明显这个时候是越界的,说明我们找不到要寻找的数字。而如果我们一开始将R赋值为n-1,也就是赋值为下标5的时候,他返回的R是5,是没有越界的,被我们当成了答案,但其实这时候我们的二分是没有答案的,就发生了错误;
其次,L最小值为-1,R最小值只能取到1,因为L+1!=R为循环结束条件,R最大值为N,同理则L的最大值为N-2,则(L+R)/2的取值范围是 [0,N) ,mid的值始终位于0到N的左闭右开区间里面,不会发生越界的错误;为什么循环结束的条件是while(L+1!=R)?
这边给出的循环条件是while(L+1!=R) 其实,就是当L和R相邻的时候,循环就结束,因为区间反回的是不重合的两区间,只有L=mid和R=mid这两种情况,最后根据需要返回L或者R;例如:
- 对于a[1],他的下标为0 此时L=-1,R=1
- 对于b[2],他的下标为0,1 此时L=-1,R=2
无论何种情况,初始的L+1始终小于R,历经循环后最终L和R相邻,不会出现一开始L就和R重合等情况导致出现while(L+1!=R)循环不能结束的情况。我们就能够通过二分得到不重合的两区间,而且只需要L=mid和R=mid,不需要考虑L=mid+1,R=mid-1的情况。
如何找第一个等于target/最后一个等于target的数?
关键在于改变等于号的位置:
- (nums[mid] < target ? l : r) = mid; return r = 第一个>=target(即lower_bound函数)
- (nums[mid] <= target ? l : r) = mid;return l = 最后一个<=target
int l = -1, r = n; while( l + 1 != r) { int mid = l + (r - l ) >> 1; //寻找第一个等于target的坐标 我这边让二分的边界定为左边为5 右边>= 则所求为r // (nums[mid] < target ? l : r) = mid; //此时循环结束后r为第一个>=target的数 //如果改为求最后一个等于的,改变等于号的位置即可 //(nums[mid] <= target ? l : r) = mid; //此时循环结束后l<=target , 而r > target,l为最后一个等于target的数 }
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> res(2 , -1);
if(nums.empty()) return res;
int n = nums.size();
int l = -1 , r = n;
//寻找第一个等于K的坐标 我这边让二分的边界定为 左边为<5 右边>=5 则所求为r第一个>=target的数
while(l + 1 != r){
int mid = l + (r - l) / 2;
( nums[mid] < target ? l : r) = mid;
}
//这里需要判断targrt>nums[n - 1] 和target不存在的情况
if(r == n || nums[r] != target) return res;
else{
res[0] = r;
int ll = -1 , rr = n;
while(ll + 1 != rr){
int mid = ll + (rr - ll) / 2;
(nums[mid ]<= target ? ll : rr) = mid;
}
res[1] = ll;
}
return res;
}
};
用库函数的写法:
auto it1 = lower_bound(v.begin(), v.end(), val,cmp1); auto it2 = upper_bound(v.begin(), v.end(), val,cmp2);
lower_bound upper_bound 无自定义比较函数 返回第一个 >= val 的元素 返回第一个 > val 的元素 使用自定义比较函数 返回 第一个 false 的元素 返回第一个 true 的元素
lower_bound/upper_bound返回的是迭代器it,如果要取值就*it,返回下标就返回it-v.begin()或distance(v.begin() , it)class Solution { public: vector<int> searchRange(vector<int>& nums, int target) { int start = ranges::lower_bound(nums , target) - nums.begin(); if(start == nums.size() || nums[start] != target) return { -1 , -1}; int end = ranges::upper_bound(nums,target) - nums.begin() - 1; return {start , end}; } };
153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
思路:
设
x=nums[mid]是现在二分取到的数。我们需要判断 x 和
数组最小值的位置关系,谁在左边,谁在右边?把 x 与最后一个数 nums[n−1] 比大小:
如果 x>nums[n−1],那么可以推出以下结论:
- nums 一定被分成左右两个递增段;
- 第一段的所有元素均大于第二段的所有元素;
- x 在第一段。
- 最小值在第二段。
- 所以 x 一定在最小值的左边。所以更新l
如果 x≤nums[n−1],那么 x 一定在第二段。(或者 nums 就是递增数组,此时只有一段。)
- x 要么是最小值,要么在最小值右边。所以更新r
所以,只需要比较 x 和 nums[n−1] 的大小关系,就间接地知道了 x 和数组最小值的位置关系,从而不断地缩小数组最小值所在位置的范围,二分找到数组最小值。
class Solution {
public:
int findMin(vector<int>& nums) {
int l = -1 , r = nums.size();
while(l + 1 < r){
int mid = l + (r - l) / 2;
(nums[mid] <= nums.back() ? r : l) = mid;
}
return nums[r];
}
};
33. 搜索旋转排序数组 - 力扣(LeetCode)
有了上一题的铺垫,可以找出最小值的位置,从而更新区间范围。
自己写的,方法同灵神:
方法一:两次二分
首先找到 nums 的最小值的下标 i。
然后分类讨论:
如果
target>nums[n−1],那么 target 一定在第一段[0,i−1]中,在 [0,i−1] 中二分查找 target。如果
target≤nums[n−1],那么:
- 如果 i=0,说明 nums 是递增的,直接在 [0,n−1] 中二分查找 target。
- 如果 i>0,那么 target 一定在第二段 [i,n−1] 中,在 [i,n−1] 中二分查找 target。
这两种情况可以合并成:在 [i,n−1] 中二分查找 target。
class Solution {
private:
// 153. 寻找旋转排序数组中的最小值(返回的是下标)
int findMin(vector<int>& nums) {
int l = -1 , r = nums.size();
while(l + 1 < r){
int mid = l + (r - l) / 2;
(nums[mid] <= nums.back() ? r : l) = mid;
}
return r;
}
// 有序数组中找 target 的下标
int lower_bound(vector<int>& nums , int ll , int rr , int target){
int l = ll - 1 , r = rr + 1;
while(l + 1 != r){
int mid = l + (r - l) / 2;
(nums[mid] < target ? l : r) = mid;
}
return nums[r] == target ? r : -1;
}
public:
int search(vector<int>& nums, int target) {
int min = findMin(nums);
if(target > nums.back()){//在第一段
return lower_bound(nums , 0 , min - 1 , target);
}
else{//在第二段
return lower_bound(nums , i , nums.size() - 1 , target);
}
}
};
复杂度分析
- 时间复杂度:O(logn),其中 n 为 nums 的长度。
- 空间复杂度:O(1),仅用到若干额外变量。
方法二:一次二分
设 x=nums[mid] 是我们现在二分取到的数。
现在需要判断 x 和 target 的位置关系,谁在左边,谁在右边?
check()函数定义为 x 在 target 右边为true
if(check(x)) r = x; else l = x;核心思路
如果 x 和 target 在不同的递增段:
- 如果 target 在第一段(左),x 在第二段(右),说明 x 在 target 右边;
- 如果 target 在第二段(右),x 在第一段(左),说明 x 在 target 左边。
如果 x 和 target 在相同的递增段:
- 比较 x 和 target 的大小即可。
分类讨论
下面只讨论 x 在 target 右边,或者等于 target 的情况(x >= target)。其余情况 x 一定在 target 左边。
if(check(x)) r = x; else l = x;1.如果
x>nums[n−1],说明 x 在第一段中,那么 target 也必须在第一段中(target > nums[n - 1])(否则 x 一定在 target 左边)且x 必须大于等于 target。
- 写成代码就是
target > nums[n - 1] && x >= target。2.如果
x≤nums[n−1],说明 x 在第二段中(或者 nums 只有一段),那么 target 可以在第一段,也可以在第二段。
- 如果 target 在第一段,则
target > nums[n - 1]。- 如果 target 在第二段,那么
x 必须大于等于 target。- 写成代码就是
target > nums[n - 1] || x >= target。根据这两种情况,去判断 x 和 target 的位置关系,从而不断地缩小 target 所在位置的范围,二分找到 target。
细节
下面代码用的开区间二分,用其他二分写法也是可以的。
二分的范围可以是 (−1,n−1),也就是闭区间 [0,n−2]。
这是因为,如果 target 在 nums 中的位置是 n−1(x 不可能在 target 右边),那么上面分类讨论中的代码,计算结果均为 false。这意味着每次二分更新的都是 left,那么循环结束后,答案自然就是 n−1 了。
class Solution {
public:
int search(vector<int>& nums, int target) {
int end = nums.back();
auto check = [&](int i) -> bool {
int x = nums[i];
if (x > end) {
return target > end && x >= target;
}
return target > end || x >= target;
};
int left = -1, right = nums.size() - 1; // 开区间 (-1, n-1)
while (left + 1 < right) { // 开区间不为空
int mid = left + (right - left) / 2;
if (check(mid)) {
right = mid;
} else {
left = mid;
}
}
return nums[right] == target ? right : -1;
}
};
复杂度分析
- 时间复杂度:O(logn),其中 n 为 nums 的长度。
- 空间复杂度:O(1),仅用到若干额外变量。
4. 寻找两个正序数组的中位数 - 力扣(LeetCode)
前言
本质上,我们需要在两个有序数组中,查找第 k 小的数,其中
k=⌈(m+n)/2⌉。
- 如果 m+n 是奇数,返回第 k 小的数。
- 如果 m+n 是偶数,返回第 k 小的数和第 k+1 小的数的平均值。
本文先从最暴力的排序做法开始,然后讲解双指针做法,最后过渡到二分做法。
一、引入:均匀分组
这里的关键是「均匀分组」,每组 5 个数,只要第一组的最大值 ≤ 第二组的最小值,我们就找到了答案。
怎么想到要均匀分组的?请看百科中关于中位数的介绍:
中位数……可将数值集合划分为大小相等的两部分。
二、枚举:双指针做法
下面来说具体做法。
设 a 的 b 的长度分别为 m 和 n,且 m≤n(如果不满足则交换两个数组)。
- 为方便处理 i=−1,即 a 有 0 个数在第一组的情况,我们可以往 a 的最左边插入一个哨兵 −∞,这可以保证数组仍然是有序的。对于 j=−1 的情况也同理,往b的最左边插入一个−∞。
- 为方便处理 i+1=m,即 a 有 m 个数在第一组的情况,我们可以往 a 的最右边插入一个哨兵 ∞,这可以保证数组仍然是有序的。对于 j+1=n 的情况也同理,往 b 的最右边插入一个 ∞。这可以避免 \(a_{i+1}\) 和 \(b_ {j+1}\)下标越界。
- 插入 −∞ 和 ∞ 后,便可保证无论 a 和 b 是什么样的,一定存在一个 i,满足 \(a_i≤b_ {j+1}\)且 \(a_{i+1}>=b_j\) 。
- m 和 n 的值不变。
如此修改后,i 的含义变成了 a 有 i 个数在第一组,j 的含义变成了 b 有 j 个数在第一组。
初始化 i=0,那么 j 应该初始化成多少?
- 如果 m+n 是偶数,那么每组的大小为 \(\frac{m+n}{2}\),j 应当初始化成\(\frac{m+n}{2}\)
- 。
- 如果 m+n 是奇数,我们规定第一组比第二组多一个数,第一组的大小为 \(\frac{m+n + 1}{2}\) ,j 应当初始化成 \(\frac{m+n + 1}{2}\)。
两种情况可以合并为:j 初始化成\(⌊\frac{m+n + 1}{2}⌋\)
为了保证组的大小不变,i 每增加 1,j 就要减少 1。
根据图片中的结论,只要发现 \(a_i≤b_{ j+1}\)且 \(a_{i+1}>b_j\) ,那么:
- 如果 m+n 是偶数,中位数为 max(ai ,bj) 和 min(a_i+1 ,b_j+1) 的平均值。
- 如果 m+n 是奇数,中位数为 max(ai,bj)。
答疑
问:为什么图中说存在一个位置,满足 \(a_i ≤b_{j+1}\)且$ a_{i+1}>b_j$?
答:根据 i 和 j 的关系,i 变大,j 会随着变小。把 b 反转,变成一个递减数组,这样 j 会随着 i 的变大而变大,我们可以更容易地观察出性质。把这两个数组画成折线图,一个递增另一个递减,并且由于我们插入了 −∞ 和 ∞,所以二者必然相交。这说明存在一个位置,满足 \(a_i ≤b_{j+1}\)且$ a_{i+1}>b_j$。
问:保证 m≤n 有什么好处?
答:如果 m>n,我们没法从 i=0 开始枚举。以 m=5,n=3 为例,i=0 时,b 数组需要有 4 个数在第一组,但 n=3<4,无法做到。保证 m≤n 可以让我们从 i=0 开始枚举,写起来更方便。
问:如果数组中存在重复元素,上述做法是否正确?
答:仍然是正确的,因为只用到了「a 和 b 是有序数组」的条件。
class Solution { public: double findMedianSortedArrays(vector<int>& a, vector<int>& b) { if (a.size() > b.size()) { swap(a, b); // 保证下面的 i 可以从 0 开始枚举 } int m = a.size(), n = b.size(); a.insert(a.begin(), INT_MIN); // 最左边插入 -∞ b.insert(b.begin(), INT_MIN); a.push_back(INT_MAX); // 最右边插入 ∞ b.push_back(INT_MAX); // 枚举 nums1 有 i 个数在第一组 // 那么 nums2 有 (m + n + 1) / 2 - i 个数在第一组 int i = 0, j = (m + n + 1) / 2; while (true) { if (a[i] <= b[j + 1] && a[i + 1] > b[j]) { // 写 >= 也可以 int max1 = max(a[i], b[j]); // 第一组的最大值 int min2 = min(a[i + 1], b[j + 1]); // 第二组的最小值 return (m + n) % 2 ? max1 : (max1 + min2) / 2.0; } i++; // 继续枚举 j--; } } };复杂度分析
- 时间复杂度:O(m+n),其中 m 是 a 的长度,n 是 b 的长度。往 a 前面插入一个元素的时间复杂度是 O(m),往 b 前面插入一个元素的时间复杂度是 O(n),加起来是 O(m+n)。
- 空间复杂度:O(m+n)。
三、优化:二分做法
由于 a 和 b 是有序数组,i 越小,\(a_i ≤b_{j+1}\) 越能成立;i 越大,\(a_i ≤b_{j+1}\)
越不能成立。所以可以二分最大的满足 \(a_i ≤b_{j+1}\) 的 i。二分结束后,我们有 a
\(a_i ≤b_{j+1}\)且 \(a _{i+1}>b_j\)。最后,讨论二分的上下界。本文用开区间二分,其他二分写法也是可以的。
- 开区间二分左边界:0。在插入 −∞ 后, \(a_i ≤b_{j+1}\)在 i=0 时一定成立。
- 开区间二分右边界:m+1。在插入 ∞ 后, \(a_i ≤b_{j+1}\) 在 i=m+1 时一定不成立。
答疑
问:能否二分红色折线图的最小值?
答:这种做法会在有重复元素时失效。试想一下,如果我们在折线图上二分,碰巧遇到了相邻且相同的元素,你要更新 left 还是更新 right 呢?
写法一
注意在数组前面插入元素的时间复杂度是线性的,所以和上面的复杂度分析一样,都是 O(n+m)。
真正满足题目时间复杂度要求的是后面的写法二。
class Solution { public: double findMedianSortedArrays(vector<int>& a, vector<int>& b) { if (a.size() > b.size()) { swap(a, b); // 保证下面的 i 可以从 0 开始枚举 } int m = a.size(), n = b.size(); a.insert(a.begin(), INT_MIN); b.insert(b.begin(), INT_MIN); a.push_back(INT_MAX); b.push_back(INT_MAX); // 循环不变量:a[left] <= b[j+1] // 循环不变量:a[right] > b[j+1] int left = 0, right = m + 1; while (left + 1 < right) { // 开区间 (left, right) 不为空 int i = (left + right) / 2; int j = (m + n + 1) / 2 - i; if (a[i] <= b[j + 1]) { left = i; // 缩小二分区间为 (i, right) } else { right = i; // 缩小二分区间为 (left, i) } } // 此时 left 等于 right-1 // a[left] <= b[j+1] 且 a[right] > b[j'+1] = b[j],所以答案是 i=left int i = left; int j = (m + n + 1) / 2 - i; int max1 = max(a[i], b[j]); int min2 = min(a[i + 1], b[j + 1]); return (m + n) % 2 ? max1 : (max1 + min2) / 2.0; } };写法二
去掉插入的 −∞ 和 ∞,所有下标都减一。
开区间二分的左右边界改成 −1 和 m。
i=−1 时,j 的值为 ⌊(m+n+1)/2⌋−1。
i=0 时,j 的值为 ⌊(m+n+1)/2⌋−2。
一般地,j 和 i 的关系为 j = ⌊(m+n+1)/2⌋−2−i = ⌊(m+n-3)/2⌋−i。
答疑
问:当 m=0 时,是否会算出 i=0?
答:不会,m=0 不会进入二分循环,i=left=−1。
class Solution { public: double findMedianSortedArrays(vector<int>& a, vector<int>& b) { if (a.size() > b.size()) { swap(a, b); } int m = a.size(), n = b.size(); // 循环不变量:a[left] <= b[j+1] // 循环不变量:a[right] > b[j+1] int left = -1, right = m; while (left + 1 < right) { // 开区间 (left, right) 不为空 int i = (left + right) / 2; int j = (m + n + 1) / 2 - 2 - i; if (a[i] <= b[j + 1]) { left = i; // 缩小二分区间为 (i, right) } else { right = i; // 缩小二分区间为 (left, i) } } // 此时 left 等于 right-1 // a[left] <= b[j+1] 且 a[right] > b[j'+1] = b[j],所以答案是 i=left int i = left; int j = (m + n + 1) / 2 - 2 - i; int ai = i >= 0 ? a[i] : INT_MIN; int bj = j >= 0 ? b[j] : INT_MIN; int ai1 = i + 1 < m ? a[i + 1] : INT_MAX; int bj1 = j + 1 < n ? b[j + 1] : INT_MAX; int max1 = max(ai, bj); int min2 = min(ai1, bj1); return (m + n) % 2 ? max1 : (max1 + min2) / 2.0; } };复杂度分析
- 时间复杂度:O(logmin(m,n)),其中 m 是 a 的长度,n 是 b 的长度。注:这个复杂度比题目所要求的 O(log(m+n)) 更优。
- 空间复杂度:O(1)。





浙公网安备 33010602011771号