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

news2024/9/25 21:28:52

目录

  • 一,二分查找算法介绍
  • 二,算法原理和代码实现
    • 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;
    }
};

三,算法总结

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

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

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

相关文章

高性能反向代理--HAProxy

文章目录 Web架构负载均衡介绍为什么使用负载均衡负载均衡类型 HAProxy简介应用场景HAProxy是什么HAProxy功能 脚本安装HAProxy基础配置global多进程和线程HAProxy日志配置项 Proxies配置-listen-frontend-backendserver配置 frontendbackend配置实例子配置文件 HAProxy调度算法…

html+css网页设计 旅游 蜘蛛旅行社5个页面

htmlcss网页设计 旅游 蜘蛛旅行社5个页面 网页作品代码简单&#xff0c;可使用任意HTML辑软件&#xff08;如&#xff1a;Dreamweaver、HBuilder、Vscode 、Sublime 、Webstorm、Text 、Notepad 等任意html编辑软件进行运行及修改编辑等操作&#xff09;。 获取源码 1&#…

MemFire Cloud为何短短几年变成这样?

在软件开发的世界里&#xff0c;总有一些工具能够迅速崛起&#xff0c;成为开发者们心中的宠儿。MemFire Cloud&#xff0c;就是这样一个在短短几年内迅速崭露头角的存在。它不仅改变了开发者的工作方式&#xff0c;更成为了独立开发者的得力助手。今天&#xff0c;我们就来聊聊…

FALCON:打破界限,粗粒度标签的无监督细粒度类别推断,已开源| ICML‘24

在许多实际应用中&#xff0c;相对于反映类别之间微妙差异的细粒度标签&#xff0c;我们更容易获取粗粒度标签。然而&#xff0c;现有方法无法利用粗标签以无监督的方式推断细粒度标签。为了填补这个空白&#xff0c;论文提出了FALCON&#xff0c;一种从粗粒度标记数据中无需细…

CentOS7虚拟机下安装及使用Docker

文章目录 一&#xff0c;准备工作二、安装Docker三、启动Docker四、验证Docker五、使用Docker六&#xff0c;卸载Docker 有一个Centos7的虚拟机&#xff0c;想要安装个docker测试一些docker用法和熟悉命令 一&#xff0c;准备工作 1&#xff0c;使用uname -r命令检查系统内核…

价值流架构指南:构建业务创新与竞争优势的全面方法论

如何通过价值流引领企业数字化转型&#xff1f; 在当前数字化转型的背景下&#xff0c;企业面临的挑战日益复杂化&#xff1a;如何更快响应市场变化&#xff1f;如何优化资源配置提升效率&#xff1f;如何确保客户体验始终处于行业领先&#xff1f;《价值流指南》由The Open G…

Java实现简易计算器功能(idea)

目的&#xff1a;写一个计算器&#xff0c;要求实现加减乘除功能&#xff0c;并且能够循环接收新的数据&#xff0c;通过用户交互实现。 思路&#xff1a; &#xff08;1&#xff09;写4个方法&#xff1a;加减乘除 &#xff08;2&#xff09;利用循环switch进行用户交互 &…

解决windows中项目启动端口被占用报错

1.在启动项目时报错端口被占用 解决: 1.cmd命令行输入命令: netstat -ano | findstr “8000” 2.可以看到PID为8448&#xff0c;下面只要将该PID关闭即可&#xff0c;有两种方法: ①在任务管理器中找到PID为8448的服务结束 ②命令行输入: >taskkill /PID 8448 /F 3.再查…

关于低代码平台几个新技术应用的实践体验

最近在整理平台的基本功能使用体验&#xff1a; 1&#xff0c;使用低码平台&#xff0c;创建用户业务站点交互原型&#xff0c;基本是可行的。虽然相对于专业的 墨刀、蓝湖、figma 等在用户体验上还有差距&#xff0c;但对于普通应用差别不大。 2&#xff0c;根据UI/UE原型&am…

力扣(LeetCode)每日一题 2181. 合并零之间的节点

题目链接https://leetcode.cn/problems/merge-nodes-in-between-zeros/description/?envTypedaily-question&envId2024-09-09 题目描述 给你一个链表的头节点 head &#xff0c;该链表包含由 0 分隔开的一连串整数。链表的 开端 和 末尾 的节点都满足 Node.val 0 。 对…

