当前位置: 首页 > news >正文

专题三 之 【二分查找】

目录

1.二分查找的简介

2.例题

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

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

2.3 LCR 068. 搜索插入位置 - 力扣(LeetCode)

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

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

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

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

​编辑

2.8 LCR 173. 点名 - 力扣(LeetCode)


1.二分查找的简介

当查找的数据将整堆数据划分为两部分,即数据具有 二段性

二分查找就可以很好的解决问题,并不只有有序数组才能使用二分查找算法

  • 朴素二分查找模板
        //朴素二分查找int left = 0, right = nums.size() - 1;while(left <= right){//中间点的取值,向上或向下取整均可int midi = left + (right - left + 1) / 2;//核心移动逻辑if(......) right = midi - 1;else if(.....) left = midi + 1;else return .....;}
  • 二分查找左边界模板
        int left = 0, right = nums.size() - 1;while(left < right)//结束条件,分情况证明{int midi = left + (right - left) / 2;if(....) right = midi;else left = midi + 1;} 
  • 二分查找右边界模板
        int left = 0, right = nums.size() - 1;//左端点可以不动while(left < right){int midi = left + (right - left + 1) / 2;if(.....) right = midi - 1;else left = midi;}

根据二段性理解记忆上述模板,下面通过例题来初步感受二分查找算法

2.例题

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

方法一:暴力查找,时间复杂度为O(N)

方法二:根据二段性,使用二分查找算法

二段性:要查找的值target将数组分为了三部分

(1)横线代表数组nums,目标值为target

(1) left、right 分别指向数组的两端,mid 为 left 和 right 的中点

(1)

  • 当nums[ mid ] > target 时,由于数组是升序数组,target只可能存在于 [left,mid-1]中,此时更新 right = mid - 1,继续比较nums[ mid ] 和 target 的大小关系
  • 当nums[ mid ] < target 时,由于数组是升序数组,target只可能存在于 [mid+1,right]中,此时更新 left = mid + 1,继续比较nums[ mid ] 和 target 的大小关系
  • 当nums[ mid ] == target 时,返回 mid 即可
  • 区间不断缩小,当区间还剩下一个数时,仍需继续比较,直到left > right 时,查找结束
class Solution
{
public:int search(vector<int>& nums, int target){//使用二分查找的前提是:找到二段性//根据某种条件可以将数组分为两部分,舍弃一部分,再在另一部分中查找int left = 0, right = nums.size() - 1;while(left <= right)//只有一个数也需要进行判断{//中间点的取值,左右均可int midi = left + (right - left + 1) / 2;//核心移动逻辑if(nums[midi] > target) right = midi - 1;else if(nums[midi] < target) left = midi + 1;else return midi;}return -1;}
};

细节问题

(1) 中间值的计算方法

midi = (left + right) / 2   --------------------1

midi = (left + right + 1) / 2  ----------------2

midi = left + (right - left) / 2; --------------3

midi = left + (right - left + 1) / 2; ---------4

当数据个数为奇数时,上述计算方法得到的值相同

当数据个数为偶数时,1、3两种方法向下取整,2、4两种方法向上取整。例如1 2 3 4 四个数,left 指向1,下标为0,right 指向4,下标为3,此时 1、3两种方法向下取整 midi指向2,2、4两种方法向上取整,midi指向3

当数据两较大时,3、4两种方法还具有防溢出的好处

(2) 二分查找需要不断比较nums[ mid ] 和 target 的大小关系,所以是一个循环,同时循环结束条件应当是left <= right,因为当区间还剩下一个数时,我们还能进行一次查找判断

(3)朴素的二分查找,向上取整、向下取整均可,因为当nums[ mid ] != target时,left、right不会固定不动(可以根据下面两种模板再来理解这句话)

查找一次区间长度缩小一半,设查找了m次,一共有N个数据,则时间复杂度为O(m),又因为2^m = N,  m = logN,所以二分查找时间复杂度为O(logN)

通过严格的数学证明,二分查找效率优于n分查找(n > 2)感兴趣的朋友可以参考算法导论

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

方法一:暴力求解,直接遍历查找,时间复杂度O(N)

方法二:根据二段性使用二分查找算法,时间复杂度正好O(logN)

查找左边界

二段性:要查找的值target将数组分为了两部分

(1)横线代表数组nums,目标值为target

(1) left、right 分别指向数组的两端,mid 为 left 和 right 的中点

(1)

