搜索插入位置 - 二分法

本文详细解析了如何使用二分查找算法在有序数组中找到目标值的插入位置,时间复杂度为O(logn)。通过分析示例,明确了返回值的逻辑,并给出了优化后的代码实现,强调了正确处理边界条件的重要性。同时,文章提供了更多二分查找题型的练习,帮助读者深入理解和掌握二分查找技巧。
摘要由CSDN通过智能技术生成

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

请必须使用时间复杂度为 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
示例 4:

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

输入: nums = [1], target = 0
输出: 0
 

提示:

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

网友的方法:

暴力方法,不满足时间复杂度要求;其次,当样本数据量过大时,二分法效率更高。

 我的答案:

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        if (nums.empty())
        {
            return 0;
        }

        int start = 0;
        int end = nums.size() - 1;
        int mid = (end + start)/2;
        while(start < end && target != nums[mid])
        {
            if (target < nums[mid])
            {
                end = mid - 1;
            }
            else if (target > nums[mid])
            {
                start = mid + 1;
            }

			mid = (end + start)/2;
        }

        if (target > nums[mid])
        return mid + 1;
        if (target < nums[mid] && mid != 0)
        return mid;
        if (target < nums[mid] && mid == 0)
        return 0;
        if (target == nums[mid])
        return mid;

        return 0;
    }
};

逻辑:最后一次while循环退出(不满足条件),所以外面再判断一次,得出最终的结果。

优化:可以看到,最后的4个if 判断,最后3个条件是一样的,可以合并。

易错:当 target <= nums[mid] 时,插入 mid 位置,而不是mid - 1

题解:

写对二分查找不能靠模板,需要理解加练习 (附练习题)

liweiwei1419L6

发布于 2019-06-27145.5k二分查找C++JavaPythoncpp

这篇题解分成「本题题解」「二分查找重点概括」和「二分查找题型练习」三个部分。

关键:写对「二分查找」的重点,从来不在于二分查找怎么写,而在于分析题意,根据题目的条件和要求思考如何缩减区间。

在学习「二分查找」以及其它算法和数据结构的过程中,我们可能会有各种各样的疑问,把它们记录下来,思考得多了,很多问题自然就有答案。

本题题解

在有序数组中查找,可以使用「二分查找」。

分析

根据示例,分析题目要我们返回的「插入元素的位置」到底是什么。根据示例 3:

 

输入: [1, 3, 5, 6], 7 输出: 4

如果目标元素大于输入数组中的最后一个元素,题目需要我们返回数组的最后一个元素的下标 + 1。又根据示例 2:

 

输入: [1, 3, 5, 6], 2 输出: 1

因此题目需要我们返回第 1 个 大于等于(等于的情况可以看示例 1) 目标元素 2 的下标,因此返回 1。因此 如果当前 mid 看到的数值严格小于 target,那么 mid 以及 mid 左边的所有元素就一定不是题目要求的结果,根据这一点可以写出本题二分查找算法的完整逻辑。

参考代码 1

  • Java
 

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 int left = 0; int right = len - 1; // 在区间 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; } }

 

说明:由于执行到最后 nums[left..right] 里一定存在插入元素的位置,退出循环的时候一定有 left == right 成立,因此返回 left 或者 right 都可以。

复杂度分析

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

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

参考代码 2

  • Java
 

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)

二分查找重点概括

  • 写成 while(left < right) ,退出循环的时候有 left == right 成立,好处是不用判断应该返回 left 还是 right
  • 区间 [left..right] 划分只有以下两种情况:
    • 分成 [left..mid] 和 [mid + 1..right],分别对应 right = mid 和 left = mid + 1
    • 分成 [left..mid - 1] 和 [mid..right],分别对应 right = mid - 1 和 left = mid,这种情况下。需要将 int mid = (left + right) / 2 改成 int mid = (left + right + 1) / 2,否则会出现死循环,这一点不用记,出现死循环的时候,把 left 和 right 的值打印出来看一下就很清楚了
  • 退出循环 left == right,如果可以确定区间 [left..right] 一定有解,直接返回 left 就可以。否则还需要对 left 这个位置单独做一次判断;
  • 二分查找的循环不变量是:在区间 [left..right] 里查找目标元素。

关于如何写对二分查找,二分查找的详细讲解,可以查看我编写的 LeetBook 的「二分查找」 章节。

二分查找题型练习

提示:这些问题都不应该当做模板问题来看待。下面是这些问题的题解,重点分析了应该如何思考,讲解了如何编写正确的代码,希望能够对大家有所帮助。

题型一:二分下标(在数组中查找符合条件的元素的下标)

一般而言这个数组是有序的,也可能是半有序的(旋转有序数组或者山脉数组)。

