Leetcode二分查找合集

二分查找

二分查找,也被称为二分法或折半查找,每次查找时通过将待查找区间分成两部分并只取一部分继续查找,可大大降低查找复杂度。如:

对于一个长度为O(n)的数组,二分查找的时间复杂度为O(log n)

如何定义二分查找时区间的左右端开闭性?书中提供了两点:一是尝试熟练应用一种写法,如左闭右开满足C++、Python等语言习惯)或左闭右闭便于处理边界条件);二是思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。

更加具体的解题方法参考力扣题解

模板一:
当我们将区间[l, r]划分成[l, mid][mid + 1, r]时,其更新操作是r = mid或者l = mid + 1,计算mid时不需要加1,即mid = (l + r)/2,为防止l + r溢出可以写作mid = l + (r-l)/2
代码模板:

int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = (l + r)/2;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    return l;
}

模板2:
当我们将区间[l, r]划分成[l, mid-1][mid, r]时,其更新操作是r = mid-1或者l = mid,为了防止死循环,计算mid时需要加1,即mid = (l + r+1)/2
代码模板:

int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = ( l + r + 1 ) /2;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

问题1:为什么模板1和模板2的mid取值不同?
对于第二个模板,当我们更新区间时,如果左边界l更新为l = mid,此时mid的取值就应为mid = (l + r + 1)/ 2。因为当右边界r = l + 1时,此时mid = (l + l + 1)/2,下取整,mid仍为l,左边界再次更新为l = mid = l,相当于没有变化,while循环就会陷入死循环。因此,我们总结出来一个小技巧,当左边界要更新为l = mid时,我们就令 mid =(l + r + 1)/2,上取整,此时就不会因为r取特殊值r = l + 1而陷入死循环了。

问题2:为什么模板要取while( l < r),而不是while( l <= r)
本质上取l < rl <= r是没有任何区别的,只是习惯问题,如果取l <= r,只需要修改对应的更新区间即可。

问题3:while循环结束条件是l >= r,但为什么二分结束时我们优先取r而不是l?
二分的while循环的结束条件是l >= r,所以在循环结束时l有可能会大于r,此时就可能导致越界,因此,基本上二分问题优先取r都不会翻车。

求开方

69.x 的平方根

给你一个非负整数x,计算并返回x算术平方根

由于返回类型是整数,结果只保留整数部分,小数部分将被舍去

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5)或者 x ** 0.5

示例 1:
输入:x = 4
输出:2
示例 2:

示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842…, 由于返回类型是整数,小数部分将被舍去。

思路:

二分查找法:可以将这道题转换为:给定一个非负整数a,求f(x) = x^2 - a = 0的解。已知x >= 0,所以函数在定义域单调递增。考虑到f(0) = -a <= 0f(a) = a^2 - a >= 0,所以在[0,a]区间上使用二分法找到f(x) = 0的解。需要注意a = 0的情况需要单独讨论。

牛顿迭代法:摘自百度百科
牛顿迭代法
此处f(x) = x^2 - a = 0,有 x n + 1 = ( x n + a / x n ) / 2 x_{n+1} = (x_n + a / x_n)/2 xn+1=(xn+a/xn)/2

代码1-常规解法:

class Solution {
public:
    long int mySqrt(long int x) {
        if (x == 0) return 0;
        else if (x == 1) return 1;
        long int i;//防止溢出
        for (i = 1;i < x;i++){
            if (i * i <= x) continue;
            else break;
        }
        return i - 1;
    }
};

代码2-二分查找法:

class Solution {
public:
    int mySqrt(int x) {
        if(x == 0) return x;
        int l = 1,r = x,mid,sqrt;
        while(l <= r){
            mid = l + (r - l)/2;//详见注释
            sqrt = x / mid;
            if(sqrt == mid){
                return mid;
            }
            else if(sqrt > mid){//如果mid大于sqrt,则x/mid>mid,x-mid^2>0,证明sqrt应该在mid右边区间重新二分查找
                l = mid + 1;
            }
            else r = mid - 1;
        }
        return r;//如果到最后还是没有sqrt == mid的情况,此时返回r即可
    }
};

注释:mid = (l + r)/2 = (2l + r - l)/2 = l + (r - l)/2,这样做的目的是防止l + r过大而溢出。

代码3-牛顿迭代法:

class Solution {
public:
    int mySqrt(int x) {
        long int sqrt = x;
        while (sqrt * sqrt > x){
            sqrt = (sqrt + x / sqrt)/2;
        }
        return sqrt;
    }
};

查找区间

34. 在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组nums,和一个目标值target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值target,返回[-1, -1]

你必须设计并实现时间复杂度为O(log n)的算法解决此问题。

示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:
输入:nums = [], target = 0
输出:[-1,-1]

