二分查找算法专题 c/c++实现 LeetCode二分题讲解

二分查找算法专题

:本博客是学习完 LeetCode暑假打卡 - B站 后的产物,若是对本博客不感兴趣的可以直接去看原视频。

转载的朋友请附上原博地址:https://blog.csdn.net/RangeLZ/article/details/96978346

在学习二分的时候发现了个很好用的二分查找算法模板

它将其分成了两个情况,接下来一个一个的进行讲解。(以下两个模板均来自 二分查找算法模板 )

该模板的算法思路:假设目标值在闭区间 [ l , r ] [l, r] [l,r] 中, 假设 M M M 是我们最后要的答案,那么这个区间就会被 M M M 分成两个部分。

而在判断的时候我们是将 M M M 放在左半部分还是右半部分,就会决定着我们的代码会是一个怎么的样子, 而这两个模板就是针对这两种情况而提供的。

版本一:

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

版本一是当我们将区间 [ l , r ] [l, r] [l,r] 划分成 [ l , m i d ] [l, mid] [l,mid] [ m i d + 1 , r ] [mid + 1, r] [mid+1,r] 时(既 M M M 属于右半部分的时候),其更新操作是 r = m i d r = mid r=mid 或者 l = m i d + 1 ; l = mid + 1; l=mid+1; ,计算 m i d mid mid 时不需要加1。

这个模板中为什么 l = m i d + 1 l = mid + 1 l=mid+1 呢?

