基础算法(3)——二分

news2024/12/24 21:01:28

1. 二分查找

题目描述:

解法一:暴力解法(时间复杂度O(N))

循环遍历数组,找到相等的值返回下标即可

解法二:二分查找

“有序数组可使用二分查找”,这句话是不严谨的,应该是具有 “二段性” 的数组都可以使用二分查找

细节:

1) 循环结束的条件:left > right,因为 [left, right] 中的元素都是未知的,并没有判断是否满足条件,因此当 left == right 时,也许判断

2) 二分查找的正确性:因为在本题中,数组是有序的,如:nums = [-1,0,3,5,9,12], target = 9,当取到值 5 时,小于 9,因为数组有序,所有 5 前面的值相当于已经判断过了,肯定小于 9,一步做完了暴力枚举的很多工作,因为暴力枚举可以完成本题,所以二分查找是可以正确解决问题的

3) 时间复杂度:

当暴力枚举时间复杂度为 2 的 32 次方时,二分查找只需要查找 32 次,时间复杂度比暴力枚举小非常多

代码实现:
class Solution {
    public int search(int[] nums, int target) {
        int n = nums.length;
        //初始化 left 和 right 指针
        int left = 0;
        int right = n - 1;
        //由于两个指针相交时,当前元素还未判断,因此判断条件需要取等号
        while (left <= right) {
            //先找到中间元素
            //int mid = (left + right)/2; //该种写法有溢出风险(如:若数组长度为2的32次方,相加的话有可能会超出 int 类型能存储的极限
            int mid = left + (right - left) / 2; //让 left + 整个数组的一半长度来取中间值,肯定不会溢出
            //分三种情况讨论
            if (nums[mid] > target) {
                right = mid - 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }
}

tip:计算中间值时,使用 (left + right) / 2,这种写法有溢出风险,如:若数组长度为 2 的 32 次方,相加的话有可能会超出 int 类型能存储的极限

总结朴素二分查找模板:

while (left <= right) {
    int mid = left + (right - left) / 2; //数组长度为偶数时,该种写法会取到数组中间靠左的元素
    //int mid = left + (right - left + 1) / 2; //这两种写法都能完成任务,只是当数组长度为偶数时会有所差别,该种写法会取到数组中间靠右的元素

    if (...)
        left = mid + 1;
    else if (...)
        right = mid - 1;
    else
        return ...;
}

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

题目描述:

解法一:暴力查找(时间复杂度O(N)

遍历数组找到目标值在数组中的开始和结束位置即可

解法二:二分

朴素二分代码实现:

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int n = nums.length;
        int left = 0;
        int right = n - 1;
        int[] arr = {-1, -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 {
                left = mid - 1;
                right = mid + 1;
                while (left >= 0 && nums[left] == target) {
                    left--;
                }
                while (right < n && nums[right] == target) {
                    right++;
                }
                arr[0] = left + 1;
                arr[1] = right - 1;
                return arr;
            }
        }
        return arr;
    }
}

朴素二分在极端情况下,如:数组全是 3,要查找 3 时,时间复杂度和暴力查找是一样的,都是O(N)


优化:根据数组的“二段性”,分别二分查找其左右端点

算法思路:
第一步:查找区间的左端点

可分为两种情况:
1. x < t:left = mid + 1(当 x 小于 t 时,左侧的值肯定都小于 t,所以 left 可以直接跳到 mid 的下一个位置)

2. x >= t:right = mid
(因为这一步是查找区间的左端点,所以把大于和等于放一起,当 x = t 时,像本例中,不一定是哪一个 3,若正好是左端点的 3,此时 right = mid - 1 的话,就把左端点错过了)


细节处理:

1) 循环条件必须设置为 left < right,因为 left = right 时就是最终结果,无需判断

2) 求中点的操作

mid 必须为:left + (right - left) / 2,否则死循环


第二步:查找区间右端点

可分为两种情况:
1. x <= t:left = mid

因为此步骤是为求右端点,因此右端点以前的 target 不重要,所以将 小于和等于 这两个判断条件放到一起,只是为了避免出现当前下标处就是右端点时,操作 left = mid + 1 错过右端点

2. x > t:right = mid - 1

