LeetCode刷题笔记(3): 二分法

二分法主要是利用折半查找的思想,对区间进行查找,使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取或修改其值。

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;
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值