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

news2024/11/17 15:35:10

二分查找一

  • 1. 二分查找
  • 2.在排序数组中查找元素的第一个和最后一个位置
  • 3.x 的平方根
  • 4.搜索插入位置

在这里插入图片描述

点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

二分查找算法简单介绍
关于二分查找原理我们干巴巴的说也没有意思,我们在具体题目中在一一介绍,这里我们主要想说的是二分查找算法的特点,以及它的侧重点!

  1. 特点

最恶心,细节最多,最容易写出死循环的算法。如果掌握了它就会变成最简单的塞凡。

  1. 侧重点

算法原理:以前我们可能就听过这样一句话,二分查找适合数组有序的情况,但是当真正理解它算法原理之后,二分查找其实一个非常牛逼的算法,不仅适合数组有序的情况,即使数组无序的情况,只要发现数组中规律能够使用二分查找,就使用二分查找,管它是有序还是无序。

模板:高度抽象化之后固定得格式。但是不要死记硬背!一定要理解之后再记忆!
朴素的二分模板 ----> (简单但有局限)
查找左边界的二分模板、查找右边界的二分模板 ---->(万能但细节多)

后面在题中在总结

1. 二分查找

题目链接:704. 二分查找

题目分析

在这里插入图片描述

这道题非常重要的就是数组有序

算法原理

数组从左往右走一一排除。
解法一:暴力求解
时间复杂度O(N)

我们看也没有优化的可能,暴力解法没有利用数组有序的条件。假设数组中我们随便拿一个数4,目标数是target,因为是数组是升序的,我们发现4的左边区间的数比我这个4小,并且4本身还比target小的。那么这个区间都是比target小的,那根本就不可能能等于5,因此这块区间就可以直接干掉,然后从右边区间找就可以了。我们仅仅比较一次就干掉一批数,所以说用这个规律绝对比暴力求解强!

在这里插入图片描述

假设现在又找到数7比5大,7已经比5大了,它后面铁定比5大,因为后面数比7还大,因此我们可以把7包含7后面的区间干掉,然后再新的区间找。
在这里插入图片描述
我们总结这个规律:在一个数组中随便找一个点,发现这个数和target做比较后,划分成两个区域,其中根据规律我们可以有选择性舍去一个区域,然后在另一个区域寻找。此时我们称这个题里面是有二段性的,如果有二段性,我们就可以使用二分查找算法解决这个问题
在这里插入图片描述

解法二:二分查找算法
本道题有二段性就可以用二分查找算法,这是非常重要的,而不是像别人说的数组有序才能用二分查找。二分查找的本质是当数组有二段性就可以用二分查找算法。数组无序但是发现有二段性也可以用二分查找

二段性:当我们发现一个规律,根据这个规律选取某一个点,能把数组分成两部分,然后根据规律有选择性的舍去一部分,在另一部分查找。此时就可以用二分查找算法

这个点怎么选呢?建议就选中间的节点,根据数学概率学,选1/2直接干掉一半

定义两个指针left,right刚开始指向左端点和右端点,找到一个mid点,利用上面性质根据这个点来确定寻找区间,然后就有三种关系:

在这里插入图片描述

  1. x<t,根据规律把左边区间删掉 left=mid+1 ,从新的【left,right】寻找
  2. x>t,根据规律把右边区间删掉 right=mid-1,从新的【left,right】寻找
  3. x==t,返回结果

以上就是朴素二分查找算法的核心步骤

不过还有一些细节问题:

  1. 循环结束的条件

当left>right循环结束,当left<=right循环继续

  1. 为什么是正确的

虽然仅比较一次就去掉一部分区间,但是我们做到暴力解法比较多次才能去掉这段区间。所以是和暴力解法一样正确的

  1. 时间复杂度

时间复杂度一般是最坏情况下的执行次数,
在这里插入图片描述

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;
    else
        return ...;
}

细节一:一定是left<=right
细节二:找中间点防溢出的方式,left+(right-left)/2, 其实找中间点有两种写法,+1和不+1:
普通+1:(left+right+1)/2
防溢出+1:left+(right-left+1)/2
不过对于数组个数为奇数求中间点的时候,+1,不+1是没有区别的都是指向同一个点。个数是偶数求中间点的时候,中间点是有两个数的,不+1在左边,+1在右边。

