35. 搜索插入位置 (Search Insert Position)

3,843 阅读7分钟

"每天都在搜索的路上"

之前总结过二分法的套路(重识二分法(上)重识二分法(中)重识二分法(下)),那就从二分搜索开始刷起吧!

据说组内2年+前端经验小老弟拿了字节37K+(2-1)的offer,比我这四年的老油条还多(酸酸酸...),励志从今天开启刷题模式。

35. 搜索插入位置

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

示例1

输入: nums=[1,3,5,6]nums = [1,3,5,6], target=5target = 5

输出: 22

解释: targettarget 在数组 numsnums 中存在,即返回其下标。

示例2

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

输出: 11

解释: targettarget 在数组 numsnums 中不存在,即返回其应该存在的位置下标

分析

本题属于二分搜索入门,重点考察二分法细节处理,即,当 targettarget 元素不存在与 numsnums 数组中,如果确定返回的 leftleftrightrightleft+1left+1right+1right+1指针。

由于数组元素是严格递增无重复的,自然要想到二分法。因此我们套用二分法的三个区间模型对应的计算模版,即左闭右闭 [left,right][left,right]左闭右开[left,right)[left,right)左开右闭(left,right](left,right]

targettarget 元素存在两种情况:1、 ,2、 targettarget 元素不在 numsnums 数中。

  • 如果 targettarget 元素 numsnums 数组中,一定存在 nums[mid]==targetnums[mid] == target 情况,所以最终结果一定会在二分的过程中通过 midmid 指针返回

  • 如果 targettarget 元素不在 numsnums 数组中,一定在通过 leftleftrightright 指针(亦或需要额外计算)返回。故我们着重讨论一下这种情况。

如果 targettarget 元素不在 numsnums 数组中,那么即存在三种情况,即,

  • targettarget 在数组边界外,即 target<nums[0]target < nums[0]
  • targettarget 在数组边界外,即 target>nums[len1]target > nums[len - 1]
  • targettarget 在数组中某个位置,即 nums[0]<target<nums[len1]nums[0] < target < nums[len - 1]

其中,len=nums.length1len = nums.length - 1

接下来,我们就开始讨论这种情况( targettarget 不在数组内),确定在不同区间模型下的最终返回值。

左闭右闭

