【ONE·基础算法 || 二分查找】

在这里插入图片描述

总言

  主要内容:编程题举例,理解二分查找的思想。
  
  


  
  
  
  
  
  

1、二分查找

  折半查找法也称为二分查找法,它充分利用了元素间的次序关系,采用分治策略,达成特定元素的搜索。其时间复杂度为 O ( l o g n ) O(log n) O(logn),因此在数据量较大时,查找效率非常高。
  
  说明:
  1、二分查找不一定要求数组有序,实际上只要满足区间的二段性即可。(找到某一位置,能将区间分成两段,每段具有不同特性/规律)
  2、通常可将二分查找分为朴素的二分查找、查找左边界的二分查找、查找右边界的二分查找三种类型。(第一种为最基础的二分查找,我们在学习C语言时曾经写过,后两者为反而是最常用的方法,但相比之下细节点也较多)
  
  
  
  
  

2、二分查找(easy)

  题源:链接

在这里插入图片描述

  
  

2.1、朴素的二分查找

  1)、暴力解法过渡到二分查找,说明二分查找为什么更高效

  暴力查找下,则需要从头到尾遍历一遍数组,找出满足条件的目标元素(nums[i] == target)。这种情况下时间复杂度为 O ( n ) O(n) O(n),一次只能排除一个元素。
在这里插入图片描述
  之所以说暴力查找慢,是因为它没有利用当前数组有序的特性。 我们找到一个中间值mid,根据条件可知,mid 左边的元素值均小于mid ([left, mid) < nums[mid] ),mid 右边的元素值均大于mid([left, mid) > nums[mid] )。因此,只需要将mid处的元素值和target比较,就可以知道target在哪一段区间中。
在这里插入图片描述

  mid和target做比较,情况有三:
  1、nums[mid] < target,说明[left,mid]区间内元素均 < target,可以排除。即有:left = mid +1,继续新一轮循环查找。
  2、nums[mid] > target,说明[mid,right]区间内元素均 > target,可以排除。既有:right = mid -1,继续新一轮循环查找。
  3、nums[mid] == target,说明该中间值正是目标元素,跳出循环返回结果。
  

  这样一次就能干掉一批元素。(如图,这就是区间二段性的体现,只不过不同题中,这里的特性规律会有不同。)

  
  二分查找的时间复杂度正是由此而来:(设有 n n n个元素)
   1 1 1 次查找,还剩 n 2 \frac{n}{2} 2n个元素;
   2 2 2 次查找,还剩 n 2 ∗ 1 2 = n 4 \frac{n}{2}*\frac{1}{2} = \frac{n}{4} 2n21=4n个元素;
   3 3 3 次查找,还剩 n 4 ∗ 1 2 = n 8 \frac{n}{4}*\frac{1}{2} = \frac{n}{8} 4n21=8n个元素;
   … … …… ……
   x x x 次查找,还剩 n 2 x \frac{n}{2^x} 2xn个元素。最坏情况下,最后一次查找剩余 1 1 1 个元素,则有 n 2 x = 1 \frac{n}{2^x} = 1 2xn=1,即 n = 2 x , x = l o g 2 n n = 2^x, x =log_2n n=2x,x=log2n

  即时间复杂度为: O ( n ) = l o g n O(n) = logn O(n)=logn
  
  
  

  2)、其它细节

  1、我们在找中值时,一般采取中间值,即(left + right )/2处,实际上mid不一定要在 1 2 \frac{1}{2} 21 位置处1/3、1/4等等位置处也可以,只是选择1/2位置能够减少查找次数,有助于保持查找过程的平衡性,避免因为划分不均而导致某些情况下的性能下降。(数学期望问题。)

  
  此外,相比于直接写成mid = (left + right )/2,为了避免left+right的值存在溢出,一般情况下都会选择写成:

mid = left + (right - left) /2;
mid = left + (right - left + 1) /2;

  由于向零取整,上述两种写法:
  ①对于元素个数为奇数(即right - left 的差是偶数时),计算结果一样。例如:下标为0、1、2,此时(2 - 0) / 2 = 1(2 - 0 + 1) / 2 = 1
  ②对于元素个数为偶数(即right - left 的差是奇数)时,前者 mid 偏向较小的那个索引,后者mid 偏向较大的那个索引。 在朴素的二分查找中,二者无区别,在后续的寻找左边界或寻找右边界的二分查找中,就需要留意写法。例如:下标为0、1、2、3,此时(3 - 0) / 2 = 1, (3 - 0 + 1) / 2 = 2