是因为,当给 l l l 赋值的时候,是 m i d mid mid 不在区间的右半部分的时候。又因为答案 M M M 是在右半部分,所以可以知道 m i d mid mid 一定不是我们要的答案,因此 l = m i d + 1 l = mid + 1 l=mid+1 。(这里是满足区间相邻两个数差值为 1 1 1时的情况下 + 1 +1 +1

版本二:

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

版本二是当我们将区间 [ l , r ] [l, r] [l,r] 划分成 [ l , m i d − 1 ] [l, mid - 1] [l,mid1] [ m i d , r ] [mid, r] [mid,r] 时(既 M M M 属于左半部分的时候),其更新操作是 r = m i d − 1 r = mid - 1 r=mid1 或者 l = m i d ; l = mid; l=mid; ,此时为了防止死循环,计算 m i d mid mid 时需要加 1 1 1

对于为什么 r = m i d − 1 r = mid - 1 r=mid1 是因为,当给 r r r 赋值的时候,是 m i d mid mid 不在区间的左半部分的时候。又因为答案 M M M 是在左半部分,所以可以知道 m i d mid mid 一定不是我们要的答案,因此 r = m i d − 1 r = mid - 1 r=mid1 。(这里是满足区间相邻两个数差值为 1 1 1时的情况下 + 1 +1 +1

m i d = l + r + 1 > > 1 mid = l + r + 1 >> 1 mid=l+r+1>>1 是怎么回事呢?

我们可以想一下,若 l + 1 = r l + 1 = r l+1=r 时,我们采用 m i d = l + r > > 1 mid = l + r>> 1 mid=l+r>>1 会出现一个什么样的情况?

∵ m i d = l + r = ( 2 ∗ l + 1 ) / 2 = l \because mid = l + r = (2 * l + 1) / 2 = l mid=l+r=(2l+1)/2=l ∴ l = m i d = l \therefore l = mid = l l=mid=l ∴ 区 间 范 围 依 然 是 [ l , r ] \therefore区间范围依然是[l, r] [l,r]

所以为了出现这种死循环的情况,我们要这样计算 m i d = l + r + 1 > > 1 mid = l + r + 1 >> 1 mid=l+r+1>>1

例题:

了解了二分的基础后,就来做一点题进行实践。

  1. LeetCode 69. x 的平方根
  2. LeetCode 35. 搜索插入位置
  3. LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置
  4. LeetCode 74. 搜索二维矩阵
  5. LeetCode 153. 寻找旋转排序数组中的最小值
  6. LeetCode 33. 搜索旋转排序数组
  7. LeetCode 278. 第一个错误的版本
  8. LeetCode 162. 寻找峰值
  9. LeetCode 287. 寻找重复数
  10. LeetCode 275. H指数 II

No.1 LeetCode 69. x 的平方根

原题链接:x 的平方根 - 力扣

题目描述

请实现 int sqrt(int x)

请计算并返回 x x x 的正平方根,保证 x x x 是一个非负整数。
注意返回类型是整数,所以我们只返回正平方根的整数部分。

样例1

输入:4
输出:2

样例2

输入:8
输出:2
解释:8的正平方根是 2.82842...,它的整数部分是2.

解题思路:

首先我们要思考采用它是属于哪一种类型的模板。

最初我以为两个模板都可以使用,于是就直接用了第一个模板来写了一个,最后连样例都没有过…

然后思考后发现,这道题只能采用第二个模板,即答案 M M M 在左边的情况。

因为我们这道题要求的时 “由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。” 因此如果结果时 2.333 , 2.999 , 2.123 2.333, 2.999, 2.123 2.333,2.999,2.123 之类的最后输出都只能是 2 2 2

在确定了属于哪种类型后就直接套模板写代码即可。

A C AC AC代码:

int mySqrt(int x){
    int l = 0, r = x;
    while(l < r){
        int mid = l + (long long)r + 1 >> 1;
        if(mid <= x / mid)
            l = mid;
        else
            r = mid -1;
    }
    return r;
}

需要注意的是防止数据过大而导致数据溢出,上面的代码也做了防止溢出的相应处理。

No.2 LeetCode 35. 搜索插入位置

原题链接:LeetCode 35. 搜索插入位置

题目描述

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

样例1

输入: [1,3,5,6], 5
输出: 2

样例2

输入: [1,3,5,6], 2
输出: 1

样例3

输入: [1,3,5,6], 7
输出: 4

样例4

输入: [1,3,5,6], 0
输出: 0

解题思路:

这道题也是对公式的一个套用,因为我要寻找与 t a r g e t target target 相同的数字。若无相同的数字,则返回它将会被按顺序插入的位置。

于是我们就可以知道我们要寻找的是, n u m s [ m i d ] > = t a r g e t nums[mid] >= target nums[mid]>=target 区间的第一个位置。

所以显而易见选用模板一,解决问题。

A C AC AC代码:

int searchInsert(int* nums, int numsSize, int target){
    int l = 0, r = numsSize - 1;  
    if(nums[numsSize - 1] < target)
        return numsSize;
    while(l < r){
        int mid = l + r >> 1;
        if(nums[mid] >= target)
            r = mid;
        else
            l = mid + 1;
    }
    return l;
}

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

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

题目描述

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

你的算法时间复杂度必须是 O(log n) 级别。

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

请计算并返回 x x x 的正平方根,保证 x x x 是一个非负整数。
注意返回类型是整数,所以我们只返回正平方根的整数部分。

样例1

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

样例2

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

解题思路:

首先我们要寻找的是我们的开始处, b e g i n begin begin 的位置,这个时候 b e g i n begin begin n u m s [ m i d ] > = t a r g e t nums[mid] >= target nums[mid]>=target 区间里面的第一个数,因此采用模板一。

然后再寻找 e n d end end 的位置,这个时候 e n d end end n u m s [ m i d ] < = t a r g e t nums[mid] <= target nums[mid]<=target 区间的第一个数,因此采用模板二。

这道题,是模板一和模板二的综合应用的题。如果对上面两个模板掌握的好的话,这道题也没有上面难度。

A C AC AC代码:



/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* searchRange(int* nums, int numsSize, int target, int* returnSize){
    int *x=(int *)malloc(2*sizeof(int));
    *x=-1;
    *(x+1)=-1;
    *returnSize = 2;
    if(numsSize == 0)
        return x;
    int l = 0, r = numsSize - 1;
    while(l < r){
        int mid = l + r >> 1;
        if(nums[mid] >= target)
            r = mid;
        else
            l = mid + 1;
    }
    if(nums[l] != target)
        return x;
    *x = l;
    l = 0, r = numsSize - 1;
    while(l < r){
        int mid = l + r + 1 >> 1;
        if(nums[mid] <= target)
            l = mid;
        else
            r = mid - 1;
    }
    *(x+1) = r;
    return x;
}

No.4 LeetCode 74. 搜索二维矩阵

LeetCode 74. 搜索二维矩阵

题目描述

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

  • 每行中的整数从左到右按升序排列。
  • 每行的第一个整数大于前一行的最后一个整数。

样例1

输入:
matrix = [
  [1,   3,  5,  7],
  [10, 11, 16, 20],
  [23, 30, 34, 50]
]
target = 3
输出: true

样例2

matrix = [
  [1,   3,  5,  7],
  [10, 11, 16, 20],
  [23, 30, 34, 50]
]
target = 13
输出: false

解题思路:

其实这道题的本质内容上和 LeetCode 35. 搜索插入位置 是相似的,这道题的核心问题是如何对一个二维的数组进行判断。

首先我们第一种方法是把这个二维数组重新按照从左到右,从上到下的顺序存储到一个一维数组里面,然后再二分进行判断。但这样做不如按照顺序循环一遍依次判断是否存在 t a r g e t target target

因此我们可以用 n 、 m n、m nm 来存储矩阵的行数和列数。

然后把它看成一条线(只是看成一条线),则它的初始范围就是 l = 0 , r = n ∗ m − 1 l = 0, r = n * m - 1 l=0,r=nm1.

然后就是一个和前面类似的用二分来找我们的 t a r g e t target target

这里需要注意的是,我们只是把这个矩阵看成了一条线,但它本身并不是一条线,依然是一个二维数组。所以查看该位置的值的时候,要用 m a t r i x [ m i d matrix[mid matrix[mid / / / m ] [ m][ m][ m i d mid mid % m ] m] m] 来表示。

A C AC AC代码(算法一):

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        if(matrix.empty() || matrix[0].empty())
            return false;
        int n = matrix.size(), m = matrix[0].size();
        int l = 0, r = n * m - 1;
        while(l < r){
            int mid = l + r >> 1;
            if(matrix[mid / m][mid % m] >= target)
                r = mid;
            else
                l = mid + 1;
        }
        if(matrix[r / m][r % m] != target)
            return  false;
        else
            return true;
    }
};