区间模型为 [left,right][left, right]midmid 计算方式选择 (left+right)/2(left+right)/2(left+right+1)/2(left+right+1)/2(left+right1)/2(left+right-1)/2,循环条件为 leftrightleft \le right。(参考 重识二分法(上)

1️⃣ targettarget 在数组左外边界

【最终结果: 0】 这种情况下 leftleft 指针会一直指向 00,而 rightright 指针会不断向 leftleft 靠拢,并在最后一次循环前指向 left=right=0left = right = 0。此时计算出 mid=0mid = 0 ❓,nums[mid]nums[mid] 依旧大于 targettarget,故 right=mid1=1right = mid - 1 = -1 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=0,right=1left= 0, right = -1(故应返回 leftleft 或返回 right+1right + 1)。

细心的同学会发现:当 left=right=0left = right = 0 时,mid=(left+right)/2=0mid = (left+right)/2=0 是没有问题的,但是为什么经(left+right+1)/2(left+right+1)/2(left+right1)/2(left+right-1)/2 计算得到的值也是 00 呢?

在实际编程中,会采用差值方式计算 midmid 指针,即 mid=left+((rightleft+1)>>1)=0mid= left + ((right - left + 1) >> 1) = 0 mid=left+((rightleft1)>>1)=0mid = left + ((right - left - 1) >> 1) = 0,而且位运算中有一个向 00 取整的概念,即当实数大于0时,向下取整,实数小于0时,向上取整。JS语法中可通过">> 1(右移一位)",亦或"~~"符号,处理(0.5)以及(-0.5),最后计算出来的值均为0。(详见参考代码)

2️⃣ targettarget 在数组右外边界

【最终结果:nums.lengthnums.length。讨论:这种情况下 rightright 指针会一直指向 nums.length1nums.length-1,而 leftleft 指针会不断向 rightright 靠拢,并在最后一次循环前指向,left=right=nums.length1left = right = nums.length - 1。此时,计算出的 mid=nums.length1,nums[mid]mid = nums.length - 1 ❓, nums[mid] 依旧小于 targettarget,故 left=mid+1=len+1=nums.lengthleft = mid + 1 = len + 1 = nums.length 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=nums.length,right=nums.length1left = nums.length, right = nums.length - 1(应返回 leftleft 或返回 right+1right + 1

细心的同学又会发现,如果 mid=(left+right1)/2mid = (left + right - 1) /2 计算方式时,mid=nums.length2mid= nums.length - 2 (向下取整), nums[mid]nums[mid] 依旧小于 targettarget,故 left=mid+1=nums.length1left = mid + 1 = nums.length - 1,貌似进入了死循环 🤔...这里依然使用差值计算方式,即 mid=left+((rightleft1)>>1)mid = left + ((right - left - 1) >> 1)

3️⃣ targettarget 在数组中间

讨论: 假设存在索引 kk,其中 k[0,n1)k \in [0,n-1)nn 是数组的长度,即 n=nums.lengthn = nums.length,使得 nums[k]<target<nums[k+1]nums[k] < target < nums[k+1]。先说结论:targettarget 应插入到 k+1k + 1 这个位置,故返回 k+1k + 1

最后一次循环前leftleftrightright 指向有两种情况,指向 kkk+1k + 1。我们对这两种情况分开讨论。

  • leftleftrightright 同时指向 kk 时,进入最后一次循环,此时 mid=kmid = knums[mid]nums[mid] 小于 targettarget,因此 left=mid+1=k+1left = mid + 1 = k + 1 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=k+1,right=kleft = k + 1, right = k(应返回 leftleft 或返回 right+1right + 1)。

  • leftleftrightright 同时指向 k+1k+1 时,进入最后一次循环,此时 mid=k+1mid = k+1nums[mid]nums[mid] 大于 targettarget,因此 right=mid1=kright = mid - 1 = k 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=k+1,right=kleft = k + 1, right = k(应返回 leftleft 或返回 right+1right + 1)。

综上所述,在左闭右闭区间模型下,midmid 计算方式为 mid=(left+right)/2mid = (left+right) /2mid=(left+right1)/2mid = (left+right-1) /2mid=(left+right+1)/2mid = (left+right+1) /2,且当元素不在 numsnums 数组中时,当退出循环时直接返回 leftleft,亦或返回 right+1right + 1

4️⃣ 参考代码

/**
 * 时间复杂度 O(log(N))
 * 空间复杂度 O(1)
 */
function searchInsert(nums: number[], target: number): number {
	let left = 0, right = nums.length - 1;

	while(left <= right) {
                // 这个采用 ~~ 向 0 取整
		const mid = left + ~~((right - left) / 2);
		// const mid = left + ~~((right - left - 1) / 2); // 亦可
		// const mid = left + ~~((right - left + 1) / 2); // 亦可

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

		if (nums[mid] > target) {
			right = mid - 1;
		} else {
			left = mid + 1;
		}
		
	}

	return left;
	// return right + 1; // 亦可
};

左闭右开

区间模型为 [left,right)[left, right)midmid 计算方式选择 (left+right)/2(left+right)/2(left+right1)/2(left+right-1)/2,循环条件为 left<rightleft \lt right。参考 重识二分法(中)

1️⃣ targettarget 在数组左外边界

【最终结果: 0】 讨论:这种情况下 leftleft 指针会一直指向 00,而 rightright 指针会不断向 leftleft 靠拢,并在最后一次循环前指向 left=0,right=1left = 0, right = 1。此时计算出 mid=0mid = 0nums[mid]nums[mid] 依旧大于 targettarget,故 right=mid=0right = mid = 0 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=right=0left = right = 0(应返回 leftleft 或返回 rightright)。

2️⃣ targettarget 在数组右外边界

【最终结果:nums.lengthnums.length。讨论:这种情况下 rightright 指针会一直指向 num.lengthnum.length,而 leftleft 指针会不断向 rightright 靠拢,并在最后一次循环前指向,left=nums.length1,right=nums.lengthleft = nums.length - 1, right = nums.length。此时,计算出的 mid=nums.length1,nums[mid]mid = nums.length - 1, nums[mid] 依旧小于 targettarget,故 left=mid+1=nums.lengthleft = mid + 1 = nums.length 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=right=nums.lengthleft = right = nums.length(应返回 leftleft 或返回 rightright

3️⃣ targettarget 在数组中间

讨论: 假设存在索引 kk,其中 k[0,n1)k \in [0,n-1)nn 是数组的长度,即 n=nums.lengthn = nums.length,使得 nums[k]<target<nums[k+1]nums[k] < target < nums[k+1]。先说结论:targettarget 应插入到 k+1k + 1 这个位置,故返回 k+1k + 1

最后一次循环前leftleft 指向 kkrightright 指向 k+1k + 1。进入最后一次循环,此时 mid=kmid = knums[mid]nums[mid] 小于 targettarget,因此 left=mid+1=k+1left = mid + 1 = k + 1 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=right=k+1left = right = k + 1(应返回 leftleft 或返回 rightright)。

综上所述,在左闭右闭区间模型下,midmid 计算方式为 mid=(left+right)/2mid = (left+right) /2mid=(left+right1)/2mid = (left+right-1) /2,且当元素不在 numsnums 数组中时,当退出循环时直接返回 leftleft,亦或返回 rightright

4️⃣ 参考代码

/**
 * 时间复杂度 O(log(N))
 * 空间复杂度 O(1)
 */
function searchInsert(nums: number[], target: number): number {
	let left = 0, right = nums.length;

	while(left < right) {
                // 这个采用 ~~ 向 0 取整
		const mid = left + ~~((right - left) / 2);
		// const mid = left + ~~((right - left - 1) / 2); // 亦可
		// const mid = left + ~~((right - left + 1) / 2); // 不可

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

		if (nums[mid] > target) {
			right = mid;
		} else {
			left = mid + 1;
		}
		
	}

	return left;
	// return right; // 亦可
};

左开右闭(left,right](left, right]

区间模型为 [left,right)[left, right)midmid 计算方式选择 (left+right+1)/2(left+right+1)/2,循环条件选择 left<rightleft \lt right。参考 重识二分法(下)

1️⃣ targettarget 在数组左外边界

【最终结果: 0】 讨论:这种情况下 leftleft 指针会一直指向 1-1,而 rightright 指针会不断向 leftleft 靠拢,并在最后一次循环前指向 left=1,right=0left = -1, right = 0。此时计算出 mid=0mid = 0nums[mid]nums[mid] 依旧大于 targettarget,故 right=mid1=1right = mid - 1 = -1 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=0,right=1left= 0, right = -1(应返回 left+1left + 1 或返回 right+1right + 1)。

2️⃣ targettarget 在数组右外边界

【最终结果:nums.lengthnums.length。讨论:这种情况下 rightright 指针会一直指向 nums.length1nums.length -1,而 leftleft 指针会不断向 rightright 靠拢,并在最后一次循环前指向,left=nums.length2,right=nums.length1left = nums.length - 2, right = nums.length - 1。此时,计算出的 mid=nums.length1,nums[mid]mid = nums.length - 1, nums[mid] 依旧小于 targettarget,故 left=mid=nums.length1left = mid = nums.length - 1 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=right=nums.length1left = right = nums.length - 1(应返回 left+1left + 1 或返回 right+1right + 1

3️⃣ targettarget 在数组中间

讨论: 假设存在索引 kk,其中 k[0,n1)k \in [0,n-1)nn 是数组的长度,即 n=nums.lengthn = nums.length,使得 nums[k]<target<nums[k+1]nums[k] < target < nums[k+1]。先说结论:targettarget 应插入到 k+1k + 1 这个位置,故返回 k+1k + 1

最后一次循环前leftleftrightright 指向有两种情况,指向 kkk+1k + 1。我们对这两种情况分开讨论。

最后一次循环前leftleft 指向 kkrightright 指向 k+1k + 1。进入最后一次循环,此时 mid=k+1mid = k + 1nums[mid]nums[mid] 大于 targettarget,因此 right=mid1=kright = mid - 1 = k 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=right=kleft = right = k(应返回 left+1left + 1 或返回 right+1right + 1)。

综上所述,在左闭右闭区间模型下,midmid 计算方式为 mid=(left+right+1)/2mid = (left+right+1) /2,且当元素不在 numsnums 数组中时,当退出循环时直接返回 left+1left+1,亦或返回 right+1right+1

4️⃣ 参考代码

/**
 * 时间复杂度 O(log(N))
 * 空间复杂度 O(1)
 */
function searchInsert(nums: number[], target: number): number {
	let left = -1, right = nums.length - 1;

	while(left < right) {
                // 这个采用 ~~ 向 0 取整
		const mid = left + ~~((right - left + 1) / 2);
		// const mid = left + ~~((right - left) / 2); // 不可
		// const mid = left + ~~((right - left - 1) / 2); // 不可

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

		if (nums[mid] > target) {
			right = mid - 1;
		} else {
			left = mid;
		}
		
	}

	return left + 1;
	// return right + 1; // 亦可
};

总结

区间模型循环条件midmid计算方式二分指针赋值返回值
[left,right][left,right]leftrightleft \le rightmid=(lef+right)/2mid=(lef+right)/2mid=(lef+right1)/2mid=(lef+right-1)/2mid=(lef+right+1)/2mid=(lef+right+1)/2nums[mid]>targetnums[mid]>targetright=mid1right = mid - 1,否则 left=mid+1left = mid + 1leftleft 亦或 right+1right+1
[left,right)[left,right)left<rightleft \lt rightmid=(lef+right)/2mid=(lef+right)/2mid=(lef+right1)/2mid=(lef+right-1)/2nums[mid]>targetnums[mid]>targetright=midright = mid,否则 left=mid+1left = mid + 1leftleft 亦或 rightright
(left,right](left,right]left<rightleft \lt rightmid=(lef+right+1)/2mid=(lef+right+1)/2nums[mid]>targetnums[mid]>targetright=mid1right = mid - 1,否则 left=midleft = midleft+1left+1 亦或 right+1right+1