搜索插入位置[简单]

在这里插入图片描述

一、题目

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为O(log n)的算法。

示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2

示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1

示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4

1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums为 无重复元素 的 升序 排列数组
-104 <= target <= 104

二、代码

【1】二分查找: 假设题意是叫你在排序数组中寻找是否存在一个目标值,那么训练有素的读者肯定立马就能想到利用二分法在O(log⁡n)的时间内找到是否存在目标值。但这题还多了个额外的条件,即如果不存在数组中的时候需要返回按顺序插入的位置,那我们还能用二分法么?答案是可以的,我们只需要稍作修改即可。

考虑这个插入的位置pos,它成立的条件为:nums[pos−1]<target≤nums[pos]其中nums代表排序数组。由于如果存在这个目标值,我们返回的索引也是pos,因此我们可以将两个条件合并得出最后的目标:「在一个有序数组中找第一个大于等于target的下标」。

问题转化到这里,直接套用二分法即可,即不断用二分法逼近查找第一个大于等于target的下标 。下文给出的代码是笔者习惯的二分写法,ans初值设置为数组长度可以省略边界条件的判断,因为存在一种情况是target大于数组中的所有数,此时需要插入到数组长度的位置。

class Solution {
    public int searchInsert(int[] nums, int target) {
        int n = nums.length;
        int left = 0, right = n - 1, ans = n;
        while (left <= right) {
            int mid = ((right - left) >> 1) + left;
            if (target <= nums[mid]) {
                ans = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return ans;
    }
}

时间复杂度: O(log⁡n),其中n为数组的长度。二分查找所需的时间复杂度为O(log⁡n)
空间复杂度: O(1)。我们只需要常数空间存放若干变量。

【2】思路分析: 在有序数组中查找符合条件的某个数(或者它的下标),可以使用二分查找。我们根据搜索区间[left..right]中间位置mid的值,判断下一轮搜索区间在哪里。根据「一、题意分析」中对示例的描述:(这里多说一句:下面的「情况 1」和「情况 2」的分析完全是分析本题题意得到的,如果有不太清楚的地方,把题目中的 3 个「示例」多看几遍。)
情况 1:如果当前mid看到的数值严格小于target,那么mid以及mid左边的所有元素就一定不是「插入元素的位置」,因此下一轮搜索区间是[mid + 1..right],下一轮把left移动到mid + 1位置,因此设置left = mid + 1
情况 2:否则。如果mid看到的数值大于等于target,那么mid可能是「插入元素的位置」,mid的右边一定不是「插入元素的位置」。如果mid的左边不存在「插入元素的位置」,我们才可以说mid是「插入元素的位置」。因此下一轮搜索区间是[left..mid],下一轮把right移动到mid位置,因此设置right = mid

说明:上面的两点中,「情况 2」其实不用分析得那么细致, 因为只要「情况 1」的区间分析是正确的,「情况 2」一定是「情况 1」得到的区间的反面区间。

看到一个数大于等于目标元素,此时不能说它一定是第一个大于等于目标元素的元素。
1、如果它的左边没有大于等于目标元素的元素,它才是第一个大于等于目标元素的元素;
2、如果它的左边有大于等于目标元素的元素,它不是第一个大于等于目标元素的元素。

就是这样的特点决定了:这个问题的答案有些时候需要再退出循环以后才能得到。

如果你非常清楚「二分查找」,或者看过我讲的「二分查找」的视频或者题解,下面的代码应该不难理解。我在「五、其它代码讲解」会说明:本题解里所有的代码其实是一样的,就只有一种解法,特殊情况和边界的分析也完全一样。

public class Solution {

    public int searchInsert(int[] nums, int target) {
        // 不用判断数组为空,因为题目最后给出的数据范围说数组不为空
        int len = nums.length;
        // 特殊判断
        if (nums[len - 1] < target) {
            return len;
        }

        // 程序走到这里一定有 nums[len - 1] >= target,插入位置在区间 [0..len - 1]
        int left = 0;
        int right = len - 1;
        // 在区间 nums[left..right] 里查找第 1 个大于等于 target 的元素的下标
        while (left < right) {
            int mid = (left + right) / 2;
            if (nums[mid] < target){
                // 下一轮搜索的区间是 [mid + 1..right]
                left = mid + 1;
            } else {
                // 下一轮搜索的区间是 [left..mid]
                right = mid;
            }
        }
        return left;
    }
}

说明:
1、while (left < right)表示当leftright重合的时候,我们找到了「目标值」的下标;
2、根据题意和示例,当代码中的「特殊判断」不成立时,区间里一定存在大于等于「目标值」的元素;
3、while循环里只把区间分成两个部分,退出循环的时候一定有left == right成立,所以leftright重合的这个位置就是问题的答案,因此返回left或者right都可以。

因为题目的最后说:nums中没有重复元素,所以可以在循环体里面加一个判断:

if (nums[mid] == target) {
    return mid;
}

时间复杂度: O(log⁡N),这里N是输入数组的长度;
空间复杂度: O(1)

既然len也有可能是答案,可以在初始化的时候,把right设置成len,在一开始的时候就不需要特殊判断了。

public class Solution {

    public int searchInsert(int[] nums, int target) {
        int len = nums.length;
        int left = 0;
        int right = len;
        // 在区间 nums[left..right] 里查找第 1 个大于等于 target 的元素的下标
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target){
                // 下一轮搜索的区间是 [mid + 1..right]
                left = mid + 1;
            } else {
                // 下一轮搜索的区间是 [left..mid]
                right = mid;
            }
        }
        return left;
    }
}