第二种做法,先用二分查找判断每一行的第一个数,找到 m a t r i x [ m i d ] [ 0 ] < = t a r g e t matrix[mid][0] <= target matrix[mid][0]<=target 区间第一个,就是我们 t a r g e t target target 可能存在的的那一行。

如果 t a r g e t target target 存在,则一定在这一行里,然后我们再在这一行里面用二分,找到 m a t r i x [ r ] [ m i n d ] < = t a r g e t matrix[r][mind] <= target matrix[r][mind]<=target 区间里的第一个位置。

然后判断这个位置的答案是否和我们要寻找的 t a r g e t target target 是否相等。

如果相等放回 t r u e true true, 否则返回 f a l s e false false

注意要判断一下空集的情况。

A C AC AC代码(算法二):

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        if(matrix.empty() || matrix[0].empty())
            return false;
        int l = 0, r = matrix.size() - 1;
        while(l < r){
            int mid = l + r + 1 >> 1;
            if(matrix[mind][0] <= target)
                l = mid;
            else
                r = mid - 1;
        }
        int n = 0, m = matrix[0].size() - 1;
        while(n < m){
            int mid = n + m + 1 >> 1;
            if(matrix[r][mid] <= target)
                n = mid;
            else
                m = mid - 1;
        }
        if(matrix[r][m] == target)
            return true;
        else
            return false;
    }
};

No.5 LeetCode 153. 寻找旋转排序数组中的最小值

LeetCode 153. 寻找旋转排序数组中的最小值

题目描述

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

请找出其中最小的元素。

你可以假设数组中不存在重复元素。

样例1

输入: [3,4,5,1,2]
输出: 1

样例2

输入: [4,5,6,7,0,1,2]
输出: 0

解题思路:

这道题有些人第一眼看到会以为这是要排序,但其实这道题也是可以用二分来做的。

只是这道题对于二分要分成两段的那个判断不是很清楚。

