一、二分搜索
顾名思义,二分搜索就是将1个区间分割成2个区间,再对这2个区间进行计算。常见的二分搜索区间是左闭右开的形式,nums[0:length)
1.1、算法框架
int binarySearch(vector<int> nums, int target) {
int left = 0, right = nums.size();
while(left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return -1;
}
1.2、细节:
1.2.1、终止条件:while(left < right)
while(left < right)循环终止时,left==right
因为是左闭右开的区间:
int left = 0, right = nums.size();
nums[ 0:nums.size() )
如果,采用左闭右闭的区间,那么就要修改成
int left = 0, right = nums.size() - 1;
nums[0:nums.size() - 1]
1.2.2、mid的求取:mid = left + (right - left) / 2
计算 mid 时需要防止溢出,代码中left + (right - left) / 2就和(left + right) / 2的结果相同,但是前者有效防止了left和right太大直接相加导致溢出。
1.2.3、区间的分割:left = mid + 1;right = mid;
因为算法中使用左闭右开的区间。
如果,采用左闭右闭的区间,那么就要修改成
left = mid + 1;right = mid - 1;
1.3、上述二分查找的局限
比如说给你有序数组nums = [1,2,2,2,3],target为 2,此算法返回的索引是 2,没错。但是如果我想得到target的左侧边界,即索引 1,或者我想得到target的右侧边界,即索引 3,这样的话此算法是无法处理的。
这样的需求很常见,你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。
1.4、寻找左侧边界的二分搜索
int left_bound(vector<int> nums, int target) {
if (nums.length == 0) return -1;
int left = 0;
int right = nums.length(); // 注意
while (left < right) { // 注意
int mid = left + (right - left) / 2;// 注意
if (nums[mid] == target) {
right = mid;// 新的细节
} else if (nums[mid] < target) {
left = mid + 1;// 注意
} else if (nums[mid] > target) {
right = mid; // 注意
}
}
return nums[left] == target ? left : -1;// 新的细节
}
1.4.1、如何寻找左侧边界
if (nums[mid] == target) {
right = mid;// 新的细节
- 找到 target 时不要立即返回,而是缩小「搜索区间」的上界right,在区间[left, mid)中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
左侧边界的特殊含义:
比如对于有序数组nums = [2,3,5,7],target = 1,算法会得到 left = 0,含义是:nums中小于 1 的元素有 0 个。
再比如说nums = [2,3,5,7], target = 8,算法会得到 left = 4,含义是:nums中小于 8 的元素有 4 个。
再比如说nums = [2,3,3,7], target = 3,算法会得到 left = 1,含义是:nums中小于 3 的元素有 1 个。
返回值:return nums[left] == target ? left : -1;
1.5、寻找右侧边界的二分搜索
类似寻找左侧边界的算法,这里也会提供两种写法,还是先写常见的左闭右开的写法,只有两处和搜索左侧边界不同:
int right_bound(vector<int> nums, int target) {
if (nums.length == 0) return -1;
int left = 0, right = nums.length();
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
left = mid + 1; // 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return nums[left-1] == target ? (left-1) : -1;//注意
}
1.5.1、如何寻找右侧边界
if (nums[mid] == target) {
left = mid + 1; // 注意
当nums[mid] == target时,不要立即返回,而是增大「搜索区间」的下界left,使得区间不断向右收缩,达到锁定右侧边界的目的。
为什么最后返回left-1??
return left - 1; // 注意
- 循环终止时,left==right
- 最后1个判断
if (nums[mid] == target)
为真后,left = mid + 1;
,然后left==right
终止循环,可见最后一个target位于mid、即mid = left - 1
二、4. 寻找两个有序数组的中位数
给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。
请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
示例 1:
nums1 = [1, 3] nums2 = [2]
则中位数是 2.0
示例 2:
nums1 = [1, 2] nums2 = [3, 4]
则中位数是 (2 + 3)/2 = 2.5
2.1、解法1:合并数组再输出中位数
将2个有序数组合并成1个,合并的同时排序,最后再输出中位数即可。
但是,时间复杂度为O(n+m)
2.2、解法2:二分法查找中位数
总体思路
对于给定的2个数组A和B:
首先,让我们在任一位置 i 将 A 划分成两个部分:
-
len(left_A)=i, len(right_A)=m−i
left_A | right_A
A[0], A[1], …, A[i-1] | A[i], A[i+1], …, A[m-1]
由于 A 中有 m 个元素, 所以我们有 m+1 种划分的方法(i =0∼m)。
- 注意:当 i=0 时,left_A 为空集, 而当 i=m 时, right_A 为空集。
采用同样的方式,我们在任一位置 j 将 B 划分成两个部分:
left_B | right_B
B[0], B[1], ..., B[j-1] | B[j], B[j+1], ..., B[n-1]
- len(left_B)=j, len(right_B)=n−j
那么,将 left_A 和 left_B 放入一个集合,并将 right_A 和 right_B 放入另一个集合。 再把这两个新的集合分别命名为 left_part 和 right_part:
left_part | right_part
A[0], A[1], ..., A[i-1] | A[i], A[i+1], ..., A[m-1]
B[0], B[1], ..., B[j-1] | B[j], B[j+1], ..., B[n-1]
假设当前的划分位置 i 与 j 是最终解,那么必然成立:
len(left_part) = len(right_part) , m+n为偶数时
len(left_part + 1)=len(right_part) , m+n为奇数时
max(left_part) <= min(right_part)
以上也是循环结束的条件!
二分查找的循环步骤:
目标:寻找合适的 i 和 j ,i=0…m,j=0…n
因为条件:
len(left_part) = len(right_part) , m+n为偶数时
len(left_part + 1)=len(right_part) , m+n为奇数时
等价于:
i + j = m − i + n − j(或 i + j = m - i + n - j + 1, m+n为奇数时)
等价于:
j = (m + n + 1) / 2 - i
所以,只要找 i ,就能计算得到 j
-
1、初始条件:设 imin=0,imax=m, 然后开始在[imin,imax] 中进行搜索合适的 i
-
2、二分区间:令 i = (imin + imax) / 2 , j = (m + n + 1) / 2 - i,由此保证 len(left_part) == len(right_part)
-
3、每次循环我们会遇到三种情况:
-
B[j−1] <= A[i] && A[i−1] <= B[j]:标志我们找到了 i 目标,停止循环
-
B[j−1] > A[i]:这意味着 A[i] 太小,我们必须调整 i 以使 B[j−1]≤A[i]。
我们可以增大 i 吗?
是的,因为当 i 被增大的时候,j 就会被减小。
因此 B[j−1] 会减小,而 A[i] 会增大,那么B[j−1]≤A[i] 就可能被满足。
我们可以减小 i 吗?
不行,因为当 i 被减小的时候,j 就会被增大。
因此 B[j−1] 会增大,而 A[i] 会减小,那么 B[j−1]≤A[i] 就可能不满足。
所以我们必须增大 i。也就是说,我们必须将搜索范围调整为 [i+1,imax]。
因此,设 imin=i+1,并转到步骤 2。 -
A[i−1] > B[j]:这意味着 A[i−1] 太大,我们必须减小 i 以使 A[i−1]≤B[j]。
也就是说,我们必须将搜索范围调整为[imin,i−1]。
-
所求的中位数在上面的集合中,可以表示成:
median = ( max(left_part)+min(right_part) ) / 2 , 当m+n为偶数时
median = max(left_part) , 当m+n为基数时
以上也是寻找终止时,return的内容!
细节剖析
循环终止的条件
len(left_part) = len(right_part) , m+n为偶数时
len(left_part + 1)=len(right_part) , m+n为奇数时
max(left_part) <= min(right_part)
循环终止条件,也可以表示成:
i + j = m − i + n − j(或 i + j = m - i + n - j + 1, m+n为奇数时)
B[j − 1] <= A[i] && A[i − 1] <= B[j]
- i 的含义:A数组左半部分的长度,j 同理。
所以,我们需要做的就是,在区间 [0, m]中,找到 i 使得:
B[j − 1] <= A[i] && A[i − 1] <= B[j]
,其中 j = (m + n + 1) / 2 - i(不论m+n奇偶)
- 为了 j = (m + n + 1) / 2 - i 不出现负数!!需要增加条件 m <= n, 即要求数组A比数组B短
边界情况
i = 0时,len(left_A)=0, len(right_A)=m
那么 A[i−1] 不存在,我们就不需要检查 A[i−1]≤B[j] 是否成立。
j = n时,同理。
if(i == 0 || j ==n || A[i - 1] <= B[j]){ }
left_part | right_part
----------------------- | A[i], A[i+1], ..., A[m-1]
B[0], B[1], ..., B[j-1] | B[j], B[j+1], ..., B[n-1]
i = m时,len(left_A)=m, len(right_A)=0
那么 A[i] 不存在,我们就不需要检查 B[j−1]≤A[i] 是否成立。
j = 0时,同理。
left_part | right_part
A[0], A[1], ..., A[i-1] | -------------------------
B[0], B[1], ..., B[j-1] | B[j], B[j+1], ..., B[n-1]
if(j == 0 || i ==m || B[j - 1] <= A[i]){ }
如何对区间进行划分
1、设 imin=0,imax=m, 然后开始在[imin,imax] 中进行搜索合适的 i
2、令 i = (imin + imax) / 2 , j = (m + n + 1) / 2 - i,由此保证 len(left_part) == len(right_part)
- 3、每次循环我们会遇到三种情况:
-
(j == 0 || i ==m || B[j - 1] <= A[i]) && (i == 0 || j ==n || A[i - 1] <= B[j])
:标志我们找到了 i 目标,停止循环 -
(j > 0) && (i < m) && (B[j−1] > A[i])
:这意味着 A[i] 太小,我们必须调整 i 以使 B[j−1]≤A[i]。
我们可以增大 i 吗?
是的,因为当 i 被增大的时候,j 就会被减小,即(j > 0) && (i < m)
的由来。
因此 B[j−1] 会减小,而 A[i] 会增大,那么B[j−1]≤A[i] 就可能被满足。
我们可以减小 i 吗?
不行,因为当 i 被减小的时候,j 就会被增大。
因此 B[j−1] 会增大,而 A[i] 会减小,那么 B[j−1]≤A[i] 就可能不满足。
所以我们必须增大 i。也就是说,我们必须将搜索范围调整为 [i+1,imax]。
因此,设 imin=i+1,并转到步骤 2。 -
(i > 0) && (j < n) && (A[i−1] > B[j])
:这意味着 A[i−1] 太大,我们必须减小 i 以使 A[i−1]≤B[j]。
也就是说,我们必须将搜索范围调整为[imin,i−1]。
-
C++程序
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
// 为确保j不会出现负数,要让A.size()<B.size()
if (nums1.size() > nums2.size()) return binarySearch(nums2, nums1);
return binarySearch(nums1, nums2);
}
private:
double binarySearch(vector<int>& A, vector<int>& B){
int m = A.size(), n = B.size(), i, j;
// 为什么循环终止条件是<=而不是<,因为[imin, imax]是左闭右闭区间
int imin = 0, imax = m;
// while循环中,寻找的是i的值,明确这一点,才能明白判断条件的含义
while (imin <= imax){
// 求取mid二分线
i =imin + (imax - imin) / 2 ;
// i与j存在关系事如下,保证 len(left_part) == len(right_part)
j = (m + n + 1) / 2 - i;
// 情况1:找到目标i
if((j == 0 || i == m || B[j - 1] <= A[i]) && (i == 0 || j == n || A[i - 1] <= B[j])){
int leftmax, rightmin;
// 处理左半部分的数据,求取leftmax
if (i == 0) {
leftmax = B[j - 1];
} else if (j == 0) {
leftmax = A[i - 1];
} else {
leftmax = max(A[i - 1], B[j - 1]);
}
// m + n是奇数
if((m + n) & 1){
cout << i << j << endl;
return leftmax;
// m + n是偶数
} else {
// 处理右半部分的数据,求取rightmin
if (i == m) rightmin = B[j];
else if (j == n) rightmin = A[i];
else rightmin = min(B[j], A[i]);
return 0.5 * (leftmax + rightmin);
}
} else if (i < imax && B[j-1] > A[i]){
// 情况2:A[i] 太小,我们必须增大 i 以使 B[j−1] <= A[i]
imin = i + 1;
} else if (i > imin && A[i-1] > B[j]){
// 情况3:A[i−1] 太大,我们必须减小 i 以使 A[i−1] <= B[j]
imax = i - 1;
}
}
cout << "while end!" << endl;
return 0.0;
}
};