在这里插入图片描述

  
  
  
  

  2、循环结束条件:left > right,那么循环条件为:left <= right。
  问题:为什么此处 left == right 要算进去? left、right两指并非同时相向移动,每次循环只有其中一个指针移动,相向情况下,某一次循环势必会导致两指针碰撞,即left、right指向同一位置,而此时碰撞点处的元素还未进行相关判断,因此需要将其算入。
在这里插入图片描述
  
  
  3)、题解
  相关写法:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        
        int left = 0;int right = nums.size() - 1; // 初始化 left 与 right 指针

        while (left <=right) // 由于两个指针相交时,当前元素还未判断,因此需要取等号
        {
            int mid = left + (right - left) / 2; // 找到区间的中间元素

            if (nums[mid] < target)// 分三种情况讨论
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid - 1;
            else
                return mid;
        }
        return -1; // 如果程序⾛到这⾥,说明没有找到⽬标值,返回 -1
    }
};

  

  

  
  
  
  
  

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

  题源:链接

在这里插入图片描述
  
  

3.1、暴力解法 or 朴素的二分查找

  由于数组是按照非递减顺序排列,说明其中有区间段呈直线趋势。暴力解法下,从左到右遍历一次,首个target即目标左值,再从右到左遍历一次,首个target即目标右值。(图左)
在这里插入图片描述

  对于二分查找,若使用先前的那种最基础的二分查找方法,由于要找最左值和最右值,极端情况下,将会降为暴力查找。(图右)
  因此,我们要学习新模式下的二分查找。
  
  
  

3.2、寻找左边界 or 右边界的二分查找

3.2.1、寻找左边界的二分查找

  1)、寻找左边界的二分查找:
  无论是哪一种形式变化,二分查找的本质是要抓住区间的二段性,因两端区间的特性不同,从而能让left、right两指针进行条件挪动。
  
  以数组{1,2,2,3,3,3,5,6}举例。此例中,我们要寻找左边界,可将区间分为如下两段。用 resLeft 表示左区间的边界, resRight 表示右区间的边界,则 左边界的特点如下:

左边区间 [left, resLeft] 都是⼩于 x 的;
右边区间[resLeft, right] 都是⼤于等于 x 的;

在这里插入图片描述
  
  因此,mid 的落点有如下两种情况:

  1、当 mid 落在 [left, resLeft] 区间内,此时有 arr[mid] < target ,说明 [left, mid] 都是可以舍去的,此时更新 left 到 mid + 1 的位置,继续在 [mid + 1, right] 上寻找左边界;

  2、当 mid 落在 [resLeft, right] 区间内,此时有arr[mid] >= target ,说明 [mid + 1, right] 是可以舍去的(注意这里 mid 可能是最终结果,不能舍去),此时更新 right 到 mid 的位置,继续在 [left, mid] 上寻找左边界;

  3、由此,就可以通过二分来快速寻找左边界。

1)、x < target, 此时 left = mid + 1 ,继续在[mid+1,right]区间上找左边界;
2)、x >= target, 此时 right = mid, 继续在[left,mid]区间上找左边界。

在这里插入图片描述

  
  循环结束的条件判断:
  需要注意,后续这两种二分查找中,循环继续的条件为:left < right 为什么不像朴素二分查找一样使用 left <= right 呢?

  回答:此两种写法下,left = right时,就是我们要找的最终结果,无需判断。如果判断,就会死循环。

在这里插入图片描述

  
  
  如何求mid中值: 在选左边界的二分查找中,我们需要选哪一个作为运算 mid 表达式? 这里我们选取mid = left + (right - left) /2;,偏向于找左侧的那个元素比较合适。
在这里插入图片描述

  
  
  2)、一个模板:不建议死记硬背
  
  
  

3.2.2、寻找右边界的二分查找

  1)、寻找右边界的二分查找
  有了上述对左边界的认识,右边界理解起来也大同小异。
在这里插入图片描述
  右边界的特点:

左边区间 (包括右边界) [left, resLeft] 都是⼩于等于 x 的;
右边区间 [resRight, right] 都是⼤于 x 的;

  
  因此,mid 的落点有如下两种情况:

  1、当 mid 落在 [left, resLeft] 区间内,此时有 arr[mid] <= target ,说明 [left, mid -1] 是可以舍去的(注意这里 mid 可能是最终结果,不能舍去),此时更新 left 到 mid 的位置,继续在 [mid, right] 上寻找右边界;

  2、当 mid 落在 [resLeft, right] 区间内,此时有arr[mid] > target ,说明 [mid, right] 都是可以舍去的,此时更新 right 到 mid -1 的位置,继续在 [left, mid] 上寻找右边界;

  总结为:

1)、x <= target, 此时 left = mid,继续在[mid,right]区间上找右边界;
2)、x > target, 此时 right = mid + 1, 继续在[left,mid]区间上找右边界。

  
  循环继续的条件仍旧为:left < right

  但此处求mid中值的表达式应选择: mid = left + (right - left +1 ) / 2;,偏向于找右侧的那个元素比较合适。

