重识二分法(下)

3,909 阅读7分钟

"一听就会,一写就废"的二分法

已经分析了

本文开始分析其余两种区间模型:左 (left,right](left,right] 、左 (left,right)(left,right)

左开右闭的区间模型

区间模型,即 (left,right](left, right],我们依然按照“六步法”进行逐一分析。

1、初始区间定义

区间初始值定义:left=1,right=len1left = -1, right = len - 1,其中 lenlen 是数组的长度。

2、循环条件选择

判断 left==rightleft==right 有意义么?举个例子,当 left=right=2left = right =2 时,落盘区间为 (2,2](2,2],很明显该区间时没有意义的。故,循环条件为:left<rightleft \lt right

3、mid指针讨论

leftleftrightright 之间的元素个数是偶数时:

举例说明:设 numsnums 数组的长度为 2n+12n + 1(nn 正整数)。索引从 00n1n-1nn 个元素,索引从 n+1n+12n2nnn 个元素,所以索引为 nn 的元素恰为中间元素。初始情况下 left=1,right=2nleft = -1, right = 2nleftleftrightright 之间的元素个数为 2n2n(一定为偶数)。

👉使用 mid=(left+right)/2mid = (left + right)/ 2 会使 midmid 指向中间偏左的元素。

此时 midmid 取值为:mid=(1+2n)/2=n1mid = (-1 + 2n) / 2 = n - 1(向下取整)

👉使用 mid=(left+right+1)/2mid = (left + right + 1) / 2 会使 midmid 指向恰为中心的元素。

此时 mid=(1+2n+1)/2=nmid = (-1 + 2n + 1) / 2 = n

👉使用 mid=(left+right1)/2mid = (left + right - 1) / 2 会使 midmid 指向中间偏左的元素。

此时 mid=(1+2n1)/2=n1mid = (-1 + 2n - 1) / 2 = n - 1

leftleftrightright 之间的元素个数是奇数时:

举例说明:设 numsnums 数组的长度为 2n2n (nn 正整数)。索引从 00n2n-2n1n-1个元素,索引从 n+1n+12n12n-1n1n-1 个元素,所以索引为 n1n-1(中间偏左)nn(中间偏右) 的元素为中间元素。初始情况下 left=1,right=2n1left = -1, right = 2n - 1leftleftrightright 之间的元素个数为 2n12n-1(一定为奇数)。

👉使用 mid=(left+right)/2mid = (left + right) / 2 ,会使 midmid 指向中间偏左的元素。

此时 mid=(1+2n1)/2=n1mid = (-1 + 2n -1) / 2 = n - 1

👉使用 mid=(left+right+1)/2mid = (left + right + 1) / 2 ,会使 midmid 指向中间偏左的元素。

此时 mid=(1+2n1+1)/2=n1mid = (-1 + 2n - 1 + 1) / 2 = n - 1(向下取整)

👉使用 mid=(left+right1)/2mid = (left + right - 1) / 2 ,会使 midmid 指向过于偏左的元素(💥可能会丢失元素)。

此时 mid=(1+2n11)/2=n2mid = (-1 + 2n -1 - 1) / 2 = n - 2(向下取整)

截止目前的讨论,对于 (left,right](left, right] 区间模型,可先淘汰 mid=(left+right1)/2mid = (left + right - 1) / 2(后面也会通过计算落盘新区间,证明该计算方法的不正确性),其他两种计算方法 mid=(left+right)/2mid = (left + right) / 2mid=(left+right+1)/2mid = (left + right + 1) / 2 都可以满足条件,故还需要看计算出的落盘新区间,方可确定 midmid 的计算模型。

4、计算落盘新区间

通过比较 nums[mid]nums[mid]targettarget,从而计算 targettarget 新的落盘区间,即

  • nums[mid]==targetnums[mid] == target,直接返回 midmid 即可;

  • nums[mid]>targetnums[mid] > target,说明 targettarget 一定在 (left,mid)(left, mid) 区间内。我们发现,此区间模型已经违背了我们的“初心”,因此新的落盘区间应为 (left,mid1](left, mid-1]。故,此时应将右闭区间赋值给 rightright 指针,即 right=mid1right = mid -1

  • nums[mid]<targetnums[mid] < target,说明 targettarget 一定在 (mid,right](mid, right] 区间内。我们发现,此区间模型与我们的“初心”一致。故,此时应将左开区间赋值给 leftleft 指针,即 left=midleft = mid