在这里插入图片描述
不过对于朴素版本来说都是无所谓的 !+1不+1都可以。因为在朴素这里仅需找到一个点就可以,然后根据这个点来划分左右区间就可以,这个点在中间点那个位置我并不关心!

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

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

题目分析:

在这里插入图片描述
一个非递减的排序的数组的意思就是递增或者不变的数组。

算法原理:

解法一:暴力求解

这道题很容易就会想的到暴力求解,从前向后遍历,标记出现target的第一个位置和最后一个位置。但是时间复杂度是O(N)

在这里插入图片描述

因为这也是一个有序数组,所以还可以想到二分查找,我们用刚学的朴素二分查找算法试一下,但是虽然你找到这个位置,但这并不是答案,你还要从这个位置,往左找第一个出现的位置,往右找第一个出现的问题,极端条件下整个数组都是target,那时间复杂度就退化到O(N)了。

在这里插入图片描述

我们需要想到一个更优秀的策略,依旧是二分,依旧要用到数组有序的情况。只不过是在朴素二分的基础上对二分策略做一下优化!

回到二分的本质,当我们发现这道题有个规律 “二段性”,我们就可以利用二分查找。
因为我们要找第一个和最后一个,不能同时寻找,所以我们分开考虑,先找第一个。

1.查找区间的左端点

以下面例子为例,当我们找到最左端点时,把数组划分两部分。左区域小于t,右区域大于等于t。因此我根据target在查找区间左端时,发现数组是具有二段性的就可以用二分
在这里插入图片描述
定义两个指针,找左端点。具体操作如下
在这里插入图片描述

  1. x < t ,也就是说mid此时所在区间是小于t的,因此 left=mid+1然后再新的【left,right】找
  2. x >= t,为什么把大于等于放一块,因为你这次找的值虽然和target相同,但可能并不是最左端点,可能是最左端点的右边。或者就是值比target大的点,这两种处理方法是一样的。那此时right=mid-1吗?并不是,如果万一 这个点就是最左端点,right=mid-1,那不就走过去了,因此 right=mid, 然后再新的【left,right】找

这两句是整个代码的核心,但是难点并不在这里。最恶心,最容易出错地方在细节哪里。

细节处理:

细节一:循环条件

一种循环条件 left<=right,另一种是left<right ,选那个?
left<right作为循环条件
原因如下,这数组一共就三种情况;
1.有结果
刚开始left处于不合法区间,right处于合法区间,left一直在不合法的区间移动,right一直在合法区间移动。而且会发现right在合法区间移动不会超过ret,因为right=mid。left一直在想跳出这个不合法区间,因为left=mid+1。当left和right相遇的位置恰好就是left跳出不合法区间的位置。所以说当left和right相遇的位置正好是最终结果

在这里插入图片描述
所以,当left = right 的时候,就是最终结果,无需在进循环判断

2.全大于t
只会命中x>=t,也只有right一直在移动,直到left和right相遇为止,但是没有最终结果,仅需判断这个相遇位置的值是否等于target,相等返回下标不等返回-1,也就是
当left = right 的时候,就是最终结果,无需在进循环判断
在这里插入图片描述
3.全小于t
只会命中x<t,left一直向右移动,直到遇到right,当left和right相遇,也是仅需判断是否和target相遇,没必要在循环了。

在这里插入图片描述
所以,以上三种情况都是符合:当left = right 的时候,就是最终结果,无需在进循环判断 这是第一点。

第二点,如果判断了,就会死循环

当两个指针都指向ret的时候,如果继续判断你会发现mid依旧还是指向这个值,命中第二个条件,right并不会移动! 就死循环下去了!
在这里插入图片描述

细节二:求中点操作

求中断我们用防溢出的,无非就是两种选择。
left + ( right - left ) / 2
left + ( right - left + 1 ) / 2

前面说过如果个数是奇数选+1/不+1没有区别,指向的同一个位置。如果数组个数是偶数是,用第一种方法是求得靠左的位置,用第二种方法求得是靠右得位置。这两种方法在朴素二分哪里没有问题。但是在这里就不行了!