复杂度分析: (同参考代码 1)。

说明:
1、「参考代码 1」和「参考代码 2」其实是一版代码。「参考代码 1」其实就是做了先特殊的判断,特殊的判断不满足的时候,在一个更小的区间里做「二分查找」。
2、有的朋友把「参考代码 2」解释成:while (left < right)表示搜索区间是[left..right),所以初始化的时候right = len,这样的解释是大错特错的。错误的地方在于:while (left < right)表示搜索区间是[left..right),这句就不成立。while (left < right)只表示它本来的意思:循环可以继续的条件是left < right

初始化的时候设置right = len,是因为这道题搜索的右边界本来就是[0..len]

class Solution {
    public int searchInsert(int[] nums, int target) {
        int n = nums.length;
        int left = 0, right = n - 1, ans = n;
        while (left <= right) {
            int mid = ((right - left) >> 1) + left;
            if (target <= nums[mid]) {
                ans = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return ans;
    }
}

解释:
1、这里的ans的含义是,把有可能是题目答案的结果用ans保存起来,所以ans初始化的时候应该设置为n(根据 「示例」3);
2、在if语句里nums[mid] >= target的时候,mid位置有可能是问题的答案,所以需要用ansmid保存起来(根据「二、思路分析」「情况 2」);
3、最后要返回ans

由于有了ans变量,并且在ifelse语句中left一定是mid + 1right一定是mid - 1,这种情况不会出现死循环,所以while里面可以写left <= right

其实这里的「代码 1」和上面给出的「参考代码 1」「参考代码 2」是一样的。其中:
1、ans的含义是:搜索区间的右边界;
2、right的含义是: 搜索区间的右边界-1

class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while(left <= right) {
            int mid = (left + right) / 2;
            if(nums[mid] == target) {
                return mid;
            } else if(nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return left;
    }
}

解释:
1、这一版代码里,所有关于右边界的设置,都要比真正的右边界少 111。也就是说,如果作者分析出搜索区间是[10, 20],他在代码里设置left = 10, right = 19。除此之外,和「四、参考代码」里的「参考代码 2」是一样的;
2、因为数组的长度有可能是问题的答案,作者固定死了设置右边界的时候-1,所以初始化的时候right = nums.length - 1;
3、循环体里面 if (nums[mid] == target) 我前面分析过,直接返回 mid 就好了,这里没有设置右边界;
4、else if (nums[mid] < target)我前面也分析过,此时设置左边界;
5、elsenums[mid] > target的时候,此时本该设置right = mid,但是作者固定死了设置右边界的时候-1,所以这里写right = mid - 1;
6、因为固定死了设置右边界的时候-1,所以退出循环的时候,leftright不重合,rightleft1,所以返回left正确,返回right不正确,应该返回right + 1,大家可以自己验证。

为什么while (left <= right)正确,还是因为固定死了设置右边界的时候-1。当leftright重合的时候,虽然区间[left..right]里只有一个数,但是真正的右边界是right + 1,所以此时还应该继续搜索下去。

总结: 「二分查找」的写法很多、细节也很多。希望大家一定要有耐心,遇到问题的时候自己调试,把变量的值打印出来看一眼。

我相信,真正掌握「二分查找」的朋友,不是因为他(她)背下了「二分查找」的模板,而是他(她)对题目的意思有准确的理解。

就本题而言,一定要分析出:
1、数组的长度有可能是问题的答案,也就是nums.length有可能是问题的答案,如果不讨论,答案肯定错;
2、当nums[mid] >= target的时候,mid有可能是问题的答案,如果直接去掉,也肯定错,这一点在「三、本题的特点 」里专门强调过。

任何模板都不会覆盖上面的信息,上面也和大家解释了其它版本的代码正确也离不开对上面两点的分析,本质上本题解里出现的代码都是一样的,所以审题很重要。

我看到非常多的朋友说while (left < right)这种写法叫「左闭右开」,我在这里要很严肃地说一句:这完全没有根据。很多朋友不加思考地接受了这样的结论,所以导致它们在理解一些「二分查找」代码的时候逻辑上的混乱和矛盾。

即:while (left < right)」与「搜索区间为左闭右开」没有因果关系。
1、while (left < right)只表示它本来的意思,即:在left < right的时候循环可以继续,不能因为少了「等于」号,就说搜索区间为左闭右开[left..right)
2、什么叫区间左闭右开[left..right)左闭右开[left..right)等于左闭右闭区间[left..right - 1]

一个具体的问题,在条件确定的情况下,搜索的范围(区间)也是确定的。这个区间,你可以表示成「左闭右闭」区间(我的所有「二分查找」的题解里的所有代码)。也可以表示成「左闭右开」区间,比如本题解「五、其它代码讲解」的「代码 2」,把right设置成为真正的目标值存在区间的右边界 −1-

「二分查找」不会因为因为我们把区间表示成「左闭右闭」或者「左闭右开」而变得简单。大家会看到,表示成「左闭右开」反而更别扭。

很多朋友不假思索地在算法学习中应用别人写好的「代码模板」,如果只是为了把题目做对,有些时候是可以侥幸通过的。

在这个网站上,有很多种办法能让自己的代码通过测评,比起做对这些问题,我认为更重要的是解决问题的思想。希望大家在做题的时候,能够真的清楚每一行代码的意思。

讲解「二分查找」的人误导了很多人,希望大家能够仔细辨别。

如果你认为我在误导别人,首先恭喜你,质疑我本来就是你的权利和应该有的态度,我讲错的地方很多,被人纠正过很多次。其次,你可以有理有据说出我讲错的地方。我看到了,都会承认的。如果是恶意留言,我一条都不会回复。

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

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

相关文章

【linux开发工具】vim详解

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 “学如逆水行舟&#xff0…

项目02《游戏-11-开发》Unity3D

基于 项目02《游戏-10-开发》Unity3D &#xff0c; 任务&#xff1a;飞行坐骑 首先创建脚本&#xff0c; 绑定脚本&#xff0c; using UnityEngine; public class Dragon : MonoBehaviour{ [SerializeField] private float speed 10f; public Transfo…

如何判断线程池已经执行完所有任务了?

目录 不判断的问题 方法1&#xff1a;isTerminated 缺点分析 扩展&#xff1a;线程池的所有状态 方法2&#xff1a;getCompletedTaskCount 方法说明 优缺点分析 方法3&#xff1a;CountDownLatch&#xff08;推荐&#xff09; 优缺点分析 方法4&#xff1a;CyclicBar…

微软.NET6开发的C#特性——委托和事件

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;看到不少初学者在学习编程语言的过程中如此的痛苦&#xff0c;我决定做点什么&#xff0c;下面我就重点讲讲微软.NET6开发人员需要知道的C#特性&#xff0c;然后比较其他各种语言进行认识。 C#经历了多年发展…

计算机网络基本知识(二)

文章目录 概要分层为什么分层怎么分层&#xff1f;1.实体2.协议3.服务 分层基本原则正式认识分层详细例子解释 总结 概要 分层知识&#xff1a;概念理解 分层 为什么分层 大致以上五点 为了解决上面的问题&#xff08;复杂&#xff09; 大问题划分为小问题 怎么分层&#…

Stable Diffusion 模型下载:Disney Pixar Cartoon Type B(迪士尼皮克斯动画片B类)

文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八案例九案例十 下载地址 模型介绍 这是我之前的模型迪士尼皮克斯卡通类型A与我自己训练的Lora在中途旅程图像上的合并结果。与之前的版本相比&#xff0c;男性和老年人看起来更像真正的皮克斯角色&a…

黄金交易策略:手工同向单减保留仓

虽然保留仓的仓位不大&#xff0c;扛个一年半载不是问题&#xff0c;但闲着也可以手工处理掉&#xff08;10000点以内的不要处理&#xff09;。挑一个最大的单&#xff0c;同向相同的手数&#xff0c;并把两单的止盈设置中位数&#xff08;也没有这么严格&#xff0c;差不多就好…

Node.js之npm单独与批量升级依赖包的方式

Node.js之npm单独与批量升级依赖包的方式 文章目录 Node.js之npm单独与批量升级依赖包的方式npm查看与升级依赖包1. 单独安装或升级最新版本2. 查看依赖但不升级1. npm outdated2. npm update 3. 批量升级新版本4. npm-check-updates1. 全局安装2. ncu查看可升级的版本3. 升级依…

【Linux驱动】块设备驱动(三)—— 块设备读写(不使用请求队列)

并非每种块设备都会用到请求队列&#xff0c;从上节可以知道&#xff0c;请求队列的作用是管理和调用IO请求&#xff0c;那么反过来想&#xff0c;如果IO请求较少&#xff0c;那就可以无需使用请求队列。在以下情况中&#xff0c;可以不使用请求队列。 单任务环境: 当系统中只有…

懒人精灵 之 Lua 捕获 json解析异常 ,造成的脚本停止.

Time: 2024年2月8日20:21:17 by:MemoryErHero 1 异常代码 Expected value but found T_END at character 12 异常代码 Expected value but found T_OBJ_END at character 223 处理方案 - 正确 json 示范 while true do--Expected value but found T_END at character 1--Ex…

tab 切换类交互功能实现

tab切换类交互&#xff1a; 记录激活项&#xff08;整个对象/id/index)动态类型控制 下面以一个地址 tab 切换业务功能为例&#xff1a; <div class"text item" :class"{active : activeAddress.id item.id}" click"switchAddress(item)"…

python-pandas查漏补缺

1. create labels for Series 2. 3. 4. 用平均数等去填empty的格子 5. 6. 7.

Pandas 数据处理-排序与排名的深度探索【第69篇—python:文本数据处理】

文章目录 Pandas 数据处理-排序与排名的深度探索1. sort_index方法2. sort_values方法3. rank方法4. 多列排序5. 排名方法的参数详解6. 处理重复值7. 对索引进行排名8. 多级索引排序与排名9. 更高级的排序自定义10. 性能优化技巧10.1 使用nsmallest和nlargest10.2 使用sort_val…

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之Stepper组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之Stepper组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、Stepper组件 鸿蒙&#xff08;HarmonyOS&#xff09;仅能包含子组件StepperIte…

【Java EE】----SpringBoot的日志文件

1.SpringBoot使用日志 先得到日志对象通过日志对象提供的方法进行打印 2.打印日志的信息 3.日志级别 作用&#xff1a; 可以筛选出重要的信息不同环境实现不同日志级别的需求 ⽇志的级别分为&#xff1a;&#xff08;1-6级别从低到高&#xff09; trace&#xff1a;微量&#…

L1-088 静静的推荐

一、题目 二、解题思路 如果有的学生天梯赛成绩虽然与前一个人相同&#xff0c;但其参加过 PAT 考试&#xff0c;且成绩达到了该企业的面试分数线&#xff0c;则也可以接受——同一批次这样的人可以有多个&#xff01;&#xff01;&#xff01;如果 pta 分数不低于 175 &#…

【MySQL】:深入理解并掌握DML和DCL

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; MySQL从入门到进阶 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一. DML1.1 添加数据1.2 修改数据1.3 删除数据 二. DCL2.1 管理用户2.2 权限控制…

【已解决】:pip is configured with locations that require TLS/SSL

在使用pip进行软件包安装的时候出现问题&#xff1a; WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available. 解决&#xff1a; mkdir -p ~/.pip vim ~/.pip/pip.conf然后输入内容&#xff1a; [global] ind…

PSM-Net根据Stereo图像生成depth图像

一、新建文件夹 在KITTI数据集下新建depth_0目录 二、激活anaconda环境 conda activate pt14py37三、修改submission.py文件 3.1 KITTI数据集路径 parser.add_argument(--datapath, default/home/njust/KITTI_DataSet/00/, helpselect model)3.2 深度图像输出路径 save…

linux(redhat)重置root密码

首先将root密码改成几乎不可能记住的密码 [rootexample ~]# echo fheowafuflaeijifehowf|passwd --stdin root Changing password for user root. passwd: all authentication tokens updated successfully.重启系统&#xff0c;进入救援模式 出现此页面&#xff0c;按e键 lin…