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

【基础算法总结】二分查找

目录

  • 一,二分查找算法介绍
  • 二,算法原理和代码实现
    • 704.二分查找
    • 34.在排序数组中查找元素的第一个和最后一个位置
    • 69.x的平方根
    • 35.搜索插入位置
    • 852.山脉数组的峰顶索引
    • 162.寻找峰值
    • 153.寻找旋转排序数组中的最小值
    • LCR173.点名
  • 三,算法总结

一,二分查找算法介绍

二分查找算法是一种十分经典,十分基础的算法。它的特点是:细节最多,最容易写出死循环,但是真正掌握之后,又是最简单的算法

相信大家在之前一定听过"二分查找只用于数组有序的情况下"这种说法,其实这是不准确的!!!它的本质是:数据具有二段性。应用场景准确的说是:当数组里的一个数和目标数比较之后,划分出了两段区域(此时具有"二段性"),通过某种规律可以直接舍弃一段区域,在另一段区域查找,周而复始这个操作,直到找到目标数。本篇文章将通过若干道题目进行验证

还有就是,二分查找是有模板的,本篇文章将介绍三类模板:
(1) 朴素二分模板
(2) 查找左边界的二分模板
(3) 查找右边界的二分模板
第一类(具体模板在第一道题后面)是最简单的最普通的,但是局限性最大,后面两类(具体模板在第二道题后面)适用性最强,但是细节最多

二,算法原理和代码实现

704.二分查找

在这里插入图片描述
在这里插入图片描述

算法原理

这道题是二分算法中最简单最朴素的一道题。
我们知道只要数组里的一个位置能使数据具有二段性就可使用二分,这个位置其实可以是中间点,也可以是⅓点,¼点…但是经过前人验证可知,找中间点是效率最高的

算法流程如下:
(1) 定义left ,right 指针,分别指向数组的最左最右
(2) 找到待查找区间的中间点 mid ,找到之后分三种情况讨论:
i. arr[mid] == target 说明正好找到,返回 mid 的值;
ii. arr[mid] > target 说明 [mid, right] 这段区间都是⼤于 target 的,因此舍去右边区间,在左边[left, mid -1] 的区间继续查找,即让 right = mid - 1 ,然后重复2 过程
iii. arr[mid] < target 说明 [left, mid] 这段区间的值都是⼩于 target 的,因此舍去左边区间,在右边[mid + 1, right] 区间继续查找,即让 left = mid + 1 ,然后重复2 过程
(3) 当 left 与 right 错开时,说明整个区间都没有这个数,返回-1
在这里插入图片描述

细节/技巧问题

(1) 循环结束的条件即使 [left,right] 的区间一直缩小,指向同一个数,这个数也是需要比较的,所以结束的条件是 left > right,直到两者彻底错开才结束
(2) 求中间位置的坐标。一般求法是 mid = (left + right) / 2,但是这种方法会有数据溢出的风险,所以建议使用 mid = left + (right - left) / 2
(3) 时间复杂度: 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 - left)/2;if(nums[mid] > target) right = mid - 1;else if(nums[mid] < target) left = mid + 1;else return mid;}return -1;}
};

总结朴素二分模板

在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述

算法原理:
这是一道如何用二分查找左右端点的模板例题。下面的内容将详细分析如何用二分查找左右端点的核心代码和细节问题

1.查找区间左端点

在这里插入图片描述

细节问题:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.查找区间右端点和细节问题:

在这里插入图片描述
在这里插入图片描述

代码实现:

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;vector<int> ret;while(left < right){int mid = left + (right - left) / 2;if(nums[mid] < target) left = mid + 1;else right = mid;}// 判断是否有结果if(nums[left] == target) ret.push_back(left);else return {-1, -1};// 找右端点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;}// 跳出循环后就可以不用判断了,因为只要有左端点就一定有右端点// 只是左右端点相不相等的问题ret.push_back(right);return ret;}
};

这道题其实还有一个细节/技巧问题:当左端点存在是,右端点一定是存在的,只是左右端点相不相等的问题,所以查找右端点时可以不用判断

总结二分模板:

在这里插入图片描述

69.x的平方根

在这里插入图片描述
在这里插入图片描述

算法原理:

首先分析出 “二段性”

假设 ret 是要返回的结果,根据这个值,则 ret 的左半区间(包括ret)的每个数平方后都是小于等于 x 的,右半区间的每个数平方后都是大于 x 的,这就是本题的 “二段性”
在这里插入图片描述