题目题解说明
704. 二分查找(简单)二分查找的最原始问题,使用本题解介绍的方法就要注意,需要后处理。
34. 在排序数组中查找元素的第一个和最后一个位置(中等)文字题解视频题解查找边界问题。
33. 搜索旋转排序数组(中等)文字题解利用局部单调性,逐步缩小搜索区间(其它问题类似)。
81. 搜索旋转排序数组 II(中等)文字题解
153. 寻找旋转排序数组中的最小值(中等)文字题解
154. 寻找旋转排序数组中的最小值 II(中等)文字题解
300. 最长上升子序列(中等)文字题解特别经典的一道「动态规划」,二分查找的思路基于「动态规划」的状态定义得到,代码很像第 35 题。
275. H指数 II(中等)文字题解这个问题题目的描述让人迷惑,可以跳过不做。
852. 山脉数组的峰顶索引(简单)利用局部单调性,逐步缩小搜索区间。
1095. 山脉数组中查找目标值(中等)文字题解视频题解
4. 寻找两个有序数组的中位数(困难)文字题解视频题解
658. 找到 K 个最接近的元素(中等)文字题解这个问题二分的写法需要做复杂的分类讨论,可以放在以后做。

题型二:二分答案(在一个有范围的区间里搜索一个整数)

定位一个有范围的整数,这件事情也叫「二分答案」或者叫「二分结果」。如果题目要求的是一个整数,这个整数有明确的范围,可以考虑使用二分查找。

事实上,二分答案是我们最早接触的二分查找的场景。「幸运 52」里猜价格游戏,就是「二分查找」算法的典型应用:先随便猜一个数,如果猜中,游戏结束。如果猜大了,往小猜;如果猜小了,往大猜。

题目题解说明
69. 平方根(简单)文字题解在一个整数范围里查找一个整数,也是二分查找法的应用场景。
287. 寻找重复数(中等)文字题解在一个整数范围里查找一个整数。这个问题二分查找的解法很反常规(不应该用时间换空间,这么做太傻了),知道即可。
374. 猜数字大小(简单)文字题解
1300. 转变数组后最接近目标值的数组和文字题解

题型三:二分答案的升级版:判别条件需要遍历数组

说明:这里给出的问题解法都一样,会一题等于会其它题。问题的场景会告诉我们:目标变量和另一个变量有相关关系(一般是线性关系),目标变量的性质不好推测,但是另一个变量的性质相对容易推测(满足某种意义上的单调性)。这样的问题的判别函数通常会写成一个函数的形式。

这一类问题可以统称为「 最大值极小化 」问题,最原始的问题场景是木棍切割问题,这道题的原始问题是「力扣」第 410 题(分割数组的最大值(困难))。

思路是这样的:

  • 分析出题目要我们找一个整数,这个整数有范围,所以可以用二分查找;
  • 分析出 单调性,一般来说是一个变量 a 的值大了,另一个变量 b 的值就变小,而「另一个变量的值」 b 有限制,因此可以通过调整 a 的值达到控制 b 的效果;
  • 这一类问题的题目条件一定会给出 连续正整数 这样的关键字。如果没有,问题场景也一定蕴含了这两个关键信息。

以下给出的问题无一例外。

题目提示与题解说明
410. 分割数组的最大值(困难)文字题解
875. 爱吃香蕉的珂珂(中等)文字题解
LCP 12. 小张刷题计划(中等)题解在第 410 题题解里
1482. 制作 m 束花所需的最少天数(中等)题解在第 1300 题题解里
1552. 两球之间的磁力(中等)

下一篇:二分法 代码简洁

© 著作权归作者所有

510

条评论最热

编辑

预览

评论

精选评论(5)

bao-zi-31

孢子

2019-07-07

看一条优秀的分享,胜刷十条力扣

322踩查看 4 条回复回复

yemaoteng

夜猫腾L2

2020-11-21

三个月前,我点开了这篇题解。 一个月前,我点开了这篇题解。 今天,我又又点开了这篇题解。 如果你觉得二分法老是没掌握,放心,你不是一个人......

97踩查看 1 条回复回复

liweiwei1419

liweiwei1419L6

(编辑过)2019-07-01

在这里补充一个小细节,我只在写 Java 代码的时候这样用。

int mid = left + (right - left) / 2;            

还可以写成:

int mid = (left + right) >>> 1;

这是参考 Java 的 JDK 中 Arrays.binarySearch() 函数的写法。理由是 left + right 即使是在整型溢出以后,由于无符号右移 >>> 1 ,仍然能够得到正确的结果(我掌握的语言中,只有 Java 语言中有 >>> 这个操作符)。

虽然 >> 1 和 /2 ,但是有些语言编译器都会将 /2 转换成位运算的操作,这是编译器内部的优化。因此我们没有必要手动去做这一步优化,写代码的时候还是写 / 2

71踩查看 11 条回复回复

lzhlyle

lzhlyle

L52020-03-25

不记得几刷此题解了,真棒!