HNU-2023电路与电子学-CPU综合设计

写在前面&#xff1a; 本次实验是课程的最后一次实验&#xff0c;要求按照指导书的说明将之前的板块整合成一个完整的CPU&#xff0c;建议大家每连接一个板块都进行一次仿真验证&#xff0c;保证能正常运行且功能正常&#xff0c;如果等到CPU组装好再调试工作量较大并且有些错…

如何恢复最近删除的文件[Windows Mac]

可以通过多种方式删除文件。因此&#xff0c;用户需要恢复他们不小心删除的文件的情况并不少见。 好消息是&#xff0c;用户至少通常可以在删除最近删除的文件后几天或几周内恢复它们。 回收站是 Windows 中的文件删除保护措施&#xff0c;可以轻松恢复文件。 除非另有配置&…

第二证券:科创板股票交易规则,科创板新手可以买吗?

科创板是独立于现有主板商场的特别板块&#xff0c;面向的是国际科技前沿、经济主战场、国家严峻需求&#xff0c;首要服务于契合国家战略、打破要害核心技术、商场认可度高的科技立异企业。 科创板是独立于现有主板商场的特别板块&#xff0c;面向的是国际科技前沿、经济主战…

二叉树 - 验证二叉搜索树

98. 验证二叉搜索树 方法一&#xff1a;辅助数组 /*** Definition for a binary tree node.* function TreeNode(val, left, right) {* this.val (valundefined ? 0 : val)* this.left (leftundefined ? null : left)* this.right (rightundefined ? null :…

WPF中创建横向的ListView

在WPF中&#xff0c;要创建横向的ListView&#xff0c;您可以通过设置ItemsControl的ItemsPanel来改变其项的排列方向。以下是一个简单的示例&#xff0c;展示了如何将ListView的项横向排列&#xff1a; 在这个例子中&#xff0c;WrapPanel用于横向排列其子元素&#xff0c;而…

本地Linux服务器使用docker搭建DashDot并实现公网实时监测服务器信息

文章目录 前言1. 本地环境检查1.1 安装docker1.2 下载Dashdot镜像 2. 部署DashDot应用3. 本地访问DashDot服务4. 安装cpolar内网穿透5. 固定DashDot公网地址 前言 本篇文章我们将使用Docker在本地部署DashDot服务器仪表盘&#xff0c;并且结合cpolar内网穿透工具可以实现公网实…

C语言 ——— 学习并使用条件编译指令

目录 何为条件编译指令 常见的条件编译指令 学习条件编译指令 使用条件编译指令 在程序预编译阶段&#xff0c;条件编译指令的代码转换 多分支的条件编译指令 何为条件编译指令 在编译一个程序的时候&#xff0c;如果要将一条语句&#xff08;一组语句&#xff09;选择编…

Redis学习Day2——Redis基础使用

扩展阅读推荐: Redis 教程 | 菜鸟教程 (runoob.com) 黑马程序员Redis入门到实战教程_哔哩哔哩_bilibili 细说 Redis 九种数据类型和应用场景_redis数据类型及应用场景-CSDN博客 一、命令篇 1.1 Redis的命令分类 Redis是典型的K-V型数据库,key标识字符串,而value包含了很…

React Native 0.76版本发布

关于 React Native 的 New Architecture 概念&#xff0c;最早应该是从 2018 年 RN 团队决定重写大量底层实现开始&#xff0c;因为那时候 React Native 面临各种结构问题和性能瓶颈&#xff0c;最终迫使 RN 团队开始进行重构。 而从 React Native 0.68 开始&#xff0c;New A…

在B端管理系统中,复杂或者DIY功能,都依赖哪些编辑器/设计器

一、引言 在当今的商业环境中&#xff0c;B 端管理系统扮演着至关重要的角色。这些系统不仅需要满足企业日常的运营管理需求&#xff0c;还需要具备足够的灵活性和可扩展性&#xff0c;以适应不断变化的业务需求。而在实现复杂或可 DIY 的功能方面&#xff0c;各种编辑器和设计…