搜索插入位置 图解题解
找插入位置和找目标下标本质一样——二分找「第一个不小于 target 的位置」,存不存在都能给出答案。
在已经按号码排好的候场队列里插一张新票,找它该站的位置——不用从头一张张比,直接翻到中间:中间号码大于等于新票就往左缩,小于就往右缩,且 r 初始设成「队尾再往后一格」,因为新票可能要排到最末尾。区间收拢到空时,l 指的就是第一个「不小于目标」的位置,即插入点。
这道题到底在问什么
- 输入
- nums=[1,3,5,6], target=5
- 输出
- 2 (5 在下标 2)
- 输入
- nums=[1,3,5,6], target=2
- 输出
- 1 (插到 1 和 3 之间)
最优解:为什么这么做
一句话答案:LeetCode 35 搜索插入位置:升序数组里找 target,在就返回下标,不在就返回它该插入的位置,本质是求第一个 ≥ target 的下标。二分每轮砍一半,mid 合格收 r=mid 留住候选,时间 O(log n)。
搜索插入位置到底要返回哪个下标
给一个升序、无重复的数组 nums 和目标值 target。target 在数组里就返回它的下标;不在就返回它该插入的位置。题面例子:nums=[1,3,5,6]、target=5 返回 2、target=2 返回 1。两种情形是同一个问题:都在问「第一个大于等于 target 的数排第几」,这个下标叫 lower_bound(第一个不小于目标的位置)。
一个个往后比,为什么对不起这个有序数组
挨个往后比,最坏要看遍 n 个数,是 O(n) 的线性扫。可数组明明升序,「≥ target」一旦成立往右就一直成立、左边一律不成立,线性扫把这条性质白扔了。一刀两断的结构,用二分查找在 O(log n) 里解决。
凭什么这个数组能二分
二分要成立,得有一条把区间劈两半的单调性质,这里就是「nums[i] ≥ target 吗」。数组升序,这个判断从某个位置起由「否」翻成「是」、再不翻回:左边全小于 target,右边全大于等于 target。插入位置就是这条分界线上「是」的第一个格子,每看一个中点砍掉一半。
mid 合格时,右界为什么收 mid 不收 mid−1
把范围定成左闭右开区间 [l, r)(含 l、不含 r),起手 l=0、r=n。r 取长度 n 而非 n−1,因为插到末尾时下标正好是 n。取中点 mid=(l+r)//2 比 target:nums[mid] ≥ target 时够大、可能是答案,右界收到 mid(写 r=mid 不是 mid−1,留住它);nums[mid] < target 时它和左边全太小,左界写 l=mid+1 跳过。循环到 l==r,l 停处就是第一个 ≥ target 的下标。答案自始至终留在 [l, r) 里。
题面两个例子,逐轮收区间
先看 target=5、nums=[1,3,5,6],l=0、r=4。第一轮 mid=2,nums[2]=5≥5,收 r=2,区间 [0,2)。第二轮 mid=1,nums[1]=3<5,收 l=2,区间 [2,2)。l==r=2 退出,返回 2。
再看不存在的 target=2。l=0、r=4。第一轮 mid=2,5≥2,r=2。第二轮 mid=1,3≥2,r=1。第三轮 mid=0,nums[0]=1<2,l=1,区间 [1,1)。l==r=1 退出,返回 1。
右界起手用 n−1,末尾那个位置就没了
右界起手写成 n−1 最常见:右开区间本就够不到下标 n,target 大过所有数、该插到末尾时就返回不出 n,末尾那个位置凭空没了。收缩写反也坑:nums[mid] ≥ target 时手滑写 r=mid−1,会把可能是答案的 mid 丢掉。循环条件要配对:左闭右开用 while l<r,错成 l<=r 又不动 r,l==r 那轮空转成死循环。mid 用 (l+r)//2 数值极大时会溢出,稳妥写 l+(r−l)//2。收尾复杂度:每轮把区间对半砍,最多 log₂n 次收空,时间 O(log n);只用 l、r、mid 三个下标,空间 O(1)。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3记住「第一个 ≥ target 的位置 = 插入下标」,下面每一帧都在套它。
- 4开始在有序数组里找「第一个 ≥ 7 的位置」。左界 l=0、右界 r=8(注意右界取数组长度 8,因为答案可能落到「末尾之后」)。
- 5看中点 nums[4]=8,它 ≥ 7,说明这个位置「合格」(够大),它有可能就是答案;但更靠左也许还有合格的,得继续往左找。
- 6因为 mid 自己也可能是答案,右界收到 mid(不是 mid-1):r → 4。候选区间缩成 [0, 4),灰格是已排除的。
- 7看中点 nums[2]=5,它比 7 小,那它和它左边的数全都 < 7、统统不可能是答案,整片排除。
- 8mid 已确定太小,左界直接跳过它:l → 3。候选区间缩成 [3, 4)。
- 9看中点 nums[3]=6,它比 7 小,那它和它左边的数全都 < 7、统统不可能是答案,整片排除。
- 10mid 已确定太小,左界直接跳过它:l → 4。候选区间缩成 [4, 4)。
- 11区间收成一个点,l=4。target=7 不在数组里,但第一个 ≥ 它的数(nums[4]=8)顶上来了,target 就该插在这个下标,答案 4。
- 12开始在有序数组里找「第一个 ≥ 10 的位置」。左界 l=0、右界 r=8(注意右界取数组长度 8,因为答案可能落到「末尾之后」)。
- 13看中点 nums[4]=8,它比 10 小,那它和它左边的数全都 < 10、统统不可能是答案,整片排除。
- 14mid 已确定太小,左界直接跳过它:l → 5。候选区间缩成 [5, 8)。
- 15看中点 nums[6]=12,它 ≥ 10,说明这个位置「合格」(够大),它有可能就是答案;但更靠左也许还有合格的,得继续往左找。
- 16因为 mid 自己也可能是答案,右界收到 mid(不是 mid-1):r → 6。候选区间缩成 [5, 6),灰格是已排除的。
- 17看中点 nums[5]=10,它 ≥ 10,说明这个位置「合格」(够大),它有可能就是答案;但更靠左也许还有合格的,得继续往左找。
- 18因为 mid 自己也可能是答案,右界收到 mid(不是 mid-1):r → 5。候选区间缩成 [5, 5),灰格是已排除的。
- 19区间收成一个点,l=5,而 nums[5] 正好等于 10——target 存在,「第一个 ≥ target 的位置」就是它自己,答案 5。
- 20开始在有序数组里找「第一个 ≥ 20 的位置」。左界 l=0、右界 r=8(注意右界取数组长度 8,因为答案可能落到「末尾之后」)。
- 21看中点 nums[4]=8,它比 20 小,那它和它左边的数全都 < 20、统统不可能是答案,整片排除。
- 22mid 已确定太小,左界直接跳过它:l → 5。候选区间缩成 [5, 8)。
- 23看中点 nums[6]=12,它比 20 小,那它和它左边的数全都 < 20、统统不可能是答案,整片排除。
- 24mid 已确定太小,左界直接跳过它:l → 7。候选区间缩成 [7, 8)。
- 25看中点 nums[7]=15,它比 20 小,那它和它左边的数全都 < 20、统统不可能是答案,整片排除。
- 26mid 已确定太小,左界直接跳过它:l → 8。候选区间缩成 [8, 8)。
- 27所有数都比 20 小,区间一路向右收到末尾,l=8=数组长度,target 插到最后,答案 8。
⚠️ 容易写错的地方
✗ 错:r 初始化成 n-1
✓ 对:应初始化 r = n
插入位置可能落到「末尾之后」(target 比所有数都大),r 必须能取到 n
✗ 错:nums[mid] >= target 时写 r = mid-1
✓ 对:应写 r = mid
mid 自己可能就是答案,砍掉它会漏解;下界二分右界要留 mid
✗ 错:mid 用 (l+r)/2 在大数据下溢出
✓ 对:写 l + (r-l)/2 更稳
l+r 可能超出 int 上限(Java/C++ 尤其注意)
✗ 错:循环条件写成 l <= r
✓ 对:本写法应是 l < r
这里 r 是「开区间右界」,l==r 即收敛,再循环会越界
完整代码(Python / C++ / Java)
Python
def searchInsert(nums, target):
l, r = 0, len(nums) # r 取 n:插入位可到末尾之后
while l < r:
mid = (l + r) // 2
if nums[mid] >= target:
r = mid # mid 合格,留在候选内
else:
l = mid + 1 # mid 太小,排除
return l # 第一个 >= target 的下标C++
int searchInsert(vector<int>& nums, int target){
int l = 0, r = nums.size(); // r 取 n
while(l < r){
int mid = l + (r - l) / 2; // 防溢出
if(nums[mid] >= target) r = mid; // 合格,缩右
else l = mid + 1; // 太小,排除
}
return l;
}Java
public int searchInsert(int[] nums, int target) {
int l = 0, r = nums.length; // r 取 n
while (l < r) {
int mid = l + (r - l) / 2; // 防溢出,等价 (l+r)/2
if (nums[mid] >= target) {
r = mid; // mid 合格,留在候选内
} else {
l = mid + 1; // mid 太小,排除
}
}
return l; // 第一个 >= target 的下标
}复杂度
时间
O(log n)
每轮把候选区间砍一半
空间
O(1)
只用 l、r、mid 三个下标,不开额外数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 搜索插入位置 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和二分查找基础题 LeetCode 704 是什么关系?+
704 在数组里找 target 的下标、找不到返回 −1,是「找准确值」的二分。本题把「找不到」的返回值从 −1 换成「该插入的位置」,等价于求第一个 ≥ target 的下标(lower_bound)。差别就在收缩规则:704 命中 nums[mid]==target 直接返回,本题即使 nums[mid] ≥ target 也不停、继续往左收 r=mid,因为要的是最左那个合格位置。会了 704,本题只需把判定从「等于」放宽成「大于等于」、把返回从 −1 改成 l。
为什么用左闭右开区间 [l, r),用左闭右闭 [l, r] 行不行?+
都行,但配套写法要一致。左闭右开时 r=n、循环 while l
数组里有重复元素时,这个写法返回哪一个下标?+
返回最左边那个等于 target 的下标。因为收缩规则是 nums[mid] ≥ target 就收 r=mid,等于 target 时也照样往左继续收,最终停在第一个 ≥ target 的位置,也就是第一个等于 target 的位置,这正是 lower_bound 的语义。若想要最右那个(第一个 > target 的位置,即 upper_bound),把判定里的 ≥ 改成 >,返回值再减一即可。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 搜索插入位置 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。