我爱学算法之—— 二分查找(上)

了解二分算法

二分查找,想必多多少少有一点了解了,我们了解的二分查找算法:

当一个数组有序的时候,我们可以使用二分算法来查找一个值;

直接比较mid((left + right)/2)和我们要查找的值target;如果nums[mid] > target就在右边查找,否则在左边查找。

但是二分查找真的如此简单吗?在什么时候才能使用二分查找呢?

使用二分查找的条件:数组有序;其实本质是是利用二段性

简单来说,将数组分为;两个区间,一个区间内是满足某个条件;而另一个区间是满足其相反的条件的。

现在我们来通过了解二分查找的算法题,来深入探究二分查找,以及什么时候能够使用二分查找。

一、二分查找

题目解析

这道题,想必之前已经见到过了;

给定一个数组nums和一个值target,让我们在nums数组中查找target,如果存在就返回下标;否则返回-1

算法思路

对于这道题,我们可以使用暴力查找:让target和数组nums中所有元素一个一个比较;

暴力解法时间复杂度为O(n);从效率上来说也是非常不错的;

但是暴力解法并没有使用到我们数组有序这一个条件;

我们这里想一下,当我们暴力查找到一个位置mid时,区间[0,mid-1]内的值是不是都是小于mid位置的值的;区间[mid+1,n]内的值是不是都是大于mid位置的值的。

简单来说就是:我们任取一个位置,这个位置的值为x;这个位置左边区间的值都是小于x的,右边区间的值都是大于x的。

那我们是不是就可以将区间划分为两部分:

  • 左边区间的值都是小于x
  • 右边区间的值都是大于x

这样我们任取一个位置,如果这个位置的值x大于我们要找的值target,那就去这个位置左边区间再去找target

如果这个位置的值x大于我们要找的值target,那就去这个位置右边区间内再去找target

那我们大致就理解了如何去找target;但是我们可以取二分点也可以取三分点四分点...,那如何去取mid呢?

这里就不叙述这个问题了,我们取二分点的效率是最高的。

二分整体思路:

  • 首先定义leftright分别指向区间的开始位置和结束位置。

  • midmid = (left + right)/2

  • 比较mid位置和要查找的值target

    如果nums[mid] > target,就去左边区间找,right = mid - 1

    如果nums[mid] < target,就去右边区间找,left = mid + 1

    如果nums[mid] == target,找到了我们要查找的值,返回结果。

  • 查找结束还没有返回结果(leftright错过去了,那就表示数组中不存在target)。

这里需要注意:

**循环结束的条件:**我们leftright指向的位置都是没有查找过的位置,所以当left > right时,循环才能结束。

**取mid:**这里如果数组过大,left + right就可能超出数据范围,我们使用left + (right - left)/2或者left +(right - left + 1)/2来计算;但是有一个问题,对于数组内数据个数是奇数时,这两种计算方式没有什么影响;但如果数组中数据个数是偶数时,第一种left + (right - left)/2求的mid是偏左的,而left + (right - left +1)/2求出的mid是偏右的。

在这道题中我们感受不到这两种求法的差别,在下面题目中我们就能感受到这两种求法的差别了。

代码实现

cpp 复制代码
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1;
        while (left <= right) {
            // int mid = (left+right)/2;
            int mid = left + (right - left) / 2;
            if (nums[mid] > target)
                right = mid - 1;
            else if (nums[mid] < target)
                left = mid + 1;
            else
                return mid;
        }
        return -1;
    }
};

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

题目解析

这道题和上面那一道题不同,上一道题目在nums数组中只存在应该target,而这道题目中可能存在多个target

我们需要找到多个target的起始位置和结束位置。

如果数组中不存在target就返回-1,-1

算法思路

首先还是来看暴力解法:从左到右遍历数组,遇到target,就记录target起始位置,然后继续向后遍历直到某个位置的值不等于target

如果数组中不存在target,那暴力解法最坏情况下的时间复杂度为O(n)

那现在我们来想如何使用二分查找来解决这个问题:

这里相信有人和博主一样,先利用二分查找查找到target的某一个位置,然后向左和向右遍历查找target出现的起始位置和结束位置;但是如果数组中的数据都是target,那我们不也是要查找完整个数组,时间复杂度也是O(n)

这里我们就不使用上面二分算法划分区间的方法了,因为我们这里target不一定只出现一次,我们找到target时不能直接返回,因为我们不确定是否还存在其他target

这里我们要查找的是target的起始位置和结束位置,说白了就是左边界和右边界。

二分算法查找左边界:

这里我们查找到target不能直接返回,那就试着将nums[mid] == target划分到左边或者右边的情况;

简单来说就是这里要找的是大于等于t区间的左边界,我们将数组划分成两部分:

  • 左边区间内的值都是小于target的。
  • 右边区间内的值都是大于等于target的。

