算法之二分查找算法

二分查找算法简介

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

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

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;
            else
                right = 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;
            else
                left = mid;
        }
        if(nums[right] == target)
            end = right;

        return {begin,end};
    }
};

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

//寻找区间左端点
        while(left < right)
        {
            int mid = left + (right-left)/2;
            if(...)
                left = mid+1;
            else
                right = mid;
        }
 //寻找区间右端点
        while(left < right)
        {
            int mid = left + (right-left+1)/2;
            if(...)
                right = mid - 1;
            else
                left = 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;
            else
                left = 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;
            else
                left = 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;
            else
                left = 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;
            else
                right = 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;
            else
                right = mid;
        }
        if(records[left] == left)
            return left+1;
        return left;
    }
};

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

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

2. 直接遍历找结果

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

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


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

运维自动化之ansible

pxe 一键安装操作系统 操作系统只是提供一个平台 lnmp 需要多软件协同完成的一个简单项目 服务器正常运行 日常运维 巡检 服务器上的软件正常运行 zabbix 普罗米修斯 系统调优&#xff0c;架构调优 云计算核心职能 搭建平台架构 日常运营保障 性能效率优化 相关工具 代…

SDWAN专线对企业接入有门槛吗

SD-WAN&#xff08;软件定义广域网&#xff09;技术作为一种新型的网络解决方案&#xff0c;正在成为企业网络接入的热门选择。然而&#xff0c;对于企业来说&#xff0c;接入SD-WAN专线是否存在门槛&#xff0c;是一个值得探讨的问题。本文将从不同角度分析SD-WAN专线对企业接…

HTML 学习笔记(十一)表单

一、分块 1.单行文本框控件–文本框和密码框 文本框控件通过单标签input实现&#xff0c;其具有必要属性type来控制输入控件的类型(默认为text即文本信息)&#xff0c;密码框的type为password(口令)。   表单的动作属性定义了目的文件的文件名。由动作属性定义的这个文件通常…

国内可用免费AI工具集

1、Kimi Chat 由月之暗面科技有限公司&#xff08;Moonshot AI&#xff09;开发的人工智能助手。擅长中英文对话&#xff0c;能够提供安全、有帮助且准确的回答。它的能力包括阅读和理解用户上传的文件&#xff0c;访问互联网内容&#xff0c;以及结合搜索结果来回答问题。比如…

【C#】WPF获取屏幕分辨率

SystemParameters提供的接口&#xff0c;其实是获取渲染过程中的实际高宽&#xff0c;是受系统DPI设置的影响。 以 1920 * 1080 和 125% DPI为例&#xff1a; 分辨率高度&#xff1a;1080&#xff0c;实际获取的高度为&#xff1a;864。 分辨率宽度&#xff1a;1920&#xff…

高项-项目整合管理

项目整合管理的目标 资源分配平衡竞争性需求研究各种备选方法裁剪过程以实现项目目标管理各个项目管理知识域之间的依赖关系 项目整合管理的过程 制定项目章程制定项目管理计划指导与管理项目工作管理项目知识监控项目工作实施整体变更控制结束项目或阶段 七个过程和五大过…

【1688运营】如何拆解竞争对手店铺和单品数据?

关注竞争对手数据是1688运营中不可或缺的一环&#xff0c;它有助于企业更好地了解市场环境、发现市场机会、学习成功经验、预测市场变化以及提升竞争力。以下是一些建议&#xff0c;帮助你全面、深入地分析竞争对手的店铺和单品数据&#xff1a; 1、监控店铺数据 可以通过店雷…

求第n个斐波那契数--c语言

用递归的方法&#xff1a; //用递归求第n个斐波那契数 int fib(int n){if(n<2){return 1;}else{return fib(n-1)fib(n-2); } } #include<stdio.h> int main(){int n0;printf("请输入n的值&#xff1a;");scanf("%d",&n);int result fib(n);…

未来艺术展览新趋势——3D线上画展如何创新展示?

一、艺术展示的数字化转型 随着科技的不断进步&#xff0c;3D线上画展作为艺术展示的新趋势&#xff0c;正逐渐改变着人们欣赏和购买艺术作品的方式。对于画家而言&#xff0c;3D线上画展不仅提供了一个全新的平台来展示他们的作品&#xff0c;还开辟了销售渠道&#xff0c;扩大…

面向对象技术(第一周)