在这里插入图片描述
  
  
  2)、一个模板:不建议死记硬背
  
  
  
  

3.2.3、题解

  此题要寻找左边界和右边界,上述两种方式的二分查找均要用到。

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {

        if(nums.empty())// 处理边界情况

            return {-1,-1};

        vector<int> ret;
        int left = 0; int right = nums.size()-1;

        //找左边界
        while(left < right)
        {
            int mid = left + (right - left ) / 2;
            if(nums[mid] < target)
                left = mid + 1;
            else // nums[mid] >= target
                right = mid;
        }
        if(nums[right] == target)
            ret.push_back(right);
        else ret.push_back(-1);

        //找右边界
        left = 0; right = nums.size()-1;
        while(left < right)
        {
            int mid = left + (right - left + 1) /2;
            if(nums[mid] > target)
                right = mid -1;
            else // nums[mid] <= target
                left = mid;
        }
        if(nums[left] == target)
            ret.push_back(left);
        else ret.push_back(-1);
    
        return ret;
    }
};

  
  
  
  
  
  
  

4、搜索插入位置(easy)

  题源:链接

在这里插入图片描述

  
  

4.1、题解

  说明:此题上述三种方法均可使用,只要理清楚其中逻辑和细节即可。
  
  1)、寻找左边界的二分查找:
在这里插入图片描述

  

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {

        if(nums.back() < target)//处理边界情况
            return nums.size();

        int left = 0; int right = nums.size()-1;
        while(left < right)
        {
            int mid = left + (right - left) /2;
            if(nums[mid] < target)
                left = mid + 1;
            else right = mid;
        }
        return left;// left == right 的时候, left 或者 right 所在的位置就是我们要找的结果。
    }
};

  
  
  
  
  2)、寻找右边界的二分查找:

在这里插入图片描述

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        
        if(nums[0] > target)//处理边界情况
            return 0;

        int left = 0; int right = nums.size()-1;
        while(left < right)
        {
            int mid = left + (right - left + 1) /2;
            if(nums[mid]<= target)
                left = mid;
            else right = mid -1;
        }

        //判断最终left\right指向位置
        if(nums[left] == target) return left;
        else return left + 1;
    }
};

  
  
  
  
  
  
  
  

5、x的平方根(easy)

  题源:链接

在这里插入图片描述

  

5.1、题解

  1)、暴力解法
  说明: 从 1 到 x 穷举所有数的平方。如果 i * i == x ,直接返回 x ;如果 i * i > x ,说明之前的⼀个数是结果,返回 i - 1 。
  需要注意,由于 i * i 可能超过 int 的最⼤值,因此此处使用用 long long 类型比较合适。

class Solution {
public:
    int mySqrt(int x) { 
        long long i = 0;// 由于两个较⼤的数相乘可能会超过 int 最⼤范围,因此⽤ long long
        for (i = 0; i <= x; i++) 
        {
            if (i * i == x)// 如果两个数相乘正好等于 x,直接返回 i
                return i;

            if (i * i > x) // 如果第⼀次出现两个数相乘⼤于 x,说明结果是前⼀个数
                return i - 1;
        }
       
        return -1; // 为了处理oj题需要控制所有路径都有返回值
    }
};

  
  
  2)、二分查找
  说明: 结果只保留整数部分,根据题可知,是按照向下取整获取结果的,因此,此题使用寻找右边界的二分查找相对方便(也可以使用另外的二分查找,明确边界、循环结束条件等即可)。
在这里插入图片描述

  
  题中 0 < = x < = 2 31 − 1 0 <= x <= 2^{31} - 1 0<=x<=2311,下述不必单独将0拎出讨论,由于0不满足left < right的条件判断,会直接返回结果,而其正好是0。

class Solution {
public:
    int mySqrt(int x) {
        long long left = 0; long long right = x;
        while(left < right)
        {
            long long mid = left + (right - left + 1) /2;
            if(mid*mid <= x)
                left = mid;
            else right = mid -1;
        }
        return left;
    }
};

  
  
  
  

6、山峰数组的峰顶(easy)

  题源:链接

在这里插入图片描述

  

6.1、题解

  1)、暴力解法
  根据题目可知,峰顶的特点是该位置的元素比两侧的元素都要大。因此,我们可以遍历数组内的每一个元素,找到某一个元素比两边的元素大即可。

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int n = arr.size();
        // 遍历数组内每⼀个元素,直到找到峰顶
        for (int i = 1; i < n - 1; i++)
            // 峰顶满⾜的条件:两侧元素小于山顶元素
            if (arr[i] > arr[i - 1] && arr[i] > arr[i + 1])
                return i;
        return -1;// 找不到
    }
};

  
  
  
  2)、二分查找
  虽然此题中数组不是单调排列的,但仍旧具有二段性,可以使用二分查找。这也侧面说明二分查找不需要数组有序。
