vlambda博客
学习文章列表

二分查找边界条件一些思考

今天在刷算法题目的时候碰到一个二分查找的题目;

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:

输入: 4 输出: 2

在解答的过程中,遇到一些边界条件,始终没想明白为什么边界条件要这么设置,所以把最原始的二分查找找来、仔细研读一下其中的边界条件问题?想以此提供给自己今后正确处理二分查找边界问题的通用思考方式。

通常一个二分查找的写法是这样的

public static int search(int[] array, int target) {
  int left  = 0;
  int right = array.length - 1;
  while (left <= right) {  ①
    int mid = left + ((right - left) >> 1);②
    if (array[mid] == target) {
      return mid;
    }
    if (array[mid] < target) {  ③
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
  return -1;
}

这里面有三个关键点

① 为什么一定要使用 <= 否则就会死循环

② 处如果 (left + right) / 2 那么有可能会发生溢出,例如 0~Integer.MAX_VALUE 那么当 left = Integer.MAX_VALUE/2 right = Integer.MAX_VALUE 此时就会发生整数溢出、从而使的结果错误

③ 出为什么要先判断 array[mid] < target

对于很久没有写二分查找的人来说、以上三点尤其是第一点和第三点涉及到边界的问题更是容易出错,有时候也想不明,今天重新拿到二分查找的代码看看,想从里面寻找到一些可以解释的东西。而不是死机硬背,从而对于类似于二分查找的题目的边界条件在脑子里面有自己的思考方式。而不是碰运气。

因我的我的逻辑是这么写的。

if (array[mid] == target) {
  return mid;
}
if (array[mid] < target) {  
  left = mid + 1;
else {
  right = mid - 1;
}

考虑一种极端情况在1~9中寻找9、那么在整个二分查找的过程中,right指针始终不会动,left指针一步一步的逼近right指针,当剩余8~9两个元素的时候、left指向8,right指向9,此时mid=8,left=right=9,如果边界条件是left<right,那么循环就会退出,从而没有找到元素,实际上元素是存在的。

也就是说left和right指针实际上都是可能指向最终的结果。

在看我上面那个问题。我最终的解答方式是。

public int mySqrt(int x) {
   if (x == 1) {
     return 1;
   }
   int left  = 0;
   int right = x ;
   while (right - left > 1) {
    int m = left + ((right - left) >> 1);
    if (x / m < m) {
       right = m;
    } else {
       left = m;
    }
   }
   return left;
}

此问题同样、在枚举1~x的过程中,因为我的逻辑是这么写的

if (x / m < m) {
  right = m;
else {
  left = m;
}

也就是说当 x/m >= m 的时候 left=m,证明 left要么是x的平方根、要么left小于x的平方根。right肯定是大于x的平方根。

所以在结果只剩下两个元素的时候,循环就可以结束了。left必然是x的正整数平方根。所以边界条件是 right - left > 1也就是元素还有两个的时候停止处理。

总结上来说就是一定要明白我们定义的变量的实际意义是什么、第一二分查找中、我们定义的left和right就是有可能是最终的target值,而第二个题目中的left指针指向的值必然小于等于x的平方,right指针必然大于x的平方。所以当只有两个元素的时候就可以确定left为x的正整数平方根。