目录 ⚽前言 &#x1f3d0;面向对象思想 起源 现实 编程联系 面向对象思想总结 &#x1f3c0;面向对象开发方法 开发中的名词&#xff1a; 名词间的关系 名词具体阐释 一、对象 二、消息和方法&#xff1a; 前言 本文所有知识点和内容均来自山东大学潘丽老师及山东…

京津冀协同发展:北京·光子1号金融算力中心——智能科技新高地

京津冀协同发展是党中央在新的历史条件下提出的一项重大国家战略&#xff0c;对于全面推进“五位一体”总体布局&#xff0c;以中国式现代化全面推进强国建设、民族复兴伟业&#xff0c;具有重大现实意义和深远历史意义。随着京津冀协同发展战略的深入推进&#xff0c;区域一体…

unique_ptr使用说明

背景 指针问题一直是一个比较麻烦的事情&#xff0c;比如很多人说要用智能指针完全替换掉裸指针&#xff0c;有人说要用unique_ptr, 有人建议shared_ptr,可是实际看各种经典框架&#xff0c;发现一个框架什么指针都有&#xff0c;使用的方法也是无法八门&#xff0c;这里简单说…

可访问性使命:Facebook构建无障碍社交空间

在当今数字化时代&#xff0c;社交媒体已成为人们日常生活的重要组成部分&#xff0c;而Facebook作为全球最大的社交平台之一&#xff0c;其使命不仅在于连接世界&#xff0c;还在于构建一个无障碍的社交空间&#xff0c;让每个人都能参与其中。本文将深入探讨Facebook在可访问…

几个增强诊断详解

几个增强诊断 基于CAN线 ISO15031-5是排放相关的应用层协议&#xff0c;它不关心我们使用K线还是CAN线&#xff0c;主要用于监控车辆基本参数&#xff0c;例如监控里程、车速&#xff1b;用于监控排放相关的参数&#xff0c;比如各种尾气的含量&#xff0c;氧含量等等&#xf…

红队笔记7--Web机器为Linuxdocker逃逸

其实&#xff0c;不知道大家有没有想过&#xff0c;我们之前练习的都是web机器是windows的版本&#xff0c;但是其实&#xff0c;在现实生活中&#xff0c;服务器一般都是Linux的版本&#xff0c;根本不可能用到windows的版本 那么如果是Linux的话&#xff0c;我们就有很多的困…

【正点原子STM32探索者】CubeMX+Keil开发环境搭建

文章目录 一、简单开箱二、资料下载三、环境搭建3.1 安装Keil MDK3.2 激活Keil MDK3.3 安装STM32CubeMX3.4 安装STM32F4系列MCU的Keil支持包 四、GPIO点灯4.1 查阅开发板原理图4.2 创建STM32CubeMX项目4.3 配置系统时钟和引脚功能4.4 生成Keil项目4.5 打开Keil项目4.6 编译Keil…

K8s的kubeadm方式部署集群实例

目录 一、准备环境 主机清单 修改主机名 设置防火墙、selinux状态 主机名解析 固定ip 重启网卡 同步时间 关闭swap分区 二、获取镜像 三、安装docker 四、配置kubeadm源 安装依赖包及常用插件 1.配置kubeadm源&#xff0c;安装对应版本 2.加载相关ipvs模块 3.配…

Day17:开发流程、开发社区首页、项目的调试、版本控制

开发流程 一次请求过程 先开发DAO&#xff0c;再开发service&#xff0c;再开发controller 开发社区首页的分布实现 显示前10个帖子 创建帖子数据表 CREATE TABLE discuss_post (id int NOT NULL AUTO_INCREMENT,user_id varchar(45) DEFAULT NULL,title varchar(100) DEF…

使用Java的等待/通知机制实现一个简单的阻塞队列

Java的等待/通知机制 Java的等待通知机制是多线程间进行通信的一种方式。 有三个重要的方法&#xff1a;wait()&#xff0c;notify() 和以及notifyAll() wait()&#xff1a;该方法用于让当前线程&#xff08;即调用该方法的线程&#xff09;进入等待状态并且释放掉该对象上的…

SpringBoot中配置nacos

SpringBoot中配置nacos 在SpringBoot中使用nacos一定要注意name&#xff0c;使用openfeign特别要注意这个点。 spring:application:name: item-service需要的依赖包 config需要引入的包 <dependency><groupId>com.alibaba.cloud</groupId><artifactId…