思路:

nums = [5,7,7,8,8,10], target = 8为例进行分析:

一、第一次二分查找target第一次出现的位置:

  1. 二分的范围:l = 0r = nums.size() - 1,需要去二分查找>= target的最左边界;
  2. nums[mid] >= target时,往左半区域找,r = mid
  3. nums[mid] < target时,往右半区域找,l = mid + 1
  4. 如果最后nums[r] != target,说明数组中不存在目标值target,返回[-1,-1],否则得到target第一次出现的位置L

第一次二分查找

二、第二次二分查找target最后一次出现的位置:

  1. 二分的范围:l = 0r = nums.size() - 1,需要去二分查找<= target的最右边界;
  2. nums[mid] <= target时,往右半区域找,l = mid
  3. nums[mid] > target时,往左半区域找,r = mid - 1
  4. 得到target最后一次出现的位置R

第二次二分查找

代码1-不完全二分法但是是自己独立思考出来的:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        //必须设计并实现时间复杂度为 O(log n) 的算法解决此问题:使用二分查找实现要求复杂度
        //第一个出现的位置和最后出现的位置 参考763
        //数组越界问题需要考虑
        vector<int> ans(2,-1);
        if(nums.size() == 0) return ans;
        int l = 0,r = nums.size()-1,mid;
        while(l <= r){
            mid = l + (r - l)/2;
            if(nums[mid] == target){//证明需要往前往后找
                l = mid;
                r = mid;
                while(l != 0 && nums[l - 1] == target){//这里就不是二分了 属于一个一个查找
                    l--;
                }
                while(r != nums.size() - 1 && nums[r + 1] == target){
                    r++;
                }
                ans[0] = l;
                ans[1] = r;
                return ans;
            }
            else if(nums[mid] < target){//证明需要往后找
                l = mid + 1;
            }
            else{//证明需要往前找
                r = mid - 1;
            }
        }
        return ans;
    }
};

代码2-二分查找法:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if (nums.size() == 0) return {-1,-1};
        int l = 0, r = nums.size() - 1, L, R;
        //第一次查找 找大于等于目标值的最左边界
        while(l < r){
            int mid = (l + r) / 2;
            if(nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        if(nums[r] != target) return{-1,-1};
        //第二次查找 找小于等于目标值的最右边界
        L = r; 
        l = 0, r = nums.size() - 1;
        while(l < r){
            int mid = (l + r + 1) / 2;
            if(nums[mid] <= target) l = mid;
            else r = mid - 1;
        }
        R = r;
        return {L,R};
    }
};

**补充知识std::lower_bound()std::upper_bound() **

这道题可以看作是自己实现C++ 里的lower_bound 和upper_bound 函数。

std::lower_bound() 是在区间内找到第一个大于等于 value 的值的位置并返回,如果没找到就返回 end() 位置;
std::upper_bound() 是找到第一个大于 value 值的位置并返回,如果找不到同样返回 end() 位置。

代码:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> ans = {-1, -1};
        //查找第一个大于或等于target的元素的迭代器
        auto it_begin = lower_bound(nums.begin(), nums.end(), target);
        //如果找到且等于target
        if(it_begin != nums.end() && *it_begin == target)
            ans[0] = it_begin - nums.begin();
        //查找第一个大于target的元素的迭代器
        auto it_end = upper_bound(nums.begin(), nums.end(), target);
        //当nums只有一个元素时且大于或等于target时,it_end肯定会指向nums.end()
        //                    小于时,nums.begin() == it_end,此时返回-1
        if(it_end != nums.begin() && *(it_end - 1) == target)
            ans[1] = it_end - nums.begin() - 1;
        return ans;  
    }
};

旋转数组查找数字

81.搜索旋转排序数组 II

已知存在一个按非降序排列的整数数组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,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4]

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false

你必须尽可能减少整个操作步骤。

示例 1:
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true

示例 2:
输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false

思路:
对于当前的中点,如果它指向的值小于等于右端,那么说明右区间是排好序的;反之,那么说明左区间是排好序的。
如果目标值位于排好序的区间内,我们可以对这个区间继续二分查找;反之,我们对于另一半区间继续二分查找。
注意,因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找。

图解:
图解

代码:

class Solution {
public:
    bool search(vector<int>& nums, int target) {
        int l = 0, r = nums.size() - 1;
        while (l <= r){
            int mid = l + (r - l) / 2;
            if (nums[mid] == target){
                return true;
            }
            if (nums[l] == nums[mid]){ //必须最先判断这个条件
                ++l;
            }
            else if (nums[mid] <= nums[r]){//右区间升序
                if(target > nums[mid] && target <= nums[r]){
                    l = mid + 1;
                }
                else r = mid - 1;
            }
            else{//左区间升序
                if (target >= nums[l] && target < nums[mid]){
                    r = mid - 1;
                }
                else l = mid + 1;
            }
        }
        return false;
    }
};