在这里插入图片描述
  将区间分为两端,根据 mid 落下的位置,可以分为下述三种情况:
  1、若mid落在左段,此时 mid 位置呈现上升趋势(mid < mid + 1),在mid左侧的元素不会比mid大。因此,接下来可以在 [mid + 1, right] 区间继续搜索;
  2、若 mid 落在右段,此时 mid 位置呈现下降趋势(mid > mid + 1 ),在mid右侧的元素不会比mid大,说明我们接下来要在 [left, mid - 1] 区间搜索;
  3、如果 mid 位置就是山峰,直接返回结果。
  
  这里使用左边界的二分查找或右边界的二分查找都可以,也可以按照上述将分三种情况来讨论。

在这里插入图片描述
  
  朴素的二分查找写法:

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int left = 0; int right = arr.size() -1;
        while(left <= right)
        {
            int mid = left + (right - left) /2;
            if(arr[mid] > arr[mid+1] && arr[mid] > arr[mid -1])
                return mid;
            else if(arr[mid] < arr[mid + 1])//左山坡:上升趋势
                left = mid + 1;
            else right = mid -1;//右山坡:下降趋势
        }
        return -1;//这里是为了完善OJ的输出,题目告知了 arr 一定是山脉数组
    }
};

  使用左边界的二分查找写法:

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int left = 0;int right = arr.size()-1;
        while(left < right)
        {
            int mid = left + (right - left) /2;
            if(arr[mid] < arr[mid+1]) //左山坡
                left = mid + 1;
            else right = mid;//右山坡
        }
        return left;
    }
};

  
  
  
  
  
  

7、寻找峰值(medium)

  题源:链接

在这里插入图片描述

  
  

7.1、题解

在这里插入图片描述

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

  
  
  
  
  
  
  

8、搜索旋转排序数组中的最小值(medium)

  题源:链接

在这里插入图片描述

  
  

8.1、题解

  
在这里插入图片描述

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0, right = nums.size() - 1;
        int x = nums[right]; // 标记⼀下最后⼀个位置的值
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > x)
                left = mid + 1;
            else
                right = mid;
        }
        return nums[left];
    }
};

  
  
  
  
  
  
  

9、0~n-1中缺失的数字(easy)

  题源:链接

在这里插入图片描述

  
  

9.1、题解

  此题常见解法:①哈希表;②直接遍历;③位运算;④高斯求和;⑤二分查找。
  
  
  这里我们以二分查找为主:关键点在于如何获得区间的二段性。
在这里插入图片描述

class Solution {
public:
    int takeAttendance(vector<int>& records) {

        int left = 0; int right = records.size()-1;
        while(left < right)
        {
            int mid = left + (right -left) /2;
            if(mid ==records[mid])
                left = mid +1;
            else right = mid;
        }
        return left == records[left] ? left + 1 : left;
    }
};

  
  
  
  

10、等差数列中缺失的数字(easy)

  题源:链接

在这里插入图片描述

  
  

10.1、题解

  暴力解法只用遍历数组,查找元素是否满足等差数列条件即可。如:
  1、 a n + 1 − a n = a n − a n − 1 a_{n+1} -a_n = a_{n} -a_{n-1} an+1an=anan1 ,对比当前项与前后两个项的差等于同一个常数。
  2、 a n + d = a n + 1 a_n + d= a_{n+1} an+d=an+1,先根据首位元素获取公差,再判断当前项与后一项是否满足公差,若不满足,则后一项为返回值。
  3、 a n = a 1 + ( n − 1 ) × d a_n = a_1 + (n-1)×d an=a1+(n1)×d,根据公式,遍历数组,与首项计算结果做比较。
  
  
  二分查找同理,关键在于如何想到判断条件,使得leftright分别移动的。
在这里插入图片描述

class Solution {
public:
    int missingNumber(vector<int>& arr) {

        int d =(arr.back()-arr.front())/(int)arr.size();
        //这里数组长度要加上缺少的那一个元素,此外vector::size()返回值是unsigned int,此处运算存在整值提升(公差为负数会出问题)。

        int left = 0; int right = arr.size()-1;
        while(left < right)
        {
            int mid = left + (right - left) /2;
            if(arr[0]+mid*d == arr[mid])
                left = mid + 1;
            else right = mid;
        }
        return arr[left]-d;
    }
};


  
  
  
  
  
  
  
  

Fin、共勉。

在这里插入图片描述

  • 18
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值