假设我们选left + ( right - left + 1 ) / 2求中点,当最后一次操作就剩下两个数了,mid落在right这里,如果命中第一个条件,left=mid+1此时没问题循环,但是如果命中第二个条件,right=mid,你会发现mid根本没动,下一次还是它,在下一次还是它,就会死循环

在这里插入图片描述
假设我们选left + ( right - left ) / 2求中点,最后一次操作就是想两个数,mid落在left这里,命中x<t,那left和right相遇就会跳出循环,命中x>=t,left和right也还会相遇也还是跳出循环。
在这里插入图片描述
所以求中点操作left + ( right - left ) / 2

当这两个细节问题处理完了,我们查找区间的左端点就完成了。

总结查找区间的左端点的核心:

  1. x < t, left=mid+1,然后再新的【left,right】找
  2. x >= t,right=mid, 然后再新的【left,right】找

循环条件:left < right
求中点操作:left + ( right - left ) / 2

2.查找区间右端点
其实查找区间右端点思路和查找区间左端点思路是一样的,但是细节不一样。
根据右端点依旧可以把数据划分成两部分,左边小于等于target,右边大于target。
根据这个二段性,我们就可以使用二分。

在这里插入图片描述

定义两个指针,找到mid,根据这个x分情况讨论
在这里插入图片描述

  1. x <= t ,mid落在左边区域,同理left不能越过mid,因为mid可能就是指向了最终结果,如果指向最终结果,left=mid+1了,那接下里查找区间就没有最终结果了。left=mid,然后再新的【left,right】找
  2. x > t,mid落在右边区域,因为mid落在的区间一定大于target不符合要求,所以right=mid-1,然后再新的【left,right】找

重点依旧是处理细节问题:

细节处理:

细节一:循环条件

原因同上面一样 left < right

细节二:求中点操作

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

假设选的是left + ( right - left ) / 2,当最后一次操作就剩下两个数了,mid此时在left这里,如果命中x>t,right=mid-1跳出循环,如果命中x<=t,left=mid,mid经过计算还是老位置根本不动,最终会导致死循环
在这里插入图片描述
假设选的是left + ( right - left ) / 2,当最后一次操作就剩下两个数了,mid此时在right这里,如果命中x>t,right=mid-1,那left和right相遇就会跳出循环,如果命中x<=t,left=mid,那left和right还是相遇也会跳出循环
在这里插入图片描述
因此求中点操作left + ( right - left + 1) / 2

总结查找区间的右端点的核心:

  1. x <= t, left=mid,然后再新的【left,right】找
  2. x > t,right=mid-1, 然后再新的【left,right】找

循环条件:left < right
求中点操作:left + ( right - left + 1 ) / 2

原理我们都清楚了,我们就可以写代码了:

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


        if(nums.empty()) return{-1,-1};
		
		int begin=0;
        int left=0,right=nums.size()-1;
        //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 {-1,-1};
        
        begin=left;

        // 2.二分右端点
        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,right};
    }
};

总结:查找左边界的二分模板、查找右边界的二分模板

在这里插入图片描述
除了判断条件一样,其余的都不一样,如果要是死记硬背肯定有差错,一定要理解记忆。

其实我们仅需记住求中点操作就可以了, 下面两句我们画图很容易推出来。无非就是细节问题很恶心。

上面判断都是left<right我们不用记忆,仅需要记住求中点操作,求左端点不加1,求右端点要+1。还可以这样记,当下面出现 -1 的时候,上面就 +1具体left、right怎么办就分类讨论,就题论题即可!

在这里插入图片描述

3.x 的平方根

题目链接:69. x 的平方根

题目分析

在这里插入图片描述
这道题让找算术平方根,我们可以换个思路找谁的平方根是x

算法原理:

解法一:暴力求解
遍历直到找到一个数的平方大于或者等于x。
比如说x=17,当遍历到5的平方25的时候,大于x,但是4的平方16小于x,因此是一个4.xxx的数,题目只需要保留整数部分,因此最终结果是4
在这里插入图片描述

我们发现这是一个有序的,我们看看能不能发现二段性,进而将暴力解法改成二分查找。 我们就盯着结果来,发现结果要么小于x要么等于x。
在这里插入图片描述
因此我们就发现了二段性,最终结果将整个区间分成两个区间,左边区间小于等于x,右边区间大于x。