💥注意啦!我们发现:

在每次计算落盘新区间时,如果 nums[mid]>targetnums[mid] > targetrightright 指针都会发生改变(每次循环后的 rightright 会比前一次循环的少1);如果 nums[mid]<targetnums[mid] < targetleftleft 指针也不一定会发生变化(有可能当次计算出来的 midmid 与上一次的 leftleft 值相同)

💥 故我们猜想,在某种 midmid 指针的计算方法下,可能会出现“死循环”。

如采用 mid=(left+right)/2mid = (left + right) / 2 亦或 mid=(left+right1)/2mid = (left + right - 1) / 2,那么当 leftleft 指向第 kk 个元素,而 rightright 指向第 k+1k+1 个元素,其中 k[0,len1)k \in [0, len-1),并且 nums[mid]<targetnums[mid] < target 时, leftleftmidmid 会反复指向最后第一个元素,进入死循环。

证明一下:当 leftleft 指向第 kk 个元素,而 rightright 指向第 k+1k+1 个元素,

  • 代入 mid=(left+right)/2mid = (left + right) / 2,可得:mid=k+1/2=leftmid = k + 1/2 = left

  • 代入 mid=(left+right1)/2mid = (left + right - 1) / 2,可得:mid=k=leftmid = k = left

而此时 nums[mid]<targetnums[mid] < targetmidmid 指针要赋值给 leftleft指针,导致 midmidleftleft指针反复赋值,故一旦命中上述两种情况,二分法将陷入“死循环”中。

故只有 mid=(left+right+1)/2mid = (left + right + 1) / 2 可以满足要求。故 midmid 计算方式为: mid=left+(rightleft+1)>>1mid= left + (right - left + 1) \gt\gt 1

证明一下:当 leftleft 指向第 kk 个元素,而 rightright 指向第 k+1k + 1 个元素,

代入 mid=(left+right+1)/2mid = (left + right + 1) / 2,可得:mid=k+1=rightmid = k + 1 = right

  • 此时如果 nums[mid]==targetnums[mid] == target,直接返回 midmid

  • 此时如果 nums[mid]<targetnums[mid] < targetmidmid 指针要赋值给 leftleft 指针,leftleftrightright 均指向第 k+1k + 1 个元素,循环退出(因为循环执行条件是 left<rightleft < right),返回 1-1

  • 此时如果 nums[mid]>targetnums[mid] > targetmid1mid - 1 赋值给 rightright 指针,此时leftleftrightright 均指向第 kk 个元素,循环退出,返回 1-1;

5、最终返回值

如果 targettarget 的确存在于 numsnums 数组中,一定会在第四步中 nums[mid]==targetnums[mid] == target 时返回,否则返回 1-1 即可。

6、代码参考

迭代法

/**
 * 区间模型 (left, right]
 * 空间复杂度 o(1)
 * 时间复杂度 o(logN)
 */
function search(nums: number[], target: number): number {
	let left = -1;
	let right = nums.length - 1;
	
	while(left < right) {
		const mid = left + ~~((right - left + 1) / 2); // 只有一种 mid 模型
		const midVal = nums[mid];
		
		if (midVal === target) {
			return mid;
		}
		
		if (midVal > target) {
			right = mid - 1;
		} else  {
			left = mid;
		}
	}
	
	return -1;
}

递归法

/**
 * 区间模型 (left, right]
 * 空间复杂度 o(logN)
 * 时间复杂度 o(logN)
 */
 function search(nums: number[], target: number): number {
     return searchHelper(nums, target, -1, nums.length - 1); 
 }
 
 function searchHelper(nums: number[], target: number, left: number, right: number ): number {
     if (left >= right) {
         return -1;
     }
     // [0, +infinity), 强行向下取整, 例如 ~~(1.5) == 1
     // (-infinity, 0), 强行向上取整, 例如 ~~(-0.5) == 0, ~~(-1.5) == -1
     
     const mid = left + ~~((right - left + 1) / 2); // 仅一种方式
     if (nums[mid] === target) {
         return mid;
     }
     
     if (nums[mid] > target) {
         return searchHelper(nums, target, left, mid - 1);
     }
     
     return searchHelper(nums, target, mid, right);
 }

