重试二分法(中)

4,032 阅读4分钟

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

上文分析了左 [left,right][left,right] 区间模型: 重识二分法(上),本文开始分析左 [left,right)[left,right) 区间模型。

左闭右开的区间模型

同理,既然选择了 [left,right)[left, right) ,那接下来的二分区间都要遵循这个"初心"。

1、区间初始定义

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

2、循环条件选择

依然要判断 left==rightleft == right 是否有意义?显然,对于 [left,right)[left, right) 区间, left==rightleft == right 是有没有意义的。因为 leftleft 不会有越界的情况,所以永远不会有 left==rightleft == right 的情况。故,循环条件选择:left<rightleft \lt right

3、mid指针讨论

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

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

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

此时 mid=(0+2n+1)/2=nmid = (0 + 2n+1) / 2 = n(向下取整)

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

此时 mid=(0+2n+1+1)/2=n+1mid = (0 + 2n + 1 +1) / 2 = n+1

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

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

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

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

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

此时 mid=(0+2n)/2=nmid = (0 + 2n) / 2 = n

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

此时 mid=(0+2n+1)/2=n+1mid = (0 + 2n + 1) / 2 = n+1

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

此时 mid=(0+2n1)/2=n1mid = (0 + 2n -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] \gt target, 说明 targettarget 一定在 [left,mid)[left, mid) 区间内。我们发现,此区间模型恰与我们的“初心”保持一致,即 [left,right)[left, right)。故,此时应将新的右开区间赋值给 rightright 指针,即 right=midright = mid

  • nums[mid]<targetnums[mid] \lt target,说明 targettarget 一定在 (mid,right)(mid, right)区间内。我们发现,此区间模型违背了我们的“初心”,因此新的落盘区间应为 [mid+1,right)[mid+1, right)。故,此时应将新的左闭区间赋值给 leftleft 指针,即 left=mid+1left = mid + 1

💥 注意啦!我们发现:

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

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

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

证明一下: 当 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 指针要赋值给 rightright,这样就导致 midmidrightright 反复赋值,故一旦命中上述情况,二分法将陷入“死循环”中。

mid=(left+right)/2mid = (left + right) / 2mid=(left+right1)/2mid = (left + right -1) / 2 均可以满足要求(可自行证明)。为防止计算精度溢出问题,midmid 计算方式选择为: mid=left+(rightleft)>>1mid= left + (right - left) \gt\gt 1

证明一下: 当 leftleft 指向第 kk 个元素,rightright 指向第 k+1k+1 个元素,采用 mid=(left+right)/2mid = (left + right) / 2 计算方式时,mid=k+1/2mid = k + 1/2,向下取整取 kk,即 mid=kmid = k

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

  • 此时 nums[mid]>targetnums[mid] > target,会将 midmid(上次循环的 leftleft) 赋值给 rightright,此时 leftleft == rightright, 循环退出,返回 1-1

  • 此时 nums[mid]<targetnums[mid] < target,会将 mid+1mid + 1(上次循环的 rightright) 赋值给 leftleft,此时 leftleft == rightright, 循环退出,返回 1-1

证明一下: 当 leftleft 指向第 kk 个元素,rightright 指向第 k+1k+1 个元素,采用 mid=(left+right1)/2mid = (left + right - 1) / 2 计算方式时,mid=kmid = k,这种计算方式与 mid=(left+right)/2mid = (left + right) / 2 的结果一致,不再赘述。

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 = 0;
	let right = nums.length;
	
	while(left < right) {
		const mid = left + ~~((right - left) / 2);
		// const mid = left + ~~((right - left + 1) / 2); // 不可!!!
		// const mid = left + ~~((right - left - 1) / 2); // 亦可
		
		const midVal = nums[mid];
		
		if (midVal === target) {
			return mid;
		}
		
		if (midVal > target) {
			right = mid;
		} else  {
			left = mid + 1;
		}
	}
	
	return -1;
}

递归法

/**
 * 左闭右开区间模型[left, right)下的二分法
 * 空间复杂度 O(logN)
 * 时间复杂度 O(logN)
 */
function search(nums: number[], target: number): number {
    return searchHelper(nums, target, 0, nums.length);
}

function searchHelper(nums: number[], target: number, left: number, right: number) {
    if (left >= right) {
        return -1;
    }

    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) {
        return searchHelper(nums, target, left, mid);
    }

    return searchHelper(nums, target, mid + 1, right);
}

总结

区间模型循环条件midmid 指针取值
[left,right)[left, right)left<rightleft \lt rightmid=(left+right)/2mid = (left + right) / 2mid=(left+right1)/2mid = (left + right - 1) / 2
  • 如果 [nums[mid]==target[nums[mid] == target,直接返回 midmid 提前结束二分搜索;

  • 如果 [nums[mid]>target[nums[mid] > targetleftleft 指针不动,right=midright = mid 后继续二分;

  • 如果 [nums[mid]<target[nums[mid] < targetrightright 指针不动,left=mid+1left = mid + 1 后继续二分;

接下来继续分析其他两种区间模型,即左 (left,right](left,right] 、左 (left,right)(left,right)