解法二:二分查找

目前我们学了三种二分查找,朴素、查找区间左端点、查找区间右端点三种二分查找算法。我们以后遇到的上档次题几乎不会用到朴素二分查找! 因此用不朴素的的!

在这里插入图片描述
下面就是具体分析过程

在这里插入图片描述

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;
    }
};

4.搜索插入位置

题目链接:35. 搜索插入位置

题目分析

在这里插入图片描述
根据题目意思就是要使用二分查找,我们先看看有没有二段性。根据题目要求数组有该值就返回该值的下标,没有就插入返回插入的下标,我们发现插入的下标是第一个大于它的数的位置。由此我们就发现了二段性,ret要么是本来就有该值的位置或者是插入的位置。ret将整个数组分为两部分,左区间小于t,右边大于t

在这里插入图片描述

算法原理:二分查找

在这里插入图片描述

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;
    }
};

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

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

相关文章

Dependencies:查找项目中dll关联文件是否缺失。

前言 Dependencies工具作为一款优秀的DLL解析工具&#xff0c;能让你很直观地看到DLL的相关信息&#xff0c;如具备哪些功能函数、参数&#xff0c;又比如该DLL基于哪些DLL运行。判断该dll基于哪些dll运行&#xff0c;如果基于的dll丢失&#xff0c;那么就会提示。就能判断缺少…

RobbitMQ基本消息队列的消息接收

1.先给工程引入依赖 父工程有了子工程就不用导了 <!--AMQP依赖&#xff0c;包含RabbitMQ--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId> </dependency> 2.配置yml…

前端组件库图片上传时候做自定义裁剪操作

不论是vue还是react项目&#xff0c;我们在使用antd组件库做上传图片的时候&#xff0c;有一个上传图片裁剪的功能&#xff0c;但是这个功能默认是只支持1:1的裁剪操作&#xff0c;如何做到自定义的裁剪操作&#xff1f;比如显示宽高比&#xff1f;是否可以缩放和旋转操作&…

vue 中的 Vuex

Vuex Vuex是什么&#xff1f; 概念&#xff1a;专门在vue中实现集中式状态&#xff08;数据&#xff09;管理的一个Vue插件&#xff0c;对Vue应用中多个组件的共享状态进行集中式的管理(读/写&#xff09;&#xff0c;也是一种组件间通信的方式&#xff0c;且适用于任意组件间…

【二叉树算法题记录】二叉树的所有路径,路径总和——回溯

目录 257. 二叉树的所有路径题目描述题目分析cpp代码 112. 路径总和题目描述题目分析cpp代码 257. 二叉树的所有路径 题目描述 给你一个二叉树的根节点root &#xff0c;按任意顺序&#xff0c;返回所有从根节点到叶子节点的路径。 题目分析 其实从根节点往下走&#xff0c…

C语言/数据结构——(链表的回文结构)

一.前言 今天在牛客网上刷到了一道链表题——链表的回文结构https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa?&#xff0c;巧合的是它的解题思路恰好是我们一起分享过两道链表题的汇总。这两道题分别是反转链表和链表的中间节点。废话不多数&#xff0c…

css 案例 横向滚动渐变

效果 完整代码&#xff1a; <template><view class"content"><view class"tab"><view class"tab-item" v-for"(item,index) in tab" :key"index" click"handlerTab(index)":class"ind…

Spring Cloud Gateway 11种断言工厂

系列文章目录 文章目录 系列文章目录前言前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 Spring Cloud Gateway路由匹配是Spring WebFlux基础功能的一部分,在Spri…

项目经理有哪些常见的沟通技巧?

项目经理有哪些常见的沟通技巧&#xff1f; 项目管理的核心之一是沟通。有效的沟通技巧对于确保项目团队成员之间的合作、项目信息的准确传达以及项目目标的顺利实现至关重要。一个号的项目管理工具可以让我们事半功倍&#xff0c;结合项目管理工具zz-plan 来探讨一些项目经理…

STM32F103RCT6#1

实验1 F103基准实验 1.1实验内容&#xff1a;验证以下基本功能&#xff1a;两个LED&#xff08;编号为LED1和LED2&#xff09;每500ms交替闪烁&#xff1b;计算机上的串口助手每秒输出一次字符串。 &#xff08;实验背景&#xff09;启动车辆时&#xff0c;首先将变速箱从驻车…

