算法之二分查找算法

二分查找算法简介

1. 首先说明二分查找算法是比较恶心, 细节很多, 很容易写出死循环的算法, 但熟悉了之后是最简单的算法.

2. 其次我们可能听说过二分查找的前提是数组有序的前提下进行, 但其实不一定.

3. 二分查找算法有一套模板:

  • 朴素的二分模板: 比较简单, 但是有局限性
  • 查找左边界的二分模板, 查找右边界的二分模板: 万能模板, 但是细节很多.

二分查找算法的关键是"二段性" , 当我们发现一个规律, 根据这个规律能把这个数组分为两部分, 根据规律能有选择性的舍去一部分, 进而能在另一个部分继续查找, 可以看到这其中并没有提到数组是否有序, 关键在于数组是否有"二段性". 

此外, 我们对于选择区间划分点mid的位置也并没有具体的描述必须选择在中间点, 三分之一点, 四分之一点....都可以, 因为只要我们找到的这个位置能把区间分为两部分即可, 即数组有二段性即可.

但是选择中间点作为划分点时间复杂度是最好的.


题目1: 二分查找

朴素二分查找的步骤:

a. 定义 leftright 指针, 分别指向数组的左右区间.
b. 找到待查找区间的中间点 mid , 找到之后分三种情况讨论:

朴素二分查找核心:

1. arr[mid] == target 说明正好找到, 返回 mid 的值;

2. arr[mid] > target 说明 [mid, right] 这段区间都是大于 target 的,因此舍去右边区间, 在左边 [left, mid -1] 的区间继续查找,即让 right = mid -1 ,然后重复 2 过程;
3. arr[mid] < target 说明 [left, mid] 这段区间的值都是小于 target 的, 因此舍去左边区间, 在右边 [mid + 1, right] 区间继续查找, 即让 left = mid +1 ,  然后重复 2 过程;

c. 当 left 与 right 错开时, 说明整个区间都没有这个数, 返回 -1 .

相关细节问题: 

1. 循环结束的条件: left>right, 也就是这个区间不存在的时候, 说明没找到; 注意: 当left==right的时候循环没结束, 此时代表区间内只有一个数, 这个数也是要判断的.

2. 为什么时间复杂度低? 

程序执行次数 1次 ->  区间长度n/2, 2次->n/2^2, 3次->n/2^3, ..., x次->n/2^x, 假设在第x次区间长度为1, 也就最坏的情况下找到了这个数, 那么n/2^x = 1 -> x = logn, 所以时间复杂度为O(logn).

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) left = mid + 1;else if(nums[mid] > target) right = mid -1;else return mid;}return -1;//没找到}
};

 关于朴素二分查找的模板:

    while(left <= right){int mid = left + (right-left)/2;if(...)left = mid + 1;else if(...)right = mid -1;elsereturn ...;}

其中...根据题目所分析出的二段性进行填写. 其中两个细节:

1. 条件判断为left<=right.

2. 求中间点的方式为left +(right-left)/2防溢出.

3. 朴素二分查找求中间点用 left+(right-left)/2 和 left+(right-left+1)/2 没有差别, 这两种求中间点的方法在区间长度为奇数的时候, 求出的mid是一样的, 唯一的区别在于区间长度为偶数的时候, 第一种求出的mid偏左, 第二种求出的mid偏右, 对于朴素二分查找这两种没有区别.


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

 这道题用朴素二分法无法求解, 因为要求的是一个区间, 朴素二分求到的那个值无法确定位于区间的哪一个位置, 但是还是可以用二分的, 关键是找到数组的二段性:

1. 查找区间的左端点:

可以发现, 我们可以把区间按照大于等于target小于target分为两部分:

当nums[mid]的值落在小于target的区间内时, 如何去更新值呢? 能去更新right吗?

不能, 因为区间左端点的位置在大于等于target的区间内, 更新right就错过了这个点, 所以要更新left, 而left左边区间的值包括left都小于target, 所以left = mid+1, 最好的情况就是left恰好在区间的右端, 此时mid对应的值就一定等于要找的那个位置.

 当nums[mid]的值落在大于等于target的区间内时, 如何去更新right呢?

right不能等于mid-1, 因为如果mid恰好落在区间的左端点, 那么mid-1就错过了这个位置, 而right要尽可能的往左移取找到区间的左端点, 所以right=mid.

细节处理:

1. 循环条件必须是left<right, 而不能是left<=right, 因为当left==right的时候, 一定是要找的区间左端点, 如果条件还取判断是否相等, 就会陷入死循环.

2.  求中点的操作:

上面我们说了 left+(right-left)/2 和 left+(right-left+1)/2 两种取中点的方式, 这里必须要选择左边那种, 否则又会进入死循环, 因为当区间不断缩减并只剩两个元素的时候,

1. 假如存在区间左端点: 第二种取中点取的一定是right的值, 就会陷入死循环

2. 假如不存在区间: 假如区间内的值都大于target, right一直向左移动, 最终也会达到区间长度为2的, 和上面一样陷入死循环; 假如区间内的值都小于target, left一直向左移动, 最终不会死循环, 但是前面两种情况都会死循环.

 

2. 查找区间的右端点:

操作和查找区间左端点类似, 也是寻找二段性, 把区间分为小于等于target和大于target两部分, 和上面相反当mid落在小于等于target区间, left=mid, 落在大于3的区间, right = mid-1.

循环也是left < right, 但是取中点的方式必须是第二种, 道理和之前类似, 只剩两个元素时会进入死循环.

class Solution {
public:vector<int> searchRange(vector<int>& nums, int target){//处理边界情况if(nums.empty())return {-1,-1};int begin = -1, end = -1;int left = 0, right = nums.size()-1;//寻找区间左端点while(left < right){int mid = left + (right-left)/2;if(nums[mid] < target)left = mid+1;elseright = mid;}if(nums[left] == target)begin = left;left = 0, right = nums.size()-1;//这里left可以不用重置, 因为left已经在begin位置//寻找区间右端点while(left < right){int mid = left + (right-left+1)/2;if(nums[mid] > target)right = mid - 1;elseleft = mid;}if(nums[right] == target)end = right;return {begin,end};}
};

 寻找区间左端点右端点模板:

//寻找区间左端点while(left < right){int mid = left + (right-left)/2;if(...)left = mid+1;elseright = mid;}
 //寻找区间右端点while(left < right){int mid = left + (right-left+1)/2;if(...)right = mid - 1;elseleft = mid;}

...处根据二段性填入

 这两种划分方式选择哪种呢?

根据题目去判断要找的值最终是 >=target or <=target的, 因为left和right相遇的位置一定是包含等于的那个区间, 所以要找的结果一定是要落在包含等于的区间内的.


题目3: 搜索插入位置

解法一: 朴素二分查找, 先用朴素二分查找看看是否能找到target, 能找到直接返回; 找不到的话, 如果循环结束前区间为1的值大于target, right向左移动, 此时的left就是要插入的位置, 如果小于target, left向右移动, left也是要插入的位置 , 所以返回left即可.

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

解法二: 二段性划分: 对于能找到target插入位置在数组中插入位置在数组最左侧的情况, 要返回的位置要么是=target的位置, 要么是第一个大于target的位置, 所以把区间划分为 小于target和大于等于target两部分:

注意, 需要单独处理插入位置在最右边的, 此时整个数组都在小于target的区间, 最终一定是落在数组最后一个位置, 要将返回值+1.

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(target > nums[left])return left+1;else return left;}
};

题目4: x的平方根

 二分查找方法依然是去寻找数组的二段性, 要去[1,x]的数组中去寻找x的平方根, 可以把数组划分为平方<x和平方>=x 平方<=x和平方>x ,哪一种划分是正确的? 因为此题返回的是平方根的整数部分, 比如8的算术平方根是2.82842, 应该返回2, 返回的数值是<=实际的平方根的, 所以应该划分为平方<=x和平方>x.

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(target > nums[left])//单独处理插入在右边界return left+1;else return left;}
};

题目5: 山峰数组的峰顶索引.

暴力解法:

对于数组每一个元素, 比较arr[i] > arr[i-1]是否成立, 出现的第一个不成立的位置记为k, 则k-1就是山峰位置. 

二分查找:

观察山脉数组的定义, 可以发现对于山峰左侧包括山峰的位置, arr[i] > arr[i-1], 对于山脉的右侧, arr[i] < arr[i-1], 所以由此可以把数组分为两段, 求区间的右端点.

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])right = mid-1;elseleft = mid;}return left;}
};

题目6:寻找峰值

暴力解法: 

 1. 第一个位置后如果下降, 则第一个位置就是山峰

2. 第一个位置后上升, 遇到第一个arr[i] < arr[i-1]的位置, i-1即为山峰

3. 第一个位置到最后一直上升, 最后一个位置就是山峰.

二分查找: 

 此题和上一题类似, 但是可能有多个山峰, 也可能是一路上升或者一路下降, 但只需要找到一种情况即可:

arr[i] > arr[i-1]时, 修正left = mid; arr[i] < arr[i-1]时, 修正right = mid-1, 其实就算有多个山峰, 经过区间不断地缩小, 最终一定会变成上一题的只有一个山峰的情况, 无非多了两种一路上升和一路下降,  所以代码和上一题一模一样.

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

题目7: 搜索旋转排序数组中的最小值

以这段数组为例, 以最小值为分界点, 左侧区间内的值一定大于D点的值, 右侧区间内的值一定小于等于D点的值, 根据这个性质就可以把数组分为两段, 题目变成求右区间的左端点:

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

如果我们用A点作为判断点, 左侧区间内的值都大于等于A点, 右侧区间内的值都小于A点, 可以吗?

不完全可以, 这种情况下需要单独去判断如果数组是否是完全递增的情况, 这时left会一直++直到最右侧, 此时是区间的最大值. 而用D点作为判断点则不用考虑这个情况.

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

题目8: 点名

此题以缺失的那个值作为分界点, 可以把区间划分为两块, 左区间内的值 下标数组内对应的值 是相等的, 右区间 下标数组内对应的值 是不相等的, 以此来将数组划分为两块. 需要注意, 如果最后求出来的返回值 下标 和 数组内对应的值 还是相等的, 说明缺失的是学号为n-1的那个人, 要返回left+1:

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(records[mid] == mid)left = mid+1;elseright = mid;}if(records[left] == left)return left+1;return left;}
};

 此题还有若干时间复杂度为O(n)的方法:

1. 遍历数组哈希表存储出现次数, 没出现的就是缺失的

2. 直接遍历找结果

3. 位运算, 根据相同的数字异或等于0的性质, 将前n-1个数异或求和, 然后与数组中的数再异或, 最终的结果就是缺失的值

4. 等差数列求和, 求前n-1项的和再减去数组元素的和, 得到的就是缺失的值.


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/739548.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

docker——启动各种服务

1.Mysql 2.Redis 3.nginx 4.ES 注意&#xff1a;ES7之后环境为 -e ELASTICSEARCH_HOSTS http://ip地址:9200

【libwebrtc】基于m114的构建

libwebrtc A C++ wrapper for binary release, mainly used for flutter-webrtc desktop (windows, linux, embedded).是 基于m114版本的webrtc 最新(20240309 ) 的是m122了。官方给出的构建过程 .gclient 文件 solutions = [{"name" : src,"url

用信号的方式回收僵尸进程

当子进程退出后&#xff0c;会给父进程发送一个17号SIGCHLD信号&#xff0c;父进程接收到17号信号后&#xff0c;进入信号处理函数调用waitpid函数回收僵尸进程若多个子进程同时退出后&#xff0c;这是切回到父进程&#xff0c;此时父进程只会处理一个17号信号&#xff0c;其他…

植物病害识别:YOLO水稻病害识别/分类数据集(2000多张,2个类别,yolo标注)

YOLO水稻病害识别/分类数据集&#xff0c;包含疾病和正常2类&#xff0c;共2000多张图像&#xff0c;yolo标注完整&#xff0c;可直接训练。 适用于CV项目&#xff0c;毕设&#xff0c;科研&#xff0c;实验等 需要此数据集或其他任何数据集请私信

解决input事件监听拼音输入法导致高频事件

1、业务场景 在文本框中输入内容&#xff0c;执行查询接口&#xff0c;但遇到一个问题&#xff0c;当用拼音打字写汉字去搜索的时候&#xff0c;会输入一些字母拼成汉字&#xff0c;怎么能监听等拼音文字输入完成后再触发文本框监听事件 2、解决方案 通过查阅资料得知在输入中…

算法学习11:树与图的 DFS、BFS

算法学习11&#xff1a;树与图的 DFS、BFS 文章目录 算法学习11&#xff1a;树与图的 DFS、BFS前言一、树与图的深度优先遍历1.例题&#xff1a;树的重心&#xff1a; 二、树与图的宽度优先遍历1.例题&#xff1a;图中点的层次&#xff1a; 三、拓扑排序&#xff1a;&#xff0…

vue.js 页面中设置多个swiper

效果&#xff1a; 设置主要设置了 动态的 包含类、 左右按钮的类 <template><div class"swiper-container_other"><!-- 右侧按钮 --><div :class"[(id)?swiper-button-nextid:swiper-button-next, swiper-button-next]"></div…

浅易理解:卷积神经网络(CNN)

浅易理解卷积神经网络流程 本文的目录&#xff1a; 1 什么卷积神经网络 2 输入层 3 卷积层 4 池化层 5 全连接层 1 什么是卷积神经网络 1.1卷积神经网络&#xff08;Convolutional Neural Networks, CNN&#xff09; 是一种前馈神经网络&#xff0c;它的人工神经元可以响应一…

golang中new和make的区别

1. 先看一个例子 package mainimport "fmt"func main() {var a *int*a 10fmt.Println(*a) }运行结果是啥呢&#xff1f; 问&#xff1a;为什么会报这个panic呢&#xff1f; 答&#xff1a;因为如果是一个引用类型&#xff0c;我们不仅要声明它&#xff0c;还要为…

Linux命令-权限管控

Linux命令-权限管控 目录 Linux命令-权限管控rootsu&#xff08;switch user&#xff09;sudo用户、用户组查看权限管控信息修改权限控制chmodchown 对于Linux中权限的讲解以及对权限的一些操作 root 超级管理员&#xff0c;拥有最大系统操作权限普通用户一般在其HOME目录内是…

Leetcode 675 为高尔夫比赛砍树

文章目录 1. 题目描述2. 我的尝试3. 题解1. BFS 1. 题目描述 Leetcode 675 为高尔夫比赛砍树 2. 我的尝试 typedef priority_queue<int, vector<int>, greater<int>> heap;class Solution { public:int m;int n;int bfs(vector<vector<int>>&…

2024中国(京津冀)太阳能光伏推进大会暨展览会

2024年中国(京津冀)太阳能光伏推进大会暨展览会是一个旨在促进太阳能光伏产业发展的重要会议和展览会。该活动将在中国的京津冀地区举行&#xff0c;旨在汇聚全球太阳能光伏领域的专业人士、政府代表、企业家和科研人员&#xff0c;共同探讨太阳能光伏技术的最新进展和未来发展…

数据集成工具 ---- datax 3.0

1、datax: 是一个异构数据源离线同步工具&#xff0c;致力于实现关系型数据库&#xff08;mysql、oracle等&#xff09;hdfs、hive、hbase等各种异构数据源之间的数据同步 2、参考网址文献&#xff1a; https://github.com/alibaba/DataX/blob/master/introduction.md 3、Da…

避抗指南:如何寻找OLED透明屏供应商

寻找OLED透明屏供应商&#xff0c;你可以按照以下步骤进行&#xff1a; 明确需求&#xff1a;首先&#xff0c;你需要明确自己的需求&#xff0c;包括所需OLED透明屏的尺寸、分辨率、亮度、色彩饱和度等具体参数&#xff0c;以及预算和采购量。这有助于你更精准地找到符合需求的…

【sgPhotoPlayer】自定义组件:图片预览,支持点击放大、缩小、旋转图片

特性&#xff1a; 支持设置初始索引值支持显示标题、日期、大小、当前图片位置支持无限循环切换轮播支持鼠标滑轮滚动、左右键、上下键、PageUp、PageDown、Home、End操作切换图片支持Esc关闭窗口 sgPhotoPlayer源码 <template><div :class"$options.name"…

革命性创新:聚道云软件连接器如何为企业重塑财务管理流程?

一、客户介绍 某科技股份有限公司是一家专注于高性能存储技术领域的创新型科技公司。自公司成立以来&#xff0c;该公司始终秉持创新发展的理念&#xff0c;致力于为客户提供卓越的存储解决方案&#xff0c;以满足不同行业对数据存储的需求。作为业界的佼佼者&#xff0c;该公…

SpringBoot(依赖管理和自动配置)

文章目录 1.基本介绍1.springboot是什么&#xff1f;2.快速入门1.需求分析2.环境配置1.确认开发环境2.创建一个maven项目3.依赖配置 pom.xml4.文件目录5.MainApp.java &#xff08;启动类&#xff0c;常规配置&#xff09;6.HelloController.java &#xff08;测试Controller&a…

数字证书在网络安全中的重要性与实际应用

数字证书作为一种“电子身份证”&#xff0c;在当今数字化的商业环境中有着广泛的实际应用。它主要用于身份认证、加密通信、电子签名和安全访问控制等方面&#xff0c;为各行各业提供了安全可靠的数字化解决方案。 网络安全领域 在网络通信中&#xff0c;数字证书被广泛应用…

String 底层是如何实现的?

1、典型回答 String 底层是基于数组实现的&#xff0c;并且数组使用了 final 修饰&#xff0c;不同版本中的数组类型也是不同的&#xff1a; JDK9 之前&#xff08;不含JDK9&#xff09; String 类是使用 char[ ]&#xff08;字符数组&#xff09;实现的但 JDK9 之后&#xf…

MySQl基础入门⑧

上一章的内容 练习&#xff01;上一章表的内容&#xff01;&#xff01;&#xff01;熟能生巧 先重新创建一个数据库 命令create database supermarket; 然后查看数据库、再切换到当前数据库。 查看数据库 : show databases; 切换到当前数据库: use supermarket;创建员工…