练习

154.寻找旋转排序数组中的最小值 II

已知一个长度为 n 的数组,预先按照升序排列,经由 1n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须尽可能减少整个过程的操作步骤。

示例 1:
输入:nums = [1,3,5]
输出:1

示例 2:
输入:nums = [2,2,2,0,1]
输出:0

思路:

数组nums分成左右两个升序区间,找出数组的最小值等价于找出升序区间2的头。

  1. 如果nums[mid] == nums[r],那么就让--r,相当于剔除部分重复元素;
  2. 如果nums[mid] < nums[r],证明自此往右的区间是升序区间2(的一部分),nums[mid]可能为升序区间2的头,要再从左区间找数组最小值,r = mid;
  3. 否则左区间为升序区间,头不可能在左区间内,要再从右区间找升序区间2的头,l = mid + 1;
  4. 最后l == rreturn nums[r];

代码:

class Solution {
public:
    int findMin(vector<int>& nums) {
        int n = nums.size();
        int l = 0, r = n - 1, mid, target;
        while(l < r){
            mid = l + (r - l)/2;            
            if(nums[mid] == nums[r]) --r;//注意和81题对比
            else if(nums[mid] < nums[r]){
                r = mid;
            }
            else{
                l = mid + 1;
            }
        }
        return nums[r];
    }
};

540. 有序数组中的单一元素

给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。

请你找出并返回只出现一次的那个数。

你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。

示例 1:
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2

示例 2:
输入: nums = [3,3,7,7,10,11,11]
输出: 10

思路:

当看到题目要求 O(log n) 时间复杂度时,我们的想法一定是采用二分查找。但如何逐步缩小范围得到我们所需要的值呢?需要分情况讨论,刚好可以使用两个示例作为不同情况。

  1. nums = [1,1,2,3,3,4,4,8,8]
    l = 0r = nums.size() - 1时,mid = (l + r) / 2 = 4,此时mid为偶数,证明前面已经有偶数个数的数字,如果nums[mid + 1] != nums[mid],证明单一元素一定在mid之前的区间(包含mid);否则单一元素一定在mid+1后面的区间;
  2. nums = [3,3,7,7,10,11,11]
    l = 0r = nums.size() - 1时,mid = (l + r) / 2 = 3,此时mid为奇数,证明前面已经有奇数个数的数字,如果nums[mid - 1] != nums[mid],证明单一元素一定在mid之前的区间(包含mid);否则单一元素一定在mid后面的区间。
  3. 最终l = r,返回nums[r]即可。

图解:
图解

代码:

class Solution {
public:
    int singleNonDuplicate(vector<int>& nums) {
        //在出现独立数之前和之后,奇偶位数的值发生了什么变化? c下标出现了小于0的-1
        int n = nums.size() - 1;
        int l = 0, r = n;
        while(l < r){
            int mid = l + (r - l) / 2;
            if(mid % 2 == 0){//如果mid是偶数,其前面的数字是偶数个(事实证明我的思路应该没什么大问题,重点在于判断mid是否是偶数出了问题!!)
            // /是取两数相除后的商 %是取两数相除后的余
                if(nums[mid + 1] != nums[mid]){//如果mid后面一个数不等于mid,证明单一元素一定在mid之前的区间(包含mid)
                    r = mid;
                }
                else{//否则单一元素一定在mid+1后面的区间
                    l = mid + 2;
                }
            }
            else{//如果mid是奇数,其前面的数字是奇数个
                if(nums[mid - 1] != nums[mid]){//如果mid前面一个数不等于mid,证明单一元素一定在mid之前的区间(包含mid)
                    r = mid;
                }
                else{//否则单一元素一定在mid后面的区间
                    l = mid + 1;;
                }
            }
        }           
        return nums[r];
    }
};

4. 寻找两个正序数组的中位数

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

算法的时间复杂度应该为 O(log (m+n))

示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

思路:

首先想到的一定是暴力解法,合并两个数组,排序,找出中位数,若采用冒泡排序的方法,则时间复杂度可能达到O(m + n) ^ 2;接着会想到双指针解法,两个指针分别指向两个数组,直到某一指针指向中位数所在的位置,但是这样的复杂度是O(m + n);一般看到log的复杂度最好选用二分查找法,看了力扣好多题解,头都晕了,最后参考题解进行分析加举例帮助理解叭!

中位数:奇数长度有序数组的中位数为最中间的数字,偶数长度有序数组的中位数是最中间两个数字的平均值。假设两个有序数组的长度分别为mn,由于两个数组长度之和m + n的奇偶不确定,因此需要分情况来讨论。