Baidu Comate 编程插件:提升开发效率的利器

文章目录 引言简介目的 Baidu Comate插件概述定义与功能市场现状竞品分析 安装与配置VsCode 安装&#xff1a;注意事项 版本选择 核心特性详解功能介绍代码生成实时续写错误纠正 使用体验体验地址 引言 简介 基于文心大模型&#xff0c;结合百度积累多年的编程现场大数据和外…

RFID读写头JY-V640在半导体wafer晶圆盒的使用流程

为了最大限度地提高生产效率&#xff0c;新的晶圆工厂和正在翻新升级的晶圆工厂选择采用RFID技术应用在半导体制造业上&#xff0c;通过RFID技术的非接触式采集信息特性&#xff0c;对晶圆盒在生产、存储、运输过程中进行信息追踪和管理&#xff0c;提升半导体制造业的生产效率…

洪水仿真模拟(ArcGIS),水利数字孪生新利器

这两天ArcGIS Pro的官方账号释放了一个名为“Flood Simulation in ArcGIS Pro”的洪水模拟功能视频。根据视频详情页的介绍&#xff0c;该洪水仿真模拟功能会作为新功能出现在ArcGIS Pro 3.3中。 由于我目前从事的主要应用方向都是弱GIS的领域&#xff0c;所以我已经很久没有再…

Selenium——获取元素和操纵元素的方法

1、获取元素的方法 1、通过id获取 element wd.find_element(By.ID,"id")2、通过classname获取 elements wd.find_elements_by_class_name("plant") for element in elements:print(element.text)3、通过tagname获取元素 elements wd.find_elements_…

Ubuntu20.04 设置路由器

1. 网络拓扑图 2. 查看网卡信息 ip a得出如下网卡信息&#xff0c;enp1s0和enp2s0为两个网卡名称&#xff0c;以及相关两个网卡的详细信息&#xff0c;不同设备的网卡名称可能不一样 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group defaul…

9.spring-图书管理系统

文章目录 1.开发项目流程1.1开发开发1.2数据库的设计 2.MySQL数据库相关代码3.构造图书结构3.1用户登录3.2图书列表3.3图书添加3.4图书删除3.4.1批量删除 3.5图书查询(翻页) 4.页面展示4.1登录页面4.2列表页面4.3增加图书页面4.4修改图书信息页面 5.功能展示5.1增加图书信息5.2…

技巧:无脑秒解“已知前序\后序与中序遍历序列,求后序\前序遍历序列”

目录 举例一 1、画坐标系&#xff1a; 2、填表&#xff1a; 3、连线 举例二 1、画坐标系 2、填表 3、连线 原理 这是一个笔试技巧&#xff0c;对代码能力没有什么提高。 可以用&#xff0c;但是代码也要会写&#xff0c;那才是根基。 相对于传统方法&#xff0c;此方法非常的快…

哈希表第1/9题--哈希表基础理论介绍

1. 哈希表的定义&#xff1a;哈希表是根据关键码的值而直接进行访问的数据结构。数组就是一张常见的哈希表&#xff0c;其中哈希表中关键码就是数组的索引下标&#xff0c;然后通过下标直接访问数组中的元素。 2.哈希表的作用&#xff1a;通过哈希表可以快速判断一个元素是否出…

React 第三十一章 虚拟DOM

面试题&#xff1a;什么是虚拟DOM&#xff1f;其优点有哪些&#xff1f; 标准且浅显的答案 虚拟dom本质上就是一个普通的 JS 对象&#xff0c;用于描述视图的界面结构 虚拟 DOM 最早是由 React 团队提出来的&#xff0c;因此 React 团队在对虚拟 DOM 的定义上面有绝对的话语权。…

书生·浦语大模型实战营之 OpenCompass大模型评测

书生浦语大模型实战营之 OpenCompass &#xff1a;是骡子是马&#xff0c;拉出来溜溜 为什么要研究大模型的评测&#xff1f; 百家争鸣&#xff0c;百花齐放。 首先&#xff0c;研究评测对于我们全面了解大型语言模型的优势和限制至关重要。尽管许多研究表明大型语言模型在多…