但我们细想一下可以发现,这个数组的后半部分都是小于等于最后一个数的,于是乎我们可以知道我们要寻找的是满足 n u m s [ m i d ] < = n u m s . b a c k ( ) nums[mid] <= nums.back() nums[mid]<=nums.back() 区间的第一个数。

然后就是套用模板一写出AC代码。

A C AC AC代码:

class Solution {
public:
    int findMin(vector<int>& nums) {
        int l = 0, r = nums.size() - 1;
        while(l < r){
            int mid = l + r >> 1;
            if(nums[mid] <= nums.back())
                r = mid;
            else
                l = mid + 1;
        }
        return nums[l];
    }
};

No.6 LeetCode 33. 搜索旋转排序数组

LeetCode 33. 搜索旋转排序数组

题目描述

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1

你可以假设数组中不存在重复的元素。

你的算法时间复杂度必须是 O(log n) 级别。

样例1

输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4

样例2

输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1

解题思路:

这道题其实上是上一道题的一个升级版本,这个数组我们无法直接的将他分成两个部分,所以说不能直接对他运用二分进行求解。

但是这道题还是可以用二分进行求解。

首先我们用和上一道题一样的方法找到最小值,这样我们能知道这个数组是从哪来开始分成两个部分的。

找到最小值后,我们将 t a r g e t target target 与数组的最后一个数进行比较,这样我们就可以确定我们要找的数是在左半部分、还是右半部分了。

最后再在选中的部分进行二分来找我们有无与 t a r g e t target target 相等的值。

A C AC AC代码:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        if(nums.empty())
            return -1;
        int l = 0, r = nums.size() - 1;
        while(l < r){
            int mid = l + r >> 1;
            if(nums[mid] <= nums.back())
                r = mid;
            else
                l = mid + 1;
        }
        if(target <= nums.back())
            r = nums.size() - 1;
        else
            l = 0, r--;
        while(l < r){
            int mid = l + r >> 1;
            if(nums[mid] >= target)
                r = mid;
            else
                l = mid + 1;
        }
        if(nums[l] == target)
            return l;
        else
            return  -1;
    }
};

No.7 LeetCode 278. 第一个错误的版本

LeetCode 278. 第一个错误的版本

题目描述

你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本[1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用bool isBadVersion(version)接口来判断版本号version是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

样例

给定 n = 5,并且 version = 4 是第一个错误的版本。

调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true

所以,4 是第一个错误的版本。 

解题思路:

这道题是真的很简单,直接判断一下是用模板一还是模板二即可,而且这个题的 bool isBadVersion(version) 就是我们的判断条件。

所以这道题没什么说的,直接敲代码AC完事。

唯一需要注意的是,数据可能过大而导致的溢出。

A C AC AC代码:

// Forward declaration of isBadVersion API.
bool isBadVersion(int version);

class Solution {
public:
    int firstBadVersion(int n) {
        int l = 1, r = n;
        while(l < r){
            int mid = l + (long long)r >> 1;
            if(isBadVersion(mid))
                r = mid;
            else
                l = mid + 1;
        }
        return l;
    }
};

No.8 LeetCode 162. 寻找峰值

LeetCode 162. 寻找峰值

题目描述

峰值元素是指其值大于左右相邻值的元素。

给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。

数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞

样例1

输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。

样例2

输入: nums = [1,2,1,3,5,6,4]
输出: 1 或 5 
解释: 你的函数可以返回索引 1,其峰值元素为 2;
     或者返回索引 5, 其峰值元素为 6。

说明:

  • 你的解法应该是 O(logN) 时间复杂度的。

解题思路:

这道题虽然不能直接把数组分成两个部分,但由于它只需要我们返回任何一个峰值所在的位置,所以我们每次可以通过二分来折半我们的判断范围。

我们发现,峰值有个特点,那就是它比左右两边都要大。

于是我们每次判断 n u m s [ m i d ] > n u m s [ m i d + 1 ] nums[mid] > nums[mid + 1] nums[mid]>nums[mid+1] 。若大于,则说明在当前这个点的位置及它右边的位置一定有个峰值;若小于,则说明在当前这个点的位置及它左边的位置一定有个峰值。

而若是这个点的某一边是呈单调变化的,则端点就是一个峰值,因为题目已经假设 n u m s [ − 1 ] = n u m s [ n ] = − ∞ nums[-1] = nums[n] = -∞ nums[1]=nums[n]=.

然后套用模板,就AC了。

A C AC AC代码:

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int l = 0, r = nums.size() - 1;
        while(l < r){
            int mid = l + r >> 1;
            if(nums[mid] > nums[mid + 1])
                r = mid;
            else
                l = mid + 1;
        }
        return l;
    }
}; 