当 x 大于 t 时,右侧的值肯定都大于 t,因此 right 可以直接跳到 mid 的前一个位置


细节处理:

1) 循环条件必须设为 left < right,原因同上

2) 求中点的方式

mid 必须为:left + (right - left + 1) / 2,否则死循环


代码实现:
//根据数组的 “二段性”,分别二分查找其左右端点
class Solution {
    public int[] searchRange(int[] nums, int target) {
        int[] arr = {-1, -1};
        if (nums.length == 0) { //处理边界
            return arr;
        }
        //二分左端点
        int left = 0;
        int right = nums.length - 1;
        while (left < right) { //细节1:此处判断条件不能是小于等于,否则死循环
            //细节2:此处不能用 left + (right - left + 1) / 2,否则死循环
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) { //细节3:区分二分左端点和右端点此处的条件
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        //循环结束后,left == right
        //判断一下当前下标处是否为 target,若不是则该数组中不存在目标值
        if (nums[left] != target) {
            return arr;
        }
        //若当前下标处等于 target,则此下标即为目标值在数组中的开始位置
        arr[0] = left;

        //二分右端点,已经找到左端点了,left 没必要从 0 开始了,只需将 right 重置即可
        right = nums.length - 1;
        while (left < right) {
            //细节4:此处不能用 left + (right - left) / 2,否则死循环
            int mid = left + (right - left + 1) / 2;
            if (nums[mid] <= target) { //细节5:同细节3
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        //循环结束后,left == right,此时下标即为目标值在数组中的结束位置
        arr[1] = right;
        return arr;
    }
}

总结二分模板

3. 搜索插入位置

题目描述:

解法:二分查找算法

算法思路:

代码实现:
class Solution {
    public int searchInsert(int[] nums, int target) {
        int n = nums.length;
        int left = 0;
        int right = n - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            //将数组分为两个区间
            //第一个区间都是小于 target 的,当 left 下标处值小于 target 时,left = mid + 1
            if (nums[mid] < target) {
                left = mid + 1;
            } else { //第二个区间是大于等于 target 的,当 righ 下标处值大于等于 target 时,right = mid
                right = mid;
            }
        }
        if (nums[right] < target) { //当循环结束,没有找到目标值,说明需要在数组末尾插入
            return right + 1;
        }
        return right;
    }
}

4. x 的平方根

题目描述:

解法一:暴力枚举

算法思路:

代码实现:
//暴力枚举
class Solution {
    public int mySqrt(int x) {
        //为避免两个较大的数超出 int 的范围,此处定义为 long 类型
        for (long i = 0; i <= x; i++) {
            if (i * i > x) { //如果两个数相乘大于 x,返回 i - 1
                return (int)i - 1;
            } else if (i * i == x) { //如果两个数相乘等于 x,返回 i
                return (int)i;
            }
        }
        return -1;
    }
}

解法二:二分查找

算法思路:

代码实现:
//二分查找
class Solution {
    public int mySqrt(int x) {
        //当 x 小于 1 时,直接返回 0
        if (x < 1) {
            return 0;
        }
        long left = 1; //left 从 1 开始,且需用 long 类型
        long right = x;
        while (left < right) {
            long mid = left + (right - left + 1) / 2; //下面会涉及到 *,所以使用 long 类型防溢出
            if (mid * mid <= x) {
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        return (int)left;
    }
}

5. 山峰数组的峰顶索引

题目描述:

解法一:暴力枚举

代码实现:
//暴力枚举
class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int max = -1;
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > arr[i-1]) {
                max = i;
            }
        }
        return max;
    }
}

解法二:二分查找

算法思路:

class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int left = 1; //题目规定第一个位置和最后一个位置绝无可能满足要求,因此可跳过
        int right = n - 2;
        while (left < right) {
            int mid = left + (right - left + 1) / 2;
            if (arr[mid] > arr[mid - 1]) {
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        return left;
    }
}

6. 寻找峰值

题目描述:

解法一:暴力枚举

算法思路:

//暴力枚举
class Solution {
    public int findPeakElement(int[] nums) {
        int max = 0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] > nums[max]) {
                max = i;
            }
        }
        return max;
    }
}

解法二:二分查找

算法思路:

代码实现:
//二分查找
class Solution {
    public int findPeakElement(int[] nums) {
        int n = nums.length;
        int left = 0;
        int right = n - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums[mid + 1]) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }
}

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

题目描述:

解法一:暴力枚举(时间复杂度O(N)

代码实现:
//暴力枚举
class Solution {
    public int findMin(int[] nums) {
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] < min) {
                min = nums[i];
            }
        }
        return min;
    }
}

解法二:二分查找

题目中的数组规则如下图所示:

上图中 C 点就是我们要求的点

二分的本质:找到一个判断标准,使得查找区间能够一分为二

通过图像我们可以发现,[A, B] 区间内的点都是严格大于 D 点值的,C 点的值是严格小于 D 点值的,但是当 [C, D] 区间只有一个元素的时候,C 点的值是可能等于 D 点值的

因此,初始化左右两个指针 left、right,

然后根据 mid 的落点,可以如下划分下一次查询的区间:

当 mid 在 [A, B] 区间的时候,也就是 mid 位置的值严格大于 D 点的值,下一次查询区间在 [mid + 1, right] 上

当 mid 在 [C, D] 区间的时候,也就是 mid 位置的值严格小于等于 D 点的值,下次查询区间在 [left, mid] 上

当区间长度变为 1 的时候,就是要查询的结果

代码实现:
class Solution {
    public int findMin(int[] nums) {
        int n = nums.length;
        int left = 0;
        int 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];
    }
}

以 A 点为参照物的解法

上面解法是以 D 点当作参照物,若是以 A 点当作参照物的话,需要考虑一种特殊情况:

nums = [0,1,2,4,5,6,7] 若是旋转 7 次
依旧得到 nums = [0,1,2,4,5,6,7]
此时 A 点的值严格小于 D 点

如果是上面的特殊情况,若是以 A 点作为参照物的话,会出现错误答案

将特殊情况特殊处理即可

代码实现:
class Solution {
    public int findMin(int[] nums) {
        int n = nums.length;
        int left = 0;
        int right = n - 1;
        //特判
        if (nums[0] < nums[n - 1]) {
            return nums[0];
        }
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < nums[0]) {
                right = mid;
            } else { //元素值 互不相同 的数组,没有相等的情况
                left = mid + 1;
            }
        }
        return nums[left];
    }
}

8. 点名

题目描述:

解法一:暴力枚举

//暴力枚举
class Solution {
    public int takeAttendance(int[] records) {
        int n = records.length;
        int i = 0;
        for (; i < n; i++) { //遍历数组
            if (records[i] != i) { //若当前下标处值与下标不同,此时下标值即为缺席同学学号
                return i;
            }
        }
        //题目说明,肯定有一个同学缺席
        //若循环结束仍未返回,则缺席同学学号为最后一位,也就是当前 i 的值
        return i;
    }
}

解法二:哈希表

//哈希表
class Solution {
    public int takeAttendance(int[] records) {
        int n = records.length;
        int[] hash = new int[n + 1]; //创建一个 n + 1 大小的数组模拟哈希表
        for (int i = 0; i < n; i++) { //遍历 records,使其中元素作为下标,让 hash 中对应下标处值 ++
            hash[records[i]]++;
        }
        int j = 0;
        for (; j < hash.length; j++) { //循环遍历 hash,其中值为 0 的下标即为缺席同学学号
            if (hash[j] == 0) {
                return j;
            }
        }
        //若判断条件为 j < hash.length,代码不会走到这
        //若判断条件为 j < n,代码可能会走到这
        return j;
    }
}

解法三:位运算

//异或运算
class Solution {
    public int takeAttendance(int[] records) {
        int n = records.length;
        int sum = -1;
        int i = 0;
        for (; i < n; i++) {
            //若两数相同,则异或等于 0
            //若两数不同,则异或不等于 0,说明此时 i 即为缺席的同学学号
            sum = i ^ records[i];
            if (sum != 0) {
                return i;
            }
        }
        //代码走到这,说明缺少的是最后一位学号
        return i;
    }
}

解法四:数学(高斯求和公式)