此处作者使用了一个小trick来避免讨论奇偶:无论m+n是奇数或者偶数,只需分别找第 (m + n + 1) / 2个数和第(m + n + 2) / 2个数,求其平均值即可。

  1. 假如m + n= 7,为奇数,第4个数为数组的中位数,此时(m + n + 1) / 2 = 4(m + n + 2) / 2 = 4 ,指向的值都相等,平均数仍是其本身;
  2. 假如m + n= 8,为偶数,第4个数和第5个数的平均数为数组的中位数,此时(m + n + 1) / 2 = 4(m + n + 2) / 2 = 5 ,平均数为数组中位数。

那么接下来重点就变成如何在两个有序数组中找到第k个(第k小的)元素。

首先,为了避免产生新的数组从而增加时间复杂度,我们使用两个变量 ij 分别来标记数组nums1nums2的起始位置。

  1. 递归出口:
    k=1时候,相当于求最小值,我们只要比较nums1nums2的起始位置ij上的数字就可以了。

  2. 一般情况:
    取两个数组中的第k / 2个元素(midVal1midVal2)进行比较,如果midVal1 < midVal2,则说明nums1中的前k / 2个元素不可能成为第k个元素的候选,所以将nums1中的前k / 2个元素去掉,作为新数组和nums2求第k - k / 2小的元素,因为我们把前k / 2个元素去掉了,所以相应的k值也应该减少k / 2midVal1 > midVal2的情况亦然。
    举例说明:
    假设有两数组:nums1 = [1,3,5,7]nums2 = [2,4,6,8,10],需要找到第5小的元素,即k = 5
    nums1k / 2 = 2小的元素是midVal1 = 3nums2k / 2 = 2小的元素是midVal2 = 4midVal1 < midVal2,则nums1中的前k / 2个元素<=midVal1<midVal2,无论如何nums1的前k / 2个元素都在最前面,不可能是第k个。在本例中也就是1不可能是第5小的元素,需要剔除,相应的k值也应变为k - k / 2,等于3。

  3. 边界问题:
    当某一个数组的起始位置大于等于其数组长度时,说明其所有数字均已经被淘汰了,相当于一个空数组了,那么实际上就变成了在另一个数组中找数字,直接就可以找出来了。
    由于两个数组的长度不定,所以有可能某个数组元素数不足k / 2,所以我们需要先检查一下,数组中到底存不存在第k / 2个数字,如果存在就取出来,否则就赋值上一个整型最大值,这样肯定会大于另一个数组的第k / 2个元素,从而把另一个数组的前k / 2个元素淘汰。
    ps:赋予整型最大值的意思只是说如果第一个数组的k / 2不存在,则说明这个数组的长度小于k / 2,那么另外一个数组的前k / 2个我们是肯定不要的。例如,nums1长度是2,nums2长度是12,则k为7,k / 2为3,因为nums1长度小于3,则无法判断中位数是否在其中,而nums2的前3个数中一定没有中位数!

图解:

代码:

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) 
    {
        int m = nums1.size();
        int n = nums2.size();
        //中位数 = (left + right)/2
        int left = (m + n + 1) / 2;
        int right = (m + n + 2) / 2;
        return (findKth(nums1, 0, nums2, 0, left) + findKth(nums1, 0, nums2, 0, right)) / 2.0;
    }
    //在两个有序数组中找到第k个元素(例如找第一个元素,k=1,即nums[0])
    //i: nums1的起始位置 j: nums2的起始位置(i,j都是从0开始)
    int findKth(vector<int>& nums1, int i, vector<int>& nums2, int j, int k)
    {
        //若nums1为空(或是说其中数字全被淘汰了)
        //在nums2中找第k个元素,此时nums2起始位置是j,所以是j+k-1
        if(i >= nums1.size())    return nums2[j + k - 1];
        //nums2同理
        if(j >= nums2.size())    return nums1[i + k - 1];

        //递归出口
        if(k == 1)  return std::min(nums1[i], nums2[j]);

        //这两个数组的第K/2小的数字,若不足k/2个数字则赋值整型最大值,以便淘汰另一数组的前k/2个数字
        int midVal1 = (i + k/2 - 1 < nums1.size()) ? nums1[i + k/2 - 1] : INT_MAX;
        int midVal2 = (j + k/2 - 1 < nums2.size()) ? nums2[j + k/2 - 1] : INT_MAX;
        //二分法核心部分
        if(midVal1 < midVal2)
            return findKth(nums1, i + k/2, nums2, j, k - k/2);//如果midVal1 < midVal2,则舍弃nums1的前k/2个元素
        else
            return findKth(nums1, i, nums2, j + k/2, k - k/2);//如果midVal1 >= midVal2,则舍弃nums2的前k/2个元素
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值