二分法主要是利用折半查找的思想,对区间进行查找,使O(n)复杂度的时间下降为O(log n),使用时要特别注意边界的开闭情况。
练习二分查找写法
寻找第一个大于等于x的位置
//arr[]为递增序列,返回第一个大于等于x的位置
//传入初值[0,n],用n是考虑当x大于arr[]所有元素,它应该在n位置处
int lower_bound(int arr[], int left, int right, int x){
while(left < right){
int mid = (left + right)/2;
//若上界超过int一半,则 mid = left + (right - left)/2
if(arr[mid] >= x){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
寻找第一个大于x的位置,与上面类似,只是arr[mid] >= x变成arr[mid] > x.
//arr[]为递增序列,返回第一个大于x的位置
//传入初值[0,n],用n是考虑当x大于arr[]所有元素,它应该在n位置处
int upper_bound(int arr[], int left, int right, int x){
while(left < right){
int mid = (left + right)/2;
//若上界超过int一半,则 mid = left + (right - left)/2
if(arr[mid] > x){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
当然,也可以直接使用lower_bound函数和upper_bounde函数。
lower_bound() 函数定义在<algorithm>
头文件中,注意其区间是左闭右开型的。其语法格式有 2 种,分别为:
//在 [first, last) 区域内查找不小于 val 的元素 ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val); //在 [first, last) 区域内查找第一个不符合 comp 规则的元素 ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);
其中,first 和 last 都为正向迭代器,[first, last) 用于指定函数的作用范围;val 用于指定目标元素;comp 用于自定义比较规则,此参数可以接收一个包含 2 个形参(第二个形参值始终为 val)且返回值为 bool 类型的函数,可以是普通函数,也可以是函数对象。
同时,该函数会返回一个正向迭代器,当查找成功时,迭代器指向找到的元素;反之,如果查找失败,迭代器的指向和 last 迭代器相同。可以用迭代器iter接受,再用*iter取或修改其值。
1 Sqrt(x) (69)
不用sqrt函数确定X开方后向下取整的结果。采用二分查找,实质上是找使a*a<=x成立的最大整数a。
class Solution {
public:
int mySqrt(int x) {
int low = 0,high = x,ans=-1;
//if(x==1) return x;
while(low<=high){
int mid = low + (high-low)/2;
//long product = mid*mid;
if((long)mid*mid <= x) {
low = mid+1;
ans = mid;
}
else high =mid-1;
}
return ans;
}
};
这里解释下为什么边界的取值情况:
(1)当mid*mid>x时,mid一定不是结果,所以可以直接把high = mid -1
(2)当mid*mid<=x时,low=mid+1,是为了避免落入high = low+1时可能出现的死循环,如x=4,如果low=mid,经过运算后,会出现low = 2, high =3的死循环情况。还要注意,开始我把向上取整写在 mid = low +(high - low+1)/2的情况里,这样在high = int最大值+1时超出int界限,类似的也不能写成mid = (low+high)/2的形式,也会在low+high时超过int界限。
为了避免平方超过int界限,mid*mid转化为long。
Python版本:
class Solution:
def mySqrt(self, x: int) -> int:
low,high,ans = 0,x,-1
while low<=high:
mid = low +(mid-low)//2
if mid*mid<=x:
ans = mid
low = mid+1
else:
high = mid-1
return ans
方法二:使用牛顿迭代法
原问题等价于求f(x)=x^2 -a =0的根向下取整的值,由牛顿迭代法:xn+1 = xn - f(xn)/f'(xn) = x/2 +a/2x。由于f(x)是凸函数,所以从a开始迭代,得到xn始终大于真正零点,我们只需保证误差小于一定值就可认为找到该值,再向下取整即可。
class Solution {
public:
int mySqrt(int x) {
// x=0的情况特判
if(x==0) return 0;
double a=x,x0=x;
while(true){
double xi = (x0+a/x0)/2;
// xi和x0非常接近时,就可以确定其向下取整值
if(fabs(xi-x0)<1e-7) break;
x0 = xi;
}
return int(x0);
}
};
2、在排序数组中查找元素的第一个和最后一个位置(34)
思路很明显,使用二分法查找区间。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int low = 0,high = nums.size()-1;
while(low<=high){
int mid = (low+high)/2;
if(nums[mid]<target) low = mid + 1;
else if(nums[mid]>target) high = mid -1;
else{
low = mid-1,high=mid+1;
while(low>=0&&nums[mid]==nums[low]){
--low;
}
while(high<nums.size()&&nums[mid]==nums[high]){
++high;
}
return {low+1,high-1};
}
}
return {-1,-1};
}
};
最开始的代码,刚好检测到target时,向前和向后比较确定其区间,这样又变成了线性时间的操作,在有很多个target元素时会影响效率。
实际上,我们可以寻找第一个>=target值的位置low和第一个>target位置和high。如果这样的target在nums中存在,则区间就是[low, high-1]。若不存在,则对应low大于size和nums[low]值不等于target的情况。这样整个区间都是采用二分法进行缩小得到的,效率更快。
class Solution {
public:
// 寻找第一个大于等于x元素的位置
int lower_bound(vector<int>& nums,int target){
// 由于大于等于x的元素可能不存在,所以r取最后一个元素下一位
int l=0,r=nums.size();
while(l<r){
int mid=(l+r)/2;
if(nums[mid]<target){
l = mid+1;
}
else{
r = mid;
}
}
return l;
}
// 寻找第一个大于x元素的位置
int higher_bound(vector<int>& nums,int target){
int l=0,r=nums.size();
while(l<r){
int mid=(l+r)/2;
if(nums[mid]<=target){
l = mid+1;
}
else{
r = mid;
}
}
return l;
}
vector<int> searchRange(vector<int>& nums, int target) {
//空数组特判
if(nums.empty()) return{-1,-1};
// r-1是为了判断大于target的前一位是否等于target
int l = lower_bound(nums,target),r = higher_bound(nums,target)-1;
// 不存在大于等于target的元素,或有大于taregt但没有等于的情况
if(l>=nums.szie()||nums[l]!=target) return {-1,-1};
return {l,r};
}
};
python版
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
def lower_bound(nums: List[int], target: int) -> List[int]:
l ,r = 0,len(nums)
while l<r:
mid = (l+r)//2
if nums[mid]<target:
l = mid+1
else:
r = mid
return l
def higher_bound(nums: List[int], target: int) -> List[int]:
l ,r = 0,len(nums)
while l<r:
mid = (l+r)//2
if nums[mid]<=target:
l = mid+1
else:
r = mid
return l
low = lower_bound(nums,target)
high = higher_bound(nums,target)-1
if low>=len(nums) or nums[low]!=target:
return [-1,-1]
return [low,high]
3、 搜索旋转排序数组 II(81)
旋转后的有序数组,其数组元素大小走势有如下特点:
我们仍然可以利用这种大小关系进行判断:
(1)当N[mid]<N[r]时,则[mid,r]一段是有序的,进一步判断:
i)如果N[mid]<target<N[r],则target只可能在[mid,r]上,对该段二分查找即可,l = mid +1;
ii)如果target<N[mid]或target>N[r],则target只能在[l,mid]上,r = mid -1继续探查。
(2)当N[mid]>N[l]时,则[l,mid]一段是有序的,进一步判断:
i)如果N[l]<target<N[mid],则target只可能在[l,mid]上,对该段二分查找即可,r = mid -1;
ii)如果target<N[l]或target>N[mid],则target只能在[mid,r]上,l = mid +1继续探查。
(3)当N[r]<=N[mid]<=N[l]时,由于数组存在相同元素,不好判断哪段有序,如[1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1] 数组,target=2的情况。因此我们令l++,改变区间,继续进行判断。
当探查到l\r\mid中有一个值等于target,即找到了。
class Solution {
public:
bool search(vector<int>& nums, int target) {
int l = 0, r = nums.size()-1;
while(l<=r){
int mid = (l+r)/2;
if(nums[mid]==target||nums[l]==target||nums[r]==target) return true;
if(nums[mid]<nums[r]){// mid右边数组有序
if(nums[mid]<target&&target<nums[r]){
l = mid+1;
}else{
r = mid-1;
}
}
else if(nums[mid]>nums[l]){// mid左边数组有序
if(nums[l]<target&&target<nums[mid]){
r = mid-1;
}else{
l = mid+1;
}
}
else{//nums[r]<=nums[mid]<=nums[r]无法判断哪边有序,移动下l改变区间
++l;
}
}
return false;
}
};
4、 寻找旋转排序数组中的最小值 II(154)
和上一题类似,同样可以利用旋转数组的大小关系:
(1)当N[mid]<N[r]时,则[mid,r]一段是非递减的,最小元素可能还藏在[l,mid-1]中,对该段继续探查;
(2)当N[mid]>N[l]时,则[l,mid]一段是非递减的,最小元素可能还藏在[mid+1,l]中,对该段继续探查。
(3)当N[r]<=N[mid]<=N[l]时,l++继续探查。
class Solution {
public:
int findMin(vector<int>& nums) {
int l = 0, r = nums.size()-1,minNum = INT_MAX;
while(l<=r){
int mid = (l+r)/2;
minNum = min(minNum,nums[mid]);
if(nums[mid]<nums[r]){
r = mid - 1;
}
else if(nums[mid]>nums[l]){
# 也可以直接在前面minNum更新为mid,l,r三者中最小值
minNum = min(minNum,nums[l]);
l = mid + 1;
}
else{
minNum = min(minNum,nums[l]);
++l;
}
}
return minNum;
}
};
5、有序数组中的单一元素(540)
给的数组最大的特点是:只有一个元素出现1次,其余都出现两次。设其编号为0、1、2、、、2n,那么在独特值出现前,nums[2i] 和 nums[2i+1]的值一定相同;独特值出现后,nums[2i] 和 nums[2i+1]的值一定不同。因此我们可以用二分查找:
(1)当 mid%2 == 0时,mid和mid+1是一组2i和2i+1元素:
1)若nums[mid] == nums[mid+1],则独特值出现在mid右边,l=mid+2(由于有独特值的存在,肯定不会越界)
2)若nums[mid] != nums[mid+1],则独特值出现在mid左边,r=mid;
(2)当 mid%2 != 0时,mid和mid-1是一组2i和2i+1元素:
1)若nums[mid] == nums[mid-1],则独特值出现在mid右边,l=mid+1(由于有独特值的存在,肯定不会越界)
2)若nums[mid] != nums[mid-1],则独特值出现在mid左边,r=mid-1;
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
int l = 0, r = nums.size()-1;
if(r==0) return nums[r];
while(l<r){
int mid = (l+r)/2;
if(mid%2 == 0){
if(nums[mid] == nums[mid+1]) l=mid+2;
else r=mid;
}
else{
if(nums[mid] == nums[mid-1]) l=mid+1;
else r=mid-1;
}
}
return nums[l];
}
};
进一步优化算法:只对偶数下标索引,即确保mid也是偶数的,使mid和mid+1正好对应一组2i和2i+1。一旦nums[mid] == nums[mid+1]则l=mid+2,否则r=mid。即放弃Mid是奇数还是偶数的判断,一律判断偶数的下标,这样更加简洁。
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
int lo = 0;
int hi = nums.size() - 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (mid % 2 == 1) mid--; //mid-1使之变为偶数下标
if (nums[mid] == nums[mid + 1]) {
lo = mid + 2;
} else {
hi = mid;
}
}
return nums[lo];
}
};
6、寻找两个正序数组的中位数(4)
根据中位数定义,当m+n为奇数时,我们要寻找第(n+m)/2+1位数;当m+n为偶数时,我们要寻找第(m+n)/2和(n+m)/2+1位数,并求均值(编号:1、2、、、m+n)。因此,本题是要寻找两数组的第k小元素,k = (m+n)/2或(n+m)/2+1。
利用数组的有序性,先查找A[k/2-1]和B[k/2-1]:
(1)当A[k/2-1]<=B[k/2-1],则A[k/2 -1]至多只大于A中的前k/2 -1个数和B中前k/2 -1个数,即最多大于k-2个数,因此A[k/2-1]及其之前的元素不可能是第k小元素,可以排除;
(2)当B[k/2-1]<A[k/2-1]时,同理,可不考虑B[k/2-1]及其之前的元素。
反复利用上述方法,可以不断缩小范围。直到出现边界情况:
(1)一个数组为空时,直接返回剩余元素中的第k个元素;
(2)当k==1时,返回两个数组剩余元素中最小值。
注意:(1)当k/2-1越界时,取数组边界;
(2)注意去除元素后,要更新K值为剩余元素中的新大小。
class Solution {
public:
int getKthElement(const vector<int>& nums1, const vector<int>& nums2, int k) {
//找到两个数组中第k(编号:1~n+m)位小的数字
int m = nums1.size(), n = nums2.size();
int idx1 = 0, idx2 = 0;//idx1,idx2表示nums1,nums2剩余元素的起始下标
//边界情况
while(true){
//边界情况:一个数组到达边界,或k=1
if(idx1 == m) return nums2[idx2+k-1];//k表示当前剩余元素的第k个数,这时候还剩nums2的idx2~n-1这个范围的数字
if(idx2 == n) return nums1[idx1+k-1];
if(k==1) return min(nums1[idx1],nums2[idx2]);
//k/2-1越界时,取数组边界值
int newIdx1 = min(idx1+k/2-1,m-1);
int newIdx2 = min(idx2+k/2-1,n-1);
if(nums1[newIdx1]<=nums2[newIdx2]){//nums1中newIdx1及前的数字都可以不再考虑
k -= newIdx1 - idx1 +1;//这里的+1是因为newIdx+1也可以不考虑
idx1 = newIdx1 + 1;//nums1只需考虑newIdx1之后的元素
}
else{//nums2类似,镜像处理
k -= newIdx2 - idx2 +1;
idx2 = newIdx2 +1;
}
}
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int totalLength = nums1.size() + nums2.size();
//奇数个数时,返回第(n+m+1)/2位数
if(totalLength%2==1) return getKthElement(nums1,nums2,(totalLength+1)/2);
//偶数个数时,返回第(n+m)/2和第(n+m)/2+1位数的均值
return (getKthElement(nums1,nums2,totalLength/2) + getKthElement(nums1,nums2,(totalLength)/2+1))/2.0;
}
};