一文详解二分查找各种细节问题

前言

  二分查找是在有序数组中快速搜寻某元素的算法,其思想易懂,但在实现过程中却存在很多的细节问题,比如:

  • 左区间端点更新时是加一还是不加?
  • 右区间端点更新时是减一还是不减?
  • 循环的终止条件是 < < <还是 ≤ \leq ?

  接下来,我将以例题入手,带你逐步解开这些疑惑。

例题分析一

1.题目描述

  给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

2.函数定义

int binarySearch(int *nums, int numsSize, int target)
{

}

3.原题链接

LeetCode 704. 二分查找

4.题解

int binarySearch(int *nums, int numsSize, int target)
{
    int left = 0;
    int right = numsSize - 1;
    int mid;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (nums[mid] < target)
        {
            left = mid + 1;
        }
        else if (nums[mid] > target)
        {
            right = mid - 1;
        }
        else
        {
            return mid;
        }
    }
    return -1;
}

5.分析

  这是一个元素存在性问题,我们在 [ l e f t , r i g h t ] [left,right] [left,right] 中要寻找等于 t a r g e t target target 的元素,首先判断 n u m s [ m i d ] nums[mid] nums[mid] t a r g e t target target 的大小关系,如果 n u m s [ m i d ] < t a r g e t nums[mid]<target nums[mid]<target,那么要寻找的元素必定在 [ m i d + 1 , r i g h t ] [mid + 1,right] [mid+1,right] 内,所以左区间端点更新时需要加一。同理,右区间端点更新时也需要减一。
  下面再分析循环的终止条件为什么是 ≤ \leq ,我们设想这样一种情况,当左右区间端点相等即 l e f t = r i g h t left = right left=right 时, m i d = l e f t = r i g h t mid = left = right mid=left=right,此时 n u m s [ m i d ] nums[mid] nums[mid] t a r g e t target target 的大小关系还没有被判断,因此当 l e f t = r i g h t left = right left=right 时还要再循环一次。

例题分析二

1.题目描述

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

2.函数定义

int searchInsert(int *nums, int numsSize, int target)
{

}

3.原题链接

LeetCode 35. 搜索插入位置

4.题解

int searchInsert(int *nums, int numsSize, int target)
{
    int left = 0;
    int right = numsSize;						// (1)
    int mid;
    while (left < right)						// (2)
    {
        mid = left + (right - left) / 2;		// (3)
        if (nums[mid] < target)
        {
            left = mid + 1;
        }
        else
        {
            right = mid;						// (4)
        }
    }
    return right;
}

5.分析

  如果简化题意,就是要寻找 ≥ t a r g e t \geq target target 的最小值,返回其下标。如果 n u m s [ m i d ] ≥ t a r g e t nums[mid]\geq target nums[mid]target,那么 n u m s [ m i d ] nums[mid] nums[mid] 就满足了寻找的一个条件,那 n u m s [ m i d ] nums[mid] nums[mid] 是否最小呢?这我们无法确定,它可能是最小的,但也可能不是。所以我们接下来要在 [ l e f t , m i d ] [left,mid] [left,mid] 中去寻找,这对应了 ( 4 ) (4) (4) 处右区间端点更新时不减一。
  那为什么 ( 2 ) (2) (2) 处循环终止条件是 < < <呢?当 l e f t = r i g h t left = right left=right 时, [ l e f t , r i g h t ] [left,right] [left,right] 中只有一个元素 r i g h t right right, 而 r i g h t right right 其实是由之前的 m i d mid mid 更新而来,由于之前 n u m s [ m i d ] nums[mid] nums[mid] 已经判断过和 t a r g e t target target 的大小关系,所以我们不需要再判断了。那如果写 ≤ \leq ,多判断一次会怎样?我告诉你:你将会陷入此循环中万劫不复!




  哈哈哈开玩笑的啦,不就是个死循环嘛。
  设 l e f t = r i g h t = a left = right = a left=right=a,则 m i d = a mid = a mid=a,在右区间端点 r i g h t ≠ n u m s S i z e right\neq numsSize right=numsSize 的情况下,右区间端点对应的元素已经确定大于等于 t a r g e t target target了。也就是现在的 n u m s [ m i d ] nums[mid] nums[mid] 确定是 ≥ t a r g e t \geq target target 的,那么无论接下来循环多少次, r i g h t right right 都等于 a a a。在右区间端点 r i g h t = n u m s S i z e right = numsSize right=numsSize 的情况下,还会造成数组越界访问。所以当 l e f t = r i g h t left = right left=right 时循环应终止,最后返回 l e f t left left r i g h t right right 都是一样的。
  接下来,再来看 ( 1 ) (1) (1) 处,如果 t a r g e t target target 比数组最后一个元素还要大,那么其插入位置就应该是数组尾部的后一个位置,也就是对应下标为 n u m s S i z e numsSize numsSize 的地方。所以要在 [ 0 , n u m s S i z e ] [0,numsSize] [0,numsSize] 中去寻找插入位置,右区间端点 r i g h t right right 就应初始化为 n u m s S i z e numsSize numsSize
  最后说一下 ( 3 ) (3) (3) 处,首先证明在数学上与 m i d = ( l e f t + r i g h t ) / 2 mid = (left + right) / 2 mid=(left+right)/2 是等价的。证明非常简单:

m i d = l e f t + r i g h t − l e f t 2 mid = left + \frac{right - left}{2} mid=left+2rightleft
= 2 × l e f t + r i g h t − l e f t 2 =\frac{2 \times left + right - left}{2} =22×left+rightleft
= l e f t + r i g h t 2 = \frac{left + right}{2} =2left+right

  那既然如此,为什么还要采用 ( 3 ) (3) (3) 处的写法呢?难道是为了秀操作?当然不是啦。我们知道,数据是有范围的,当 l e f t left left r i g h t right right 都很大的时候,它们的和就有可能超过 32 32 32 位整型的最大值 2147483647 2147483647 2147483647,从而产生错误,而求差则不会,所以 ( 3 ) (3) (3) 处的写法是很安全的。

举一反三

  好了,看到这里相信你已经对二分查找的各种细节都有所了解了。那么接下来就容我问你三个问题:

  给定一个排序数组 n u m s nums nums 和一个目标值 t a r g e t target target,请你返回:

  1. 大于 t a r g e t target target 的最小值的下标
  2. 小于等于 t a r g e t target target 的最大值的下标
  3. 小于 t a r g e t target target 的最大值的下标

  实际上这三个问题都与例题二类似,这里我就讲解第3个问题。

int binarySearch(int *nums, int numsSize, int target)
{
	int left = -1;
	int right = numsSize - 1;
	int mid;
	while (left < right)
	{
		mid = right - (right - left) / 2;	// (1) 
		if (nums[mid] < target)
		{
			left = mid;
		}
		else
		{
			right = mid - 1;
		}
	}
	return left;
}

  这里我就只对 ( 1 ) (1) (1) 处进行分析了。至于其他的么……


  此题 ( 1 ) (1) (1) 处的写法与例题一、例题二中写法在数学上都是等价的。但在这道题中,如果用例题一或例题二的写法就必然有问题。你能思考这是为什么吗?


  考虑这样一种情况,当 l e f t = r i g h t − 1 = a left = right -1 = a left=right1=a 时,如果采用例题一或例题二的写法,不难算出 m i d = l e f t = a mid = left = a mid=left=a ,如果 l e f t ≠ − 1 left \neq -1 left=1 n u m s [ m i d ] nums[mid] nums[mid] 确定是 < t a r g e t < target <target 了,所以接下来无论循环多少次 ,都有 l e f t = r i g h t − 1 = a left = right - 1 = a left=right1=a,也就是陷入了死循环。如果 l e f t = − 1 left = -1 left=1,会造成数组越界访问。此题 ( 1 ) (1) (1) 处写法在这种情况下, m i d = r i g h t = a + 1 mid = right = a + 1 mid=right=a+1, 接下来无论 n u m s [ m i d ] nums[mid] nums[mid] t a r g e t target target 大小关系什么,都会有 l e f t = r i g h t left = right left=right,然后跳出循环。同样的,例题二也不能采用此题的写法,可以自己试着分析一下。

总结

  好了,相信经过我的一番心思缜密( 乱 七 八 糟 乱七八糟 )的分析以后,你对二分查找的各种细节问题都已了如指掌( 稀 里 糊 涂 稀里糊涂 )。但真正让你有所提升的,一定不是看我的文章,而是自己动手实践,多思考,多总结。

在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值