No.9 LeetCode 287. 寻找重复数

LeetCode 287. 寻找重复数

题目描述

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

样例1

输入: [1,3,4,2,2]
输出: 2

样例2

输入: [3,1,3,4,2]
输出: 3

说明

  1. 不能更改原数组(假设数组是只读的)。
  2. 只能使用额外的 O(1) 的空间。
  3. 时间复杂度小于 O(n2) 。
  4. 数组中只有一个重复的数字,但它可能不止重复出现一次。

解题思路:

这道题二分的判断方法和抽屉问题类似。

抽屉问题是什么呢,简而言之就是把至少 n + 1个的苹果,按任意确定的方式分进n个抽屉里面,那么一定至少有一个抽屉中,含有至少两个苹果。

这道题也可以按照抽屉问题的思维来看,每次计算出 m i d mid mid 后,判断 c n t > m i d − l + 1 cnt > mid - l + 1 cnt>midl+1 c n t cnt cnt 是表示数组中有多少个在 l → r l \rightarrow r lr 范围内的整数。

若结果是大于,则说明在 l → r l \rightarrow r lr 范围内的苹果,大于该范围内的抽屉数,所以这个范围内一定有重复的数。反之,则不存在。

然后实现这个思路即可。

A C AC AC代码:

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int l = 1, r = nums.size() - 1;
        while(l < r){
            int mid = l + r >> 1;
            int cnt = 0;
            for(auto x : nums)
                if(x >= l && x <= mid)
                    cnt++;
            if(cnt > mid - l + 1)
                r = mid;
            else
                l = mid + 1;
        }
        return l;
    }
};

No.10 LeetCode 275. H指数 II

LeetCode 275. H指数 II

题目描述

给定一位研究者论文被引用次数的数组(被引用次数是非负整数),数组已经按照升序排列。编写一个方法,计算出研究者的 h 指数。

h 指数的定义: “h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (N 篇论文中)至多有 h 篇论文分别被引用了至少 h 次。(其余的 N - h 篇论文每篇被引用次数不多于 h 次。)"

样例

输入: citations = [0,1,3,5,6]
输出: 3 
解释: 给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6 次。
     由于研究者有 3 篇论文每篇至少被引用了 3 次,
     其余两篇论文每篇被引用不多于 3 次,
     所以她的 h 指数是 3。

说明

  • 如果 h 有多有种可能的值 ,h 指数是其中最大的那个。

解题思路:

这道题其实看清了本质后很简单的,就是判断 c i t a t i o n s [ c i t a t i o n s . s i z e ( ) − m i d ] > = m i d citations[citations.size() - mid] >= mid citations[citations.size()mid]>=mid

若该位置的值小于该位置距离末尾的长度,就说明我们寻找的 h h h 在它的右边,而且它也有可能是这个 h h h

然后AC.

A C AC AC代码:

class Solution {
public:
    int hIndex(vector<int>& citations) {
        int l = 0, r = citations.size();
        while(l < r){
            int mid = l + r + 1>> 1;
            if(citations[citations.size() - mid] >= mid)
                l = mid;
            else
                r = mid - 1;
        }
        return r;
    }
};

小结

首先,感谢闫学灿大神的教学,真的学到了很多东西。

闫学灿大神的这两套模板,基本上大部分的二分算法题都可以解决,重要的是学会判断是用模板一,还是模板二。以及 i f if if 判断时候的条件怎么写。

总之这么学习了一遍后,我感觉对于二分查找算法有一定程度的掌握了。

最后,再次感谢闫学灿大神,真的,真的学到了超多的东西。

你可能感兴趣的:(算法学习)