【算法】二分查找

【ps】本篇有 8 道 LeetCode OJ

目录

一、算法简介

二、相关题目

1)基础的二分查找

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

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

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

3)搜索插入位置

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

[4)x 的平方根](#4)x 的平方根)

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

5)山脉数组的峰顶索引

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

6)寻找峰值

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

7)寻找旋转排序数组中的最小值

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)

8)点名

[.1- 题目解析](#.1- 题目解析)

[.2- 代码编写](#.2- 代码编写)


一、算法简介

二分查找是一种高效的查找方法,其时间复杂度为 O(logN),主要针对于有序的顺序存储结构(无序有时也行,但是要有二段性),使用时的细节较多,但使用的套路相比其他算法更容易被摸清。

二、相关题目

1)基础的二分查找

704. 二分查找 - 力扣(LeetCode)

.1- 题目解析

这是一道基础的二分查找题目,我们借由这道题来详细讲述二分查找的细节,和总结二分查找的算法模板。

如果使用暴力解法,将所有数组元素跟 target 比较,确实能够解出答案,但其时间复杂度为 O(n)。通过观察不难发现,要在这个升序数组中查找 target ,target 就会将这个升序数组分为两部分,target 的左边全是比 target 小的数,target 的右边全是比 target 大的数,此时这个升序数组就具备了二段性,也就是二分查找所必要的性质。

所谓二分,也就是将一个区间不断地分成两部分,使其中某一部分不断逼近期望的结果。

要实现对一个区间进行划分的操作,一般要用到三个指针:

  • left:位于区间左端。
  • right:位于区间右端。
  • mid:始终位于 left 和 right 的中间位置。

对于本题而言,这三个指针会将升序数组划分成两个子数组,其中,mid 的左侧都是比 mid 小的数,mid 的右侧都是比 mid 大的数,此时只需确定 target 是比 mid 小,还是比 mid 大,即可进行下一步划分------如果 target 比 mid 小,说明 target 在 mid 左侧的子区间中,就需要继续对 mid 左侧的子区间进行划分,具体的方式是让此时的 mid 作为新的 right,left 不变,然后用 left 和 right来确定新的 mid;如果 target 比 mid 大,说明 target 在 mid 右侧的子区间中,就需要继续对 mid 右侧的子区间进行划分,具体的方式是让此时的 mid 作为新的 left,right 不变,然后用 left 和 right来确定新的 mid------从而逐步找到 target。

【Tips】基础的二分模板

cpp 复制代码
		while(left<=right) //二分的结束条件
		{
			int mid=left+(right-left)/2; //防溢出的写法
			if(...)
                left=mid+1;
			else if(...)
                right=mid-1;
			else 
                return ...; 
		}

【Tips】关键步骤:

  1. 找出区间的二段性;
  2. 根据二段性,确定 left 和 right 的调整条件与其相应的调整方式;
  3. 确定 mid 的计算式;
  4. 确定二分的结束条件。

.2- 代码编写

cpp 复制代码
class Solution {
public:
    int search(vector<int>& nums, int target) {
        //1.定义分别指向区间两段的 left 和 right 指针
		int left=0,right=nums.size()-1;
        //2.进行二分查找
		while(left<=right) //二分的结束条件
		{
            //1)确定新的mid
			int mid=left+(right-left)/2; 
            //2)根据mid所指的值和target的大小关系,确定要继续二分的子区间
			if(nums[mid]<target)left=mid+1;
			else if(nums[mid]>target)right=mid-1;
			else return mid; //当mid所指的值和target相等时,则说明此时target的下标即为mid
		}
		return -1;
    }
};

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

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

.1- 题目解析

由题,要返回一个升序数组中所有值为 target 的元素区间的起始下标和结束下标。

由于这是一个升序数组,那么所有值为 target 的元素会在数组中构成一个子区间,因此,本题既需要在找到子区间的左端点,又要找到子区间的右端点。

对于子区间的左端点,升序数组能够被 target 分为两部分,一部分是小于 target 的数,另一部分是大于等于 target 的数。

由此,继续划分子区间时,left 和 right 的调整方式应为;

那么, mid 的计算式其实应为 left + (right - left) / 2,这样,当数组长度为偶数时,可以保证 mid 落在所有值为 target 的元素区间的左端点上。

而二分的结束条件也应为 left < right,而非 left <= right。这是因为,在 left 和 right 的调整方式中,left 的移动始终会跳出小于 target 的区间,right 的移动始终在大于等于 target 的区间上,当 left == right, left 和 right 会指向同一个位置,即 target 的位置。

对于子区间的右端点,同理,升序数组能够被 target 分为两部分,一部分是小于等于 target 的数,另一部分是大于 target 的数。

由此,继续划分子区间时,left 和 right 的调整方式应为;

同理,mid 的计算式应为 left + (right - left + 1) / 2 ,而二分的结束条件也应为 left < right。

.2- 代码编写

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.size()==0) return {-1,-1};
        //二分查找左端点
        int left=0,right=nums.size()-1,begin=0;
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(nums[mid]<target) left=mid+1;
            else right=mid;
        }
        //保存左端点
        if(nums[left]!=target)return {-1,-1};
        else begin=left;
        //二分查找右端点
        left=0,right=nums.size()-1;
        while(left<right)
        {
            int mid=left+(right-left+1)/2;
            if(nums[mid]<=target) left=mid;
            else right=mid-1;
        }
        //返回左右端点
        return {begin,left};
    }
};