  • 当nums[ mid ] >= target 时,由于数组是非递减数组,target只可能存在于 [left,mid]中,此时更新 right = mid,继续比较nums[ mid ] 和 target 的大小关系
  • 当nums[ mid ] < target 时,由于数组是非递减数组,target只可能存在于 [mid+1,right]中,此时更新 left = mid + 1,继续比较nums[ mid ] 和 target 的大小关系
  • 区间不断缩小,当区间还剩下一个数时,跳出循环进行判断

细节问题

(1)中点midi的求法:midi = left + (right - left) / 2  向下取整,这是因为若向上取整

当区间个数为2,且nums[ mid ] >= target,right = mid的移动规则导致right根本没移动,最终进入死循环。例如3 4两个数,target = 0,left = 0,指向3,right =1,指向4,向上取整,midi = 1,指向4, 根据移动规则right = midi = 1,这不就进入死循环了吗。反之向下取整,midi = 0,不管命中哪个判断条件,区间长度都在缩小,最终达到循环结束条件

(2) 循环结束条件

整合上述情况,最终当区间长度为1时,选择跳出循环判断查找结果

查找右边界

二段性:要查找的值target将数组分为了两部分

(1)横线代表数组nums,目标值为target

(1) left、right 分别指向数组的两端,mid 为 left 和 right 的中点

(1)

  • 当nums[ mid ] <= target 时,由于数组是非递减数组,target只可能存在于 [mid,right]中,此时更新 left= mid,继续比较nums[ mid ] 和 target 的大小关系
  • 当nums[ mid ] > target 时,由于数组是非递减数组,target只可能存在于 [left,mid-1]中,此时更新 right = mid - 1,继续比较nums[ mid ] 和 target 的大小关系
  • 区间不断缩小,当区间还剩下一个数时,跳出循环进行判断

细节问题

(1)中点midi的求法:midi = left + (right - left) / 2  向上取整,这是因为若向下取整

当区间个数为2,且nums[ mid ] <= target,left = mid的移动规则导致left根本没移动,最终进入死循环。例如3 4两个数,target = 4,left = 0,指向3,right =1,指向4,向下取整,midi = 0,指向3, 根据移动规则left = midi = 0,这不就进入死循环了吗。反之向上取整,midi = 1,不管命中哪个判断条件,区间长度都在缩小,最终达到循环结束条件

(2) 循环结束条件

整合上述情况,最终当区间长度为1时,选择跳出循环判断查找结果

class Solution
{
public:vector<int> searchRange(vector<int>& nums, int target){//处理边界情况if(nums.size() == 0) return {-1, -1};int begin = 0;int left = 0, right = nums.size() - 1;//左端点  二段性    <target    >=target//细节问题:1.循环结束条件   无需判断、有可能进入死循环//2.求中点   区间可能不动while(left < right)//结束条件,分情况证明{int midi = left + (right - left) / 2;//右边界不动,向左靠if(nums[midi] >= target) right = midi;else left = midi + 1;}    if(nums[left] != target) return {-1, -1};else begin = left;//右端点  二段性    >=target    <targetleft = 0, right = nums.size() - 1;//左端点可以不动while(left < right){int midi = left + (right - left + 1) / 2;if(nums[midi] > target) right = midi - 1;else left = midi;}//有左端点,一定就有右端点return {begin, right};}
};

right处于向上取整位置,left处于向下取整位置

在二分查找左右边界的模板中,midi的求法可以根据“谁不变,取相反”的口诀进行记忆

2.3 LCR 068. 搜索插入位置 - 力扣(LeetCode)

方法一:遍历查找,时间复杂度O(N)

方法二:根据二段性使用二分查找算法

仔细分析,target将数组分为两部分,即二分查找左边界

细节问题:

三种情况中,前两种情况跳出循环后,下标就是插入或存在下标,当数组元素全小于target时,插入下标应该是 left + 1

class Solution {
public:int searchInsert(vector<int>& nums, int target) {//二分查找左端点int left = 0, right = nums.size() - 1;while(left < right){int midi = left + (right - left) / 2;if(nums[midi] < target) left = midi + 1;else right = midi;}//全小,来到数组的最后一个位置,但是应该存放在最后一个位置的下一个位置//其余情况,找得到,就返回下标,找不到,正好是比target大的第一个数的位置下标if(nums[left] < target) return left + 1;else return left;}
};

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

方法一:从前到后暴力比较每一个中,时间复杂度O(N)

方法二:根据二段性使用二分查找

仔细分析,这里算术平方根是向下取整,查找的数将[0, x]区间分为两部分,

查找平方等于x或者平方小于x的数中的最大的数,即二分查找右边界

细节问题:int类型的数进行平方可能会溢出

class Solution 
{
public:int mySqrt(int x){//所找的数的平方小于等于x//二分查找,右端点   <=  >//越界问题long long left = 0, right = x;while(left < right){long long midi = left + (right - left + 1) / 2;if(midi * midi <= x) left = midi;else right = midi -1;}return left;}
};

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

方法一:遍历查找,时间复杂度O(N)

方法二:根据二段性使用二分查找算法

上下坡不就是典型的二段性吗

山峰的左边,nums[i] < nums[i+1];   山峰的右边,nums[i] > nums[i+1];

class Solution {
public:int peakIndexInMountainArray(vector<int>& arr) {//二分查找右边界int left = 0, right = arr.size() - 1;while(left < right){int midi = left + (right - left + 1) / 2;if(arr[midi] < arr[midi - 1]) right = midi - 1;else left = midi;}return left;}
};
class Solution {
public:int peakIndexInMountainArray(vector<int>& arr) {//二分查找左边界int left = 0, right = arr.size() - 1;while(left < right){int midi = left + (right - left) / 2;if(arr[midi] < arr[midi + 1]) left = midi + 1;else right = midi;}return left;}
};

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

方法一:根据峰值特点,分下面三种情况,直接遍历查找,时间复杂度O(N)

上下坡正好具有二段性,二分查找正好适用

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

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

方法一:直接遍历查找最小值,时间复杂度O(N)

可以将旋转数组抽象为上述两种图形,要么升序排列,要么间断升序排列

通过判断首尾元素的大小就可以区分两种情况,升序排列直接返回首元素,避免下面的判断

间断升序排列时,1区域元素恒大于2区域元素,这不就是二段性