//数学(高斯求和公式,也就是等差数列求和公式
class Solution {
    public int takeAttendance(int[] records) {
        int n = records.length;
        //等差数列求和公式:首项 + 尾项 乘以 项数 除以 2
        int sum = (0 + n) * (n + 1) / 2;
        //让所有学号之和 减去 当前数组中存在的学号,剩下的就是缺席的
        for (int i = 0; i < n; i++) {
            sum -= records[i];
        }
        return sum;
    }
}

前四种解法的时间复杂度都是O(N)

解法五:二分查找(时间复杂度:O(\log N)

算法原理:

代码实现:
class Solution {
    public int takeAttendance(int[] records) {
        int n = records.length;
        int left = 0;
        int right = n - 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;
    }
}

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

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

相关文章

nodejs+express+vue教辅课程辅助教学系统 43x2u前后端分离项目

目录 技术栈具体实现截图系统设计思路技术可行性nodejs类核心代码部分展示可行性论证研究方法解决的思路Express框架介绍源码获取/联系我 技术栈 该系统将采用B/S结构模式&#xff0c;开发软件有很多种可以用&#xff0c;本次开发用到的软件是vscode&#xff0c;用到的数据库是…

蚂蚁在 RAG 与向量检索上的实践:技术应用与创新分析

引言 在AI技术迅猛发展的背景下&#xff0c;如何有效地处理海量数据成为了技术创新的关键问题。向量数据库和RAG&#xff08;Retrieval-Augmented Generation&#xff09;技术结合&#xff0c;为提升生成式AI应用的准确性和实时性提供了有效的解决方案。本文结合蚂蚁集团在向量…

宠物空气净化器该怎么选?希喂、352、霍尼韦尔哪款对吸附浮毛有效

明明我都成年很久了&#xff0c;我爸妈还把我当小孩一样&#xff0c;我干什么前都要和他们说一声。前段时间去朋友家玩&#xff0c;本来对宠物无感的我一下子就被她家可爱的猫咪萌化了。猫咪好可爱呀&#xff0c;毛茸茸的摸起来很舒服&#xff0c;眨巴的大眼睛看着你真的心软软…

荣誉 | 分贝通入选2024「Cloud 100 China」

近日,2024 Cloud 100 China 榜单于美高梅酒店正式发布,这是靖亚资本和崔牛会联合推出的第三届榜单。 全球商旅管理、企业支出全流程管控、数据BI全方位降本、AI赋能高效出行体验.......近年来,分贝通不断精进产品能力及BI&AI能力,再次上榜。 本届评选,组委会基于过去一年融…

【MRI数据】LEMON MRI 数据集下载

本文介绍使用cyberduck软件下载 LEMON MRI 数据。 数据简介 LEMON MRI 数据官网&#xff1a;https://fcon_1000.projects.nitrc.org/indi/retro/MPI_LEMON.html 提供了 228 名健康参与者的公开数据集&#xff0c;包括年轻人&#xff08;N154&#xff0c;25.13.1 岁&#xff0…

ModbusTCP报文详解

Modbus TCP与Modbus Rtu(ASCI)数据帧的区别 总结&#xff1a;Modbus TCP就是在Modbus Rtu(ASCI)基础上去掉CRC&#xff0c;再加上六个0一个6 Modbus TCP MBAP报文头 域长度描述客户机服务器事务处理标识符2字节Modbus请求/响应事务处理的识别客户机启动服务器从接收的请求中重…

vue3中如何拿到vue2中的this

vue3中常用api vue3中常用响应式数据类型&#xff1a;

【计算机网络】详解TCP/IP分层模型局域网和跨网络通信的原理

一、网络协议 两个概念&#xff1a;交换机&#xff1a;实现位于同一个子网中的主机数据交换。路由器&#xff1a;实现数据包的跨网络转发。 两台主机的距离变远了&#xff0c;会引发出一系列问题&#xff1a; 1、如何使用数据的问题 2、数据的可靠性问题 3、主机定位问题 4、…

<<编码>> 第 14 章 反馈与触发器(2)--或非门反馈 示例电路

或非门反馈电路 info::操作说明 先闭合上面的开关(置位 Set), 此时输出高电平 再断开上面的开关, 因反馈的存在, 输出保持为高电平 闭合下面的开关(复位 Reset), 输出重新回到低电平 断开下面的开关, 输出继续保持低电平 primary::在线交互操作链接 https://cc.xiaogd.net/…

产品经理有必要学习大模型技术吗?

第一&#xff0c;大模型正在成为各类产品的核心组件&#xff0c;颠覆了传统产品和应用生态&#xff0c;进入AI大模型应用的新阶段。 例如&#xff0c;NewBing、Perplexity等AI搜索产品已经颠覆了传统搜索引擎的搜索模式&#xff0c;用户不用搜索后再点开排序靠前的网页链接&am…

Linux常见查看文件命令

目录 一、cat 1.1. 查看文件内容 1.2. 创建文件 1.3. 追加内容到文件 1.4. 连接文件 1.5. 显示多个文件的内容 1.6. 使用管道 1.7. 查看文件的最后几行 1.8. 使用 -n 选项显示行号 1.9. 使用 -b 选项仅显示非空行的行号 二、tac 三、less 四、more 五、head 六、…

十八,Spring Boot 整合 MyBatis-Plus 的详细配置

十八&#xff0c;Spring Boot 整合 MyBatis-Plus 的详细配置 文章目录 十八&#xff0c;Spring Boot 整合 MyBatis-Plus 的详细配置1. MyBatis-Plus 的基本介绍2. Spring Boot 整合 MyBatis Plus 的详细配置3. Spring Boot 整合 MyBatis plus 注意事项和细节4. MyBatisx 插件的…

《微处理器系统原理与应用设计第十三讲》通用同/异步收发器USART轮询模式应用设计

USART提供两设备之间的串行双工通信&#xff0c;并支持中断和DMA工作。采用轮询、中断和DMA三种方式进行数据收发。 一、功能需求 实现远程串行通信数据的回传确认。微处理器系统构成的测控设备通过USART&#xff08;串口&#xff09;与用户设备&#xff08;上位机&#xff0…

学习使用SQL Server Management Studio (SSMS)

SQL Server Management Studio (SSMS) 是一个集成环境&#xff0c;用于管理任何SQL基础设施&#xff0c;从SQL Server到Azure SQL数据库。SSMS提供了各种工具来配置、监控和管理SQL Server的实体和组件。以下是一篇详细的使用指南&#xff0c;涵盖了SSMS的主要功能和操作。 1.…

感谢问界M9一打二十,让我们买到这么便宜的BBA

文 | AUTO芯球 作者 | 雷慢 国产豪华车&#xff0c;终于扬眉吐气了&#xff0c; 你敢信吗&#xff1f;在50万以上豪华车中&#xff0c; 现在问界M9一款车的月销量&#xff0c; 是其他前20名销量的总和&#xff01; 要知道&#xff0c;它的对手是各种宝马、奔驰、雷克萨斯的…

基于python+django+vue的医院预约挂号系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于协同过滤pythondjangovue…

APP测试基本流程与APP测试要点总结

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 APP测试实际上依然属于软件测试的范畴&#xff0c;是软件测试的一个真子集&#xff0c;所以经典软件测试理论&#xff0c;依然是在APP测试中有效的&#xff0c;只…

RocketMQ实战与集群架构详解

目录 一、MQ简介 MQ的作用主要有以下三个方面 二、RocketMQ产品特点 1、RocketMQ介绍 2、RocketMQ特点 三、RocketMQ实战 1、快速搭建RocketMQ服务 2、快速实现消息收发 1. 命令行快速实现消息收发 2. 搭建Maven客户端项目 3、搭建RocketMQ可视化管理服务 4、升级分…

镀金引线---

一、沉金和镀金 沉金和镀金都是常见的PCB金手指处理方式&#xff0c;它们各有优劣势&#xff0c;选择哪种方式取决于具体的应用需求和预算。 沉金&#xff08;ENIG&#xff09;是一种常用的金手指处理方式&#xff0c;它通过在金手指表面沉积一层金层来提高接触性能和耐腐蚀性…

【鸿蒙 HarmonyOS NEXT】popup弹窗

一、背景 给组件绑定popup弹窗&#xff0c;并设置弹窗内容&#xff0c;交互逻辑和显示状态。 常见场景&#xff1a;点击按钮弹出popup弹窗&#xff0c;并对弹窗的内容进行交互逻辑处理&#xff0c;如&#xff1a;弹窗内点击跳转到其他页面 二、给组件绑定Popup弹窗 PopupOp…