左开右开的区间模型(无解)

区间模型,即 (left,right)(left,right),我们依然按照“六步法”进行逐一分析。

1、初始区间定义

区间初始值定义:left=1,right=lenleft = -1, right = len,其中 lenlen 是数组的长度。

2、循环条件选择

判断 left==rightleft == right 有意义么?举个例子,当 left=right=2left = right =2 时,落盘区间为 (2,2)(2,2),很明显该区间是没有意义的。故,left<rightleft \lt right

3、mid指针讨论

leftleftrightright 之间的元素个数是偶数时:

举例说明:numsnums 数组的长度为 2n2n (nn 是正整数)。索引从 00n2n-2n1n-1 个元素,索引从 n+1n+12n12n-1n1n-1 个元素,所以索引为 n1n-1(中间偏左)nn(中间偏右) 的元素即为中间元素。初始情况下 left=1,right=2nleft = -1, right = 2nleftleftrightright 之间的元素个数为 2n2n(一定为偶数)。

👉使用 mid=(left+right)/2mid = (left + right) / 2 会使 midmid 指向中间偏左的元素。

此时 mid=(1+2n)/2=n1mid = (-1 + 2n) / 2 = n - 1(向下取整)

👉使用 mid=(left+right+1)/2mid = (left + right + 1) / 2 会使 midmid 指向中间偏右的元素。

此时 mid=(1+2n+1)/2=nmid = (-1 + 2n + 1) / 2 = n

👉使用 mid=(left+right1)/2mid = (left + right - 1) / 2 会使 midmid 指向中间偏左的元素。

此时 mid=(1+2n1)/2=n1mid = (-1 + 2n - 1) / 2 = n -1

leftleftrightright 之间的元素个数是奇数时:

举例说明: numsnums 数组的长度为 2n+12n + 1 (nn 正整数)。索引从 00n1n-1nn 个元素,索引从 n+1n+12n2nnn 个元素,所以索引为 nn 的元素恰为中间元素。初始情况下 left=1,right=2n+1left = -1, right = 2n + 1leftleftrgihtrgiht 之间的元素个数为 2n+12n + 1(一定为奇数)。

👉使用 mid=(left+right)/2mid = (left + right) / 2,会使 midmid 指向恰为中心的元素。

此时 mid=(1+2n+1)/2=nmid = (-1 + 2n + 1) / 2 = n

👉使用 mid=(left+right+1)/2mid = (left + right + 1) / 2,会使 midmid 指向恰为中心的元素。

此时 mid=(1+2n+1+1)/2=nmid = (-1 + 2n + 1 + 1) / 2 = n

👉使用 mid=(left+right1)/2mid = (left + right - 1) / 2 ,会使 midmid 指向中间偏左的元素。

此时 mid=(1+2n+11)/2=n1mid = (-1 + 2n + 1 - 1) / 2 = n - 1

截止目前的讨论,对于 (left,right)(left, right) 区间模型, midmid 三种计算模型都可以满足条件,故还需要讨论计算出的落盘新区间,方可确定 midmid 的计算模型。

4、计算落盘新模型

通过比较 nums[mid]nums[mid]targettarget ,从而计算 targettarget 新的落盘区间,即

  • nums[mid]==targetnums[mid] == target,直接返回 midmid 即可;

  • nums[mid]>targetnums[mid] > target,说明 targettarget 一定在 (left,mid)(left, mid) 区间内。我们发现,此区间模型符合我们的“初心”,即 (left,right)(left, right)。故,此时应将新的右开区间赋值给 rightright 指针,即 right=midright = mid

  • nums[mid]<targetnums[mid] < target,说明 targettarget 一定在 (mid,right)(mid, right) 区间内。我们发现,此区间模型与我们的“初心”一致。故,此时应将左开区间赋值给 leftleft 指针,即 left=midleft = mid

💥注意啦!我们发现:

在每次计算落盘新区间时,即使 nums[mid]>targetnums[mid] > targetrightright 指针也不一定会发生变化(有可能当次计算出来的 midmid 与上一次的 rightright 值相同);即使 nums[mid]<targetnums[mid] < targetleftleft 指针也不一定会发生变化(有可能当次计算出来的 midmid 与上一次的 leftleft 值相同)。

💥 故我们猜想,在某种 midmid 指针的计算方法下,可能会出现“死循环”。

先说结论:当 leftleft 指向第 kk 个元素,而 rightright 指向第 k+1k+1 个元素,其中 k[0,len1)k \in [0, len-1),并且 nums[mid]<targetnums[mid] < target 时,在 mid=(left+right)/2mid = (left + right) / 2leftleftmid=(left+right1)/2mid = (left + right - 1) / 2 计算方式下,midmid 会反复指向第一个元素,进入死循环。

证明一下:当 leftleft 指向第 kk 个元素,而 rightright 指向第 k+1k+1 个元素,

  • 代入 mid=(left+right)/2mid = (left + right) / 2 可得:mid=(left+left+1)/2=left+1/2=leftmid = (left + left + 1) / 2 = left + 1/2 = left

  • 代入 mid=(left+right1)/2mid = (left + right - 1) / 2 可得:mid=(left+left+11)/2=leftmid = (left + left + 1 - 1) / 2 = left

而此时 nums[mid]<targetnums[mid] < targetmidmid 指针要赋值给 leftleft 指针,导致 midmidleftleft 指针反复赋值,故一旦命中上述两种情况,二分法将陷入“死循环”中。

先说结论:当 leftleft 指向第 kk 个元素,而 rightright 指向第 k+1k+1 个元素,其中 k[0,len1)k \in [0, len-1),并且 nums[mid]>targetnums[mid] > target 时,在mid=(left+right+1)/2mid = (left + right + 1) / 2 计算方式下,rightrightmidmid 会反复指向第二个元素,进入死循环。

证明一下:当 leftleft 指向第 kk 个元素,而 rightright 指向第 k+1k+1 个元素,

  • 代入 mid=(left+right+1)/2mid = (left + right + 1) / 2 可得:mid=(right+right)/2=rightmid = (right + right) / 2 = right

而此时 nums[mid]>targetnums[mid] > targetmidmid 指针要赋值给 rightright 指针,导致 midmidrightright 指针反复赋值,故一旦命中上述两种情况,二分法将陷入“死循环”中。

故不管选择哪种计算方式,均不能满足要求(可以自行证明)。例如数组 [1,0,3,5,9,12][-1,0,3,5,9,12] 中寻找值为 22 的索引。

总结

重识二分法(上)重识二分法(中)重识二分法(下)证明了二分法四种区间模型 [left,right][left, right][left,right)[left, right)(left,right](left, right](left,right)(left, right) 的稳定性,以及其对应的循环条件以及 midmid 计算方式的有效性:

区间模型循环条件midmid 指针取值指针交换
[left,right][left, right]leftrightleft \le rightmid=(left+right)/2mid = (left + right)/2
mid=(left+right+1)/2mid = (left + right + 1) / 2
mid=(left+right1)/2mid = (left + right - 1) / 2
如果 nums[mid]>targetnums[mid] > target, right=mid1right = mid - 1
否则 left=mid+1left = mid + 1
[left,right)[left, right)left<rightleft \lt rightmid=(left+right)/2mid = (left + right) / 2
mid=(left+right1)/2mid = (left + right - 1) / 2
如果 nums[mid]>targetnums[mid] > target, right=midright = mid
否则 left=mid+1left = mid + 1
(left,right](left, right]left<rightleft \lt rightmid=(left+right+1)/2mid = (left + right + 1) / 2如果 nums[mid]>targetnums[mid] > target, right=mid1right = mid - 1
否则 left=midleft = mid
(left,right)(left, right)无解无解无解

对于二分搜索算法,最重要的三点:区间模型循环条件midmid 指针取值。熟记适合自己理解的组合,以不变应万变,方可举一反三。

参考

理论参考

二分查找的细节(左闭右闭、左闭右开、左开右闭)及其两段性


题目参考

35. 搜索插入位置