这样我们在使用二分查找时:

  • 如果nums[mid] < target,那就可以直接舍去[left , mid-1]mid位置的(left = mid + 1);
  • 如果nums[mid] >= target,我们的mid位置的值可能等于target,所以我们只能舍去区间[mid + 1 , right];(right = mid)(这里我们要找的是左边界,如果mid位置的值是等于mid+1位置的值时,我们是可以舍去mid+1位置的

这里我们要求的是大于等于target区间的左边界,所以划分成小于x和大于等于x的两个区间

这里要注意:

**循环结束条件:**这里我们当left == right时,循环就结束了;所以循环的条件是left < right而不是left <= right

  • 这里left == right时是不需要判断的,因为此时就是最终结果:

    数组中存在大于等于target的区间,也存在小于target的区间,此时leftright相等时指向的就是大于等于target区间左端点的位置

    数组中如果所有数都大于等于target,此时right最终会指向left的位置也就是数组的起始位置,也是大于等于target区间的左端点的位置。

    数组中如果所有数都小于target,此时left最终最指向left的数组的结束位置,也就是right

  • 如果left == right判断了,可能会陷入死循环

    因为这里当nums[mid] >= target时,right = mid;这样如果最后leftright指向的位置是大于等于target的,求出的mid是等于leftright的,那此时就会陷入死循环。

mid的值:

在上面朴素的二分查找算法中,我们利用哪一种求法都可以,但是在这里就不一样了;

如果数据个数是偶数个,利用mid = left + (right - left)/2求出的mid是偏左的;利用mid = left + (right - left + 1)/2求出来的mid是偏右的;

这里我们要找的是区间的左边界,我们要使用mid = left + (right - left)/2来求mid

因为最后如果leftright指向两个相邻的位置(left + 1 = right),利用第一种方法求出来的mid是等于left的;利用第二章方法求出来的mid是等于right的;

如果我们right位置的值的大于等于target的,如果求出的mid是等于right的,此时就会陷入死循环;(因为nums[mid] >= target时,right = mid

二分算法查找右边界:

和查找左边界类似:

我们要查找的是小于等于target区间的有边界,我们可以根据要查找的位置将数组划分成两部分:

  • 左边区间内的值都是小于等于target
  • 右边区间内的值都是大于target

在二分查找的过程中:

  • 如果nums[mid] <= targetmid位置可能就是最终要查找的结果,所以只能舍去区间[left , mid-1]left = mid);(这里查找的是区间的右边界,所以即使mid-1位置的值等于mid位置的值,也是直接可以舍去的
  • 如果nums[mid] > target,区间[mid , right]内的值都是大于target的,可以舍去区间[mid , right]right = mid - 1)。

这里我们要查找的是小于等于target区间的右边界所以划分为:小于等于target和大于target两区间

这里也要注意:

循环条件left < right而不是left<=right

mid

在求左边界时使用的是mid = left + (right - left)/2,这样在偶数个数据时求的是偏左位置的;

这里我们要使用mid = left + (right - left + 1)/2,这样当数组在数据个数是偶数个时,求出的mid是偏右的。

因为最后如果leftright指向两个相邻的位置(left + 1 = right),利用第一种方法求出来的mid是等于left的;利用第二章方法求出来的mid是等于right的;

如果我们left位置的值的小于等于target的,如果求出的mid是等于left的,此时就会陷入死循环;(因为nums[mid] <= target时,left = mid

代码实现

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        //数组为空
        if(nums.empty())    return {-1,-1};
        int n = nums.size();
        int begin = 0,end = 0;
        //求大于等于target区间的左边界
        int left = 0,right = n-1;
        while(left < right)
        {
            int mid = left + (right - left)/2;
            if(nums[mid] >= target) right = mid;
            else    left = mid + 1;
        }
        //判断是否存在target
        if(nums[left] != target)    return {-1,-1};
        begin = left;
        //求小于等于target区间的右边界
        left = 0,right = n-1;
        while(left < right)
        {
            int mid = left + (right - left + 1)/2;
            if(nums[mid] <= target) left = mid;
            else    right = mid -1;
        }
        end = right;
        return {begin,end};
    }
};

简单总结

这里两道题,算是最基本的二分算法题,我们一定要理解,理解之后在之后的二分算法题目再深入探究二分算法。

相关推荐
小生凡一2 分钟前
腾讯二面:TCC分布式事务 | 图解TCC|用Go语言实现一个TCC
开发语言·分布式·golang
minji...6 分钟前
C语言 函数递归
c语言·开发语言·算法
你好我是咯咯咯8 分钟前
代码随想录算法训练营Day36
算法
uhakadotcom16 分钟前
如何用AI打造高效招聘系统,HR效率提升100%!
后端·算法·面试
云上空26 分钟前
C#初级知识总结
开发语言·c#
虾球xz37 分钟前
游戏引擎学习第246天:将 Worker 上下文移到主线程创建
c++·学习·游戏引擎
纪元A梦44 分钟前
华为OD机试真题——绘图机器(2025A卷:100分)Java/python/JavaScript/C++/C/GO最佳实现
java·javascript·c++·python·华为od·go·华为od机试题
钢铁男儿1 小时前
C# 深入理解类:面向对象编程的核心数据结构
开发语言·数据结构·c#
Doker 多克1 小时前
Python-Django系列—部件
开发语言·python
Felven1 小时前
A. Everybody Likes Good Arrays!
数据结构·算法