3)搜索插入位置

. - 力扣(LeetCode)35. 搜索插入位置. - 力扣(LeetCode)

.1- 题目解析

由题,这个升序数组可以被 target 划分为两部分,一部分是小于 target 的数,另一部分是大于等于 target 的数。

由此可得:

  • left 和 right 的调整条件:target 在 mid 的右边,则调整 left;target 位于 mid 或在 mid 的左边,则调整 right。
  • left 和 right 的调整方式:left = mid + 1;right = mid。
  • mid 的计算式:left + (right - left) / 2,落在左端点。
  • 二分的结束条件:left < right;当 left == right 时,left、right、mid 重叠位于 target 或 target 左侧。

.2- 代码编写

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

4)x 的平方根

69. x 的平方根 - 力扣(LeetCode)

.1- 题目解析

求非负整数 x 的平方根,其实可以转化为求一个数的平方等于 x。

假设 x 的平方根为 mid,那么 mid 一定是一个在 [0,x] 区间上的数,且这个区间天然是升序的,mid 也能够将这个区间划分为 [0,mid) 和 (mid,x] 两个子区间。

至此,本题就转化成了,在 [0,x] 区间上找一个数的平方等于 x,可以使用二分查找来解题。

这里,0 的平方根一定是 0,0 的平方也一定是 0,因此 0 作特别处理。

由题目示例,mid 的平方一定是小于等于 x 的,因此,mid 的平方可以将 [1,x] 区间划分为两个部分,一部分是小于等于 mid 的平方的数,另一部分是大于 mid 的平方的数。

由此可得:

  • left 和 right 的调整条件:x 位于 mid 的平方或在 mid 平方的右边,则调整 left;x 在 mid 的平方的左边,则调整 right。
  • left 和 right 的调整方式:left = mid;right = mid - 1。
  • mid 的计算式:left + (right - left + 1) / 2,落在右端点。
  • 二分的结束条件:left < right;当 left == right 时,left、right、mid 重叠。

.2- 代码编写

cpp 复制代码
class Solution {
public:
    int mySqrt(int x) {
		if(x<1)return 0; //特别处理
		int left=1,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;
    }
};

5)山脉数组的峰顶索引

852. 山脉数组的峰顶索引 - 力扣(LeetCode)

.1- 题目解析

由题,在一个数组中,存在一个数是数组中最大的,且它左边的数是升序排列的,它右边的数则是降序排列的,题目要求找出这个数并返回这个数的下标。

显然,这样一个数组是具有二段性的,可以被其中的最大值分为两个部分,一部分是小于等于它的数,另一部分是大于它的数。

设 left 为升序序列的左端点,right 为降序序列的右端点,mid 是 left 和 right 的中间位置,由此可得:

  • left 和 right 的调整条件:mid 的值大于 mid - 1,说明 mid 处于升序序列中,则需调整 left 来继续划分;mid 的值小于 mid - 1,说明 mid 处于降序序列中,则需调整 right 来继续划分。
  • left 和 right 的调整方式:left = mid;right = mid - 1。
  • mid 的计算式:left + (right - left + 1) / 2,落在右端点。
  • 二分的结束条件:left < right;当 left == right 时,left、right、mid 重叠。

.2- 代码编写

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

6)寻找峰值

162. 寻找峰值 - 力扣(LeetCode)