  • 以B为target,最小值左边元素恒大于B,包含最小值在内的右边元素恒小于等于B

nums[midi] > target,即midi落在1区域时,应当更新 left = midi + 1

nums[midi] <= target,即midi落在2区域时,应当更新 right = midi

  • 以A为target,最小值左边元素恒大于等于A,包含最小值在内的右边元素恒小于A

nums[midi] >= target,即midi落在1区域时,应当更新 left = midi + 1

nums[midi] < target,即midi落在2区域时,应当更新 right = midi

class Solution {
public:int findMin(vector<int>& nums) {//经过旋转之后的数组,首大于尾int n = nums.size();if(nums[0] < nums[n - 1]) return nums[0];//二段性int left = 0, right = n - 1, target = nums[0];while(left < right){int midi = left + (right - left) / 2;if(nums[midi] >= target) left = midi + 1;else right = midi;}return nums[left];}
};

2.8 LCR 173. 点名 - 力扣(LeetCode)

方法一:哈希表存入0~n-1个值,遍历数组,存在即删除,最后剩余的数就是缺失的数

方法二:从0开始遍历,序号与数组元素对不上时,序号就是缺失的数

方法三:将数组元素和0~n-1异或在一起,异或的结果就是缺失的数

方法四:高斯求和,再减去数组元素,剩下的值就是缺失的数

方法五:根据方法二下标与数组元素的对应关系可知,要查找的数的左边的所有元素与其下标一一对应,右边的所有元素比其下标大一

class Solution {
public:int takeAttendance(vector<int>& records) {//下标划分二段性int left = 0, right = records.size() - 1;while(left < right)//二分查找右端点{int midi = left + (right - left + 1) / 2;if(records[midi] == midi) left = midi;else right = midi - 1;}if(records[left] == left)//找到了端点return left + 1;else return left;}
};
class Solution {
public:int takeAttendance(vector<int>& records) {//下标划分二段性int n = records.size();int left = 0, right = n - 1;while(left < right)//二分查找左端点{int midi = left + (right - left) / 2;if(records[midi] != midi) right = midi;else left = midi + 1;}if(records[left] == left)//找到了端点return left + 1;else return left;}
};

二分查找右端点的最终结果

(1)数组元素与下标一致,缺失的数等于left+1

(2)数组元素与下标不一致,缺失的数等于left

二分查找左端点的最终结果

(1)数组元素与下标一致,缺失的数等于left+1

(2)数组元素与下标不一致,缺失的数等于left

http://www.dtcms.com/a/549565.html

相关文章:

  • C++进阶: override和final说明符-----继承2中重写的确认官和刹车(制动器)
  • 数据科学每日总结--Day7--数据库
  • opencv 学习: 01 ubuntu20.04 下 opencv 4.12.0 源码编译
  • 满足“国六”标准的通用型故障诊断仪:Q-OBD
  • 上海专业建站公湖南网站建设设计
  • 智慧时空大数据平台:释放时空信息数据价值
  • 线程基本概念
  • MySQL MDL锁阻塞DDL 导致复制线程卡住
  • 智慧管理,赋能美容院新未来
  • Flink做checkpoint迟迟过不去的临时解决思路
  • 网站注册 优帮云wordpress首页静态化
  • [人工智能-大模型-115]:模型层 - 用通俗易懂的语言,阐述神经网络为啥需要多层
  • Actix Web 不是 Nginx:解析 Rust 应用服务器与传统 Web 服务器的本质区别
  • pdf文件上传下载记录
  • 辽阳网站设计中国建设银行的网站.
  • 2. WPF程序打包成一个单独的exe文件
  • 东软专业力考试--Java Web 开发基础
  • 8方向控制圆盘View
  • js中Map和对象{}的区别
  • 基于python构建的低温胁迫实验
  • 服装公司网站修改wordpress后台登陆
  • 2025 Avalonia 技术全景:从版本迭代到生产级落地的成熟之路
  • 做网站却不给客户源代码奥迪互动平台
  • python基础一
  • Burp Suite 代理切换插件
  • 怎么做企业网站推广网站推广方案
  • Jaccard相似度:集合相似性的经典度量
  • 十七、STM32的TIM(八)(TIM输入捕获)
  • c语言笔记 格式化输出函数的使用
  • 网络营销网站建设哪家好北京发布重磅消息