代码实现:

class Solution 
{
public:int mySqrt(int x) {// 处理特殊情况if(x == 0) 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;}
};

35.搜索插入位置

在这里插入图片描述
在这里插入图片描述

算法原理:
首先分析出 “二段性”

假设返回的插入位置是 ret ,那么就有两种情况:
(1) 当目标值存在时,就返回对应值的下标
(2) 当目标值不存在时,它的插入位置恰好就是第一次出现比目标值大的那个数的位置或是数组的最后一个位置
结合这两种情况,最终找的这个位置上的值是大于等于目标值的
所以 ret 对应值的左半区间是小于目标值的,右半区间(包括 ret 对应值)是大于等于目标值的。这就是本题的 “二段性”
在这里插入图片描述

细节问题:

当跳出循环时,说明left和right已经相等了,如果此时对应的值小于目标值,说明已经到最后的位置了,并且不存在目标值,所以返回的是下一个位置

代码实现:

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) right = mid;else left = mid + 1;}// 错误的//return left == nums.size() - 1 ? left + 1 : left;// 走到这里,说明left和right已经相等了,如果此时对应的值小于目标值// 说明已经到最后的位置了,并且不存在目标值,返回的是下一个位置return nums[left] < target ? left + 1 : left;}
};

852.山脉数组的峰顶索引

在这里插入图片描述
在这里插入图片描述

算法原理:

这道题的数据由峰顶的元素天然的被分成两段:如图,左半段的数据严格遵守 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]) left = mid;else if(arr[mid] < arr[mid -1]) right = mid - 1;}return left;}
};

162.寻找峰值

在这里插入图片描述
在这里插入图片描述
算法原理:

我们把这道题抽象出来,分析出"二段性"
假如我们选择 i 位置,根据 i 位置的值和 i+1 位置的值分类讨论:
(1) arr[i] > arr[i+1],如图1,所以在左边区间中至少会存在一个峰值,因为左边是从负无穷开始的,只能是先有上升趋势才有下降趋势,但是右边区间不一定,因为有可能是一直下降的。
(2) arr[i] < arr[i+1],如图2,与前面相反,左边区间可能没有峰值,右边区间中至少会存在一个峰值
这就分析出了本题的"二段性",根据 arr[i] 和 arr[i+1] 的值的关系可以分数组为两个区间,去其中一个区间里搜索结果
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/5257b4529c344951a0feb3f3681204ad.png
二分核心流程:
在这里插入图片描述

代码实现:

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

153.寻找旋转排序数组中的最小值

在这里插入图片描述
在这里插入图片描述

算法原理:

先分析出本题的"二段性",可以把旋转后的数组以最大值为界线,抽象为"两条直线",如图:可以清楚的看见具有"两段性"。可以以D点作为参照物,AB段区域的元素都大于D点值,CD段的都小于等于D点值,而要找的最小值就是C点。所以就是找右边区间的左端点
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0c3eeceec91942d78c2391566646f0af.png
二分核心流程:
在这里插入图片描述

代码实现

class Solution 
{
public:int findMin(vector<int>& nums) {int left = 0, n = nums.size(), 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];}
};

LCR173.点名

在这里插入图片描述
在这里插入图片描述

算法原理:

这道题很简单,也很多解。可用的解法有:一直接遍历找结果,二使用高斯求和公式,三使用哈希思想,四使用位运算,五二分查找其中前面四种解法的时间复杂度都是O(N),最后一种是O(logN)。这里重点介绍二分查找。

首先分析出"二段性"
写出下标,不难发现虚线左边的区域中的数和下标是一一对应的,虚线右边就不是,这就是"二段性"
在这里插入图片描述
二分核心流程:
在这里插入图片描述

代码实现:

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;else right = mid;}return left != records[left] ? left : left+1;}
};

下面是其他解法的代码,仅供参考:

(1) 直接遍历找结果

class Solution 
{
public:int takeAttendance(vector<int>& records) {int ret = 0;for(auto e : records)if(ret == e) ret++;return ret;}
};

(2) 使用高斯求和公式