关于取中位数,基于此题解,我记忆的口诀是「左动取左,右动取右」,即 if (...) left = mid + 1; 归为「左动」,对应左中位数;if (...) right = mid - 1; 归为「右动」,对应右中位数。

补充一些二分查找变体(来自某客APP专栏)在此题解思路下的代码(java),辅以练习

// 查找第一个值等于给定值的元素
private int firstEquals(int[] arr, int target) {
    int l = 0, r = arr.length - 1;
    while (l < r) {
        int mid = l + ((r - l) >> 1);
        if (arr[mid] < target) l = mid + 1;
        else r = mid; // 收缩右边界不影响 first equals
    }
    if (arr[l] == target && (l == 0 || arr[l - 1] < target)) return l;
    return -1;
}
// 查找最后一个值等于给定值的元素
private int lastEquals(int[] arr, int target) {
    int l = 0, r = arr.length - 1;
    while (l < r) {
        int mid = l + ((r - l + 1) >> 1);
        if (arr[mid] > target) r = mid - 1;
        else l = mid; // 收缩左边界不影响 last equals
    }
    if (arr[l] == target && (l == arr.length - 1 || arr[l + 1] > target)) return l;
    return -1;
}
// 查找第一个大于等于给定值的元素
private int firstLargeOrEquals(int[] arr, int target) {
    int l = 0, r = arr.length - 1;
    while (l < r) {
        int mid = l + ((r - l) >> 1);
        if (arr[mid] < target) l = mid + 1;
        else r = mid; // 收缩右边界不影响 first equals
    }
    if (arr[l] >= target && (l == 0 || arr[l - 1] < target)) return l; // >=
    return -1;
}
// 查找最后一个小于等于给定值的元素
private int lastLessOrEquals(int[] arr, int target) {
    int l = 0, r = arr.length - 1;
    while (l < r) {
        int mid = l + ((r - l + 1) >> 1);
        if (arr[mid] > target) r = mid - 1;
        else l = mid; // 收缩左边界不影响 last equals
    }
    if (arr[l] <= target && (l == arr.length - 1 || arr[l + 1] > target)) return l; // <=
    return -1;
}

51踩查看 14 条回复回复

bei-zhi-hu

北枝L4

(编辑过)2021-02-24

终于刷完了上述列出的所有问题,从一开始完全不知道怎么找思路找对象,边界条件模糊不清,经常莫名死循环,到现在的简单中等题基本可以乱杀,来还愿,感谢大佬的分享!!! 另外我分享一点的心得,二分查找题目最重要的是找到某种变量的单调性,不论是数组元素的相对位置,还是某种函数的相对大小,亦或是某种条件下任务的完成情况,数组有序就看数组,数组无序就找以数组为依托的其他变量,只要找准了正确的二分对象,你就成功了一半。这个寻找过程是很重要的,能够极大培养你的思维方式。 对于边界条件,我不建议背模板,学算法背什么模板,边界条件就是不断刷题积累经验,以及答案错误就重复地调试,一遍就绿给人惊喜,但红色才是coding的常态。感觉二分查找已经入门了,摸到一点动态规划的门道,转移阵地咯,各位加油啃下去,一定有收获>=付出!

28踩查看 1 条回复回复

评论(510)

Tailsory

今天你刷题了吗L3

2021-11-01

呜呜呜,看了好多遍的题解,特别清晰。今天一打开没了,我还以为是点错了找了半天。已经转阵leetbook了,再次感谢作者

1踩查看 1 条回复回复

CaoGongHui

曹功辉

2021-07-10

学习到了,但是又没完全学会

3踩查看 1 条回复回复

liuyatao

liuyataoL1

2021-09-07

是内容更新了吗?我记得之前看还有挺多图的介绍的

赞踩查看 4 条回复回复

helloleetcode-9

helloleetcodeL2

2021-07-19

mid被分到左边区间/右边区间是什么意思?。。。左右是通过什么来区分的?有谁可以解答一下吗。

赞踩查看 7 条回复回复

Tailsory

今天你刷题了吗L3

2021-05-11

这个题解是我看过最多次的题解,真受益匪浅

3踩回复

hanchan

little little star

2021-08-06

醍醐灌顶,谢谢大佬的无私分享。从讨论里也学到了很多,我又快乐了

1踩查看 1 条回复回复

geekwhm

GeekWhm

2021-05-26

佩服,总结的非常好,收获满满

2踩查看 1 条回复回复

shua-ti-loser

what_why_howL1

2021-08-06

佩服大佬

1踩回复

si-liao-you-he

死了又和

2021-07-12

太感谢了,完全解答了纠结了几天的二分边界问题

1踩查看 1 条回复回复

zeng-kan

曾侃L1

2021-04-29

看到边界设置的代码是 left = mid 时,需要把 mid 的取法调整为上取整,以避免死循环 这个小提示对我帮助非常大,感谢

1踩查看 5 条回复回复

1234514

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值