.1- 题目解析

符合题目要求的峰值一共有三种情况:

  1. 峰值位于一共升序序列和降序序列之间;
  2. 峰值位于一个升序序列的末尾;
  3. 峰值位于一个降序序列的起始。

对这三种情况进一步分析,就会发现它们都存在二段性:

对于"峰值位于一共升序序列和降序序列之间"的情况,自然不必多讨论,一定是具有二段性的;对于一个升序序列,序列的右侧一定存在峰值,左侧则不一定存在峰值;而对于一个降序序列,序列的左侧一定存在峰值,右侧则不一定存在峰值。

总结一下:升序序列的右侧一定存在峰值,降序序列的左侧一定存在峰值。

那么,我们就可以用二分查找算法来解题了。当前值大于后值时,即序列为降序,则降序序列左侧有峰值,继续去序列左侧进行二分;当前值小于后值时,即序列为升序,则升序序列右侧有峰值,继续去序列右侧进行二分。

.2- 代码编写

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

【Tips】 mid 的计算式中是否 + 1,与 left 调整时是否 + 1、right 调整时是否 - 1 有关。一般可以记忆为,若 left = mid + 1 则计算 mid 不 + 1,若 right = mid - 1 则计算 mid 要 + 1。

7)寻找旋转排序数组中的最小值

153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)

.1- 题目解析

所谓的旋转,其实就是依此将原数组末尾的数放到数组的头部。

由于原数组是升序的,因此旋转后的数组中将包含两个升序子数组,即具有了二段性,且特别的,前一个升序的子数组中所有数,都大于后一个升序的子数组中所有数。

而这个旋转数组中的最小值,其实就是后一个升序子数组中的第一个数,因此只需用二分查找找到后一个升序子数组中的第一个数即可。

.2- 代码编写

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

8)点名

LCR 173. 点名 - 力扣(LeetCode)

.1- 题目解析

由题,缺席的同学,其学号没有在升序数组中,在缺席的学号之前,都是数组元素的值等于元素下标,也就是说,学号其实是数组元素的值,只要元素的值等于元素下标,就说明该学号没有缺席。

而从缺席的学号开始,所有元素的值都不等于元素下标了,也就是说,如果元素的值不等于元素下标,就说明这个元素下标对应的学号是缺席的,且缺席的学号应该是第一个不等于元素值的元素下标。

因此,这个升序数组其实是具有二段性的,它被缺席的学号划分为两个子数组,前一个子数组的元素值都等于元素下标,后一个数组的元素值都不等于元素下标。那么,我们只需找到后一个子数组中的首元素,即只需找到第一个不等于元素值的元素下标。

但本题存在一个特殊情况,那就是所有学号都未缺席,例如:

此时应返回的是,数组尾部的下一个位置,因为数组尾部的下一个位置在数组中是缺席的。

.2- 代码编写

cpp 复制代码
class Solution {
public:
    int takeAttendance(vector<int>& records) {
        int left=0,right=records.size()-1;
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(mid==records[mid])left=mid+1;
            else right=mid;
        }
        if(left==records[left])return left+1;//如果 left==right,说明当前整个数组中的学号都没缺席,因此要返回下一个位置
        return left;
    }
};
相关推荐
不白兰几秒前
[代码随想录23回溯]回溯的组合问题+分割子串
算法
御风@户外1 小时前
质数生成函数、质数判断备份
算法·acm
Cosmoshhhyyy1 小时前
LeetCode:3083. 字符串及其反转中是否存在同一子字符串(哈希 Java)
java·leetcode·哈希算法
羑悻的小杀马特1 小时前
【AIGC篇】畅谈游戏开发设计中AIGC所发挥的不可或缺的作用
c++·人工智能·aigc·游戏开发
闻缺陷则喜何志丹1 小时前
【C++动态规划】1105. 填充书架|2104
c++·算法·动态规划·力扣·高度·最小·书架
Dong雨1 小时前
六大排序算法:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序
数据结构·算法·排序算法
析木不会编程1 小时前
【C语言】动态内存管理:详解malloc和free函数
c语言·开发语言
达帮主1 小时前
7.C语言 宏(Macro) 宏定义,宏函数
linux·c语言·算法
是十一月末2 小时前
机器学习之KNN算法预测数据和数据可视化
人工智能·python·算法·机器学习·信息可视化
chenziang12 小时前
leetcode hot100 路径总和
算法