class Solution 
{
public:int takeAttendance(vector<int>& records) {int sum = 0, tmp = 0, n = records.size();for(auto e : records) sum += e;tmp = (0 + n) * (n+1) * 1.0 / 2.0; // 高斯求和公式return tmp - sum;}
};

(3) 使用哈希思想(最慢)

class Solution {
public:int takeAttendance(vector<int>& records) {unordered_set<int> s;for(auto e : records) s.insert(e);for(int i = 0; i <= records.size(); i++)if(s.count(i) == 0) return i;return -1;}
};

(4) 使用位运算

class Solution {
public:int takeAttendance(vector<int>& records) {int ret = 0;for(int i = 0; i <= records.size(); i++)ret ^= i;for(auto e : records)ret ^= e;return ret;}
};

三,算法总结

通过上面的若干道题目可以发现,二分算法的代码十分简单,也十分固定。他并不是只能用于"数组有序"的场景,二分算法最关键,最重要的一步就是分析出"二段性"!!!只要分析出了"二段性",就可以进一步推出二分的核心代码了

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 算法练习题19——leetcode141环形链表
  • 基于51单片机的智能农业滴灌控制系统proteus仿真
  • 鸿蒙OS开发秘籍:打造优雅的登录状态管理系统
  • ai绘图软件哪个好用?解锁5款小白必备工具
  • Git的基本概念和使用方式
  • shader 案例学习笔记之绘制圆
  • springboot属性加载优先级和常见命令行属性
  • 一个Java中有用的JacksonUtil类
  • 【重点】(非常全) Node.js的生态有哪些包
  • C语言代码练习(第十九天)
  • StarRocks Lakehouse 快速入门——Apache Iceberg
  • Conmi的正确答案——MySQL的层级递归查询(递归公共表表达式,CTE)
  • 2. 下载rknn-toolkit2项目
  • PhpStudy下载安装使用学习
  • 【亲测能用!OpenVPN实验教程】Win11主机连CentOS7服务器(用户名密码模式)
  • 【Leetcode】104. 二叉树的最大深度
  • 【个人向】《HTTP图解》阅后小结
  • CSS 三角实现
  • Java基本数据类型之Number
  • Java知识点总结(JDBC-连接步骤及CRUD)
  • Material Design
  • React Transition Group -- Transition 组件
  • 服务器之间,相同帐号,实现免密钥登录
  • 猫头鹰的深夜翻译:Java 2D Graphics, 简单的仿射变换
  • 如何正确配置 Ubuntu 14.04 服务器?
  • 入门到放弃node系列之Hello Word篇
  • 原生 js 实现移动端 Touch 滑动反弹
  • AI算硅基生命吗,为什么?
  • 新年再起“裁员潮”,“钢铁侠”马斯克要一举裁掉SpaceX 600余名员工 ...
  • 支付宝花15年解决的这个问题,顶得上做出十个支付宝 ...
  • # Redis 入门到精通(七)-- redis 删除策略
  • #php的pecl工具#
  • #我与Java虚拟机的故事#连载07:我放弃了对JVM的进一步学习
  • #我与Java虚拟机的故事#连载10: 如何在阿里、腾讯、百度、及字节跳动等公司面试中脱颖而出...
  • $HTTP_POST_VARS['']和$_POST['']的区别
  • (06)金属布线——为半导体注入生命的连接
  • (1)Map集合 (2)异常机制 (3)File类 (4)I/O流
  • (附源码)ssm高校志愿者服务系统 毕业设计 011648
  • (附源码)ssm户外用品商城 毕业设计 112346
  • (附源码)ssm基于微信小程序的疫苗管理系统 毕业设计 092354
  • (回溯) LeetCode 40. 组合总和II
  • (力扣记录)1448. 统计二叉树中好节点的数目
  • (十) 初识 Docker file
  • (学习日记)2024.01.19
  • (原创)boost.property_tree解析xml的帮助类以及中文解析问题的解决
  • .NET 4.0网络开发入门之旅-- 我在“网” 中央(下)
  • .NET 6 Mysql Canal (CDC 增量同步,捕获变更数据) 案例版
  • .Net Core 中间件验签
  • .net core控制台应用程序初识
  • .NET 实现 NTFS 文件系统的硬链接 mklink /J(Junction)
  • .Net 执行Linux下多行shell命令方法
  • .NET 指南:抽象化实现的基类
  • .NET开源项目介绍及资源推荐:数据持久层 (微软MVP写作)
  • @EventListener注解使用说明
  • []C/C++读取串口接收到的数据程序