二分查找算法Ⅱ

576 阅读4分钟

写在前面

  • 文章是在前人的基础上进行总结整理再加上自己的一点理解,仅作为自己学习的记录,不作任何商业用途!
  • 如果在文章中发现错误或者侵权问题,欢迎指出,谢谢!
  • 相对于二分查找算法Ⅰ,这个版本的适合去解决更多的关于二分的题目

704. 二分查找

public int binarySearch(int[] nums, int target) {
    if (nums == null || nums.length == 0) {
        return -1;
    }
    
    int left = 0;
    int right = nums.length - 1;
    
    while (left + 1 < right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            right = mid;
        } else if (nums[mid] < target) {
            left = mid;
            // or left = mid + 1;
        } else {
            right = mid;
            // or right = mid -1;
        }
    }
    
    if (nums[left] == target) {
        return left;
    }
    
    if (nums[right] == target) {
        return right;
    }
    
    return -1;
}
  • Q1:为什么 mid = left + (right - left) / 2 来计算 mid 的值?

    • 是为了防止溢出,当 left 和 right 都接近 2^31 的时候,那么使用 mid = (left + right) / 2 的方式去计算就会导致 int 范围溢出
  • Q2:为什么使用 left + 1 < right 这个条件?

    • 先说一个 left < rightleft <= right 这两种写法,前者是当 left == right,即左右两个指针相交的时候退出循环,后者是 left == right + 1,即左指针超过右指针退出循环
    • 上面的两种方式 left 和 right 的更新逻辑没写好都有可能导致退不出循环,而 left + 1 < right 则一定会退出循环
    • left + 1 < right 退出循环的时候有 [left, right],这个区间内大多数情况下是有两个数,另外只有一个数的情况时 left 和 right 最开始就相等,也即数组只有一个元素
  • Q3:那为什么 left + 1 < right 这种方式一定可以退出循环呢?

    • 假设是使用 left < right 这个条件,查找的是 target 最后一次出现的位置,并且 left = 1right = 2 的时候,计算出 mid = 1,因为是找最后一次出现的位置,所以会进入 while 循环里面的那个 if 分支的逻辑是:left = mid。那么这种情况就会导致 left 始终等于 1,mid 也始终等于 1,最终形成死循环
    • 而使用 left + 1 < right 这种方式呢,它在相邻(或相等)的时候就退出循环了,始终不会相交,就不会导致死循环
  • Q4:为什么当 nums[mid] == target 的时候是 right = mid,而不是 return mid

    • 这个题的逻辑实际上是求 target 第一次出现的位置,只是也能求 target 在不包含重复元素的数组中的位置
  • Q5:为什么 left 和 right 的更新逻辑是 left = midright = mid

    • 实际上这道题也可以使用 left = mid + 1right = mid - 1 这个逻辑来更新,只是当碰到比如说:找第一个比 target 小的数,那么就不能 left = mid + 1,这种情况就会跳过那个最小值,因此为了方便统一,就统一使用 left = midright = mid,都不用纠结是否需要加 1 和减 1 了
  • Q6:为什么最后需要判断 nums[left] 和 nums[right]?

    • 这也是因为 left + 1 < right 这种写法导致的,最终退出循环的时候是没有去判断 left 和 right 位置的值的,所以退出循环后需要补刀
  • 二分的核心思想:通过一个 while 循环将一个 n 大小的问题转换为 n/2 大小的问题,最终变成一个 O(1) 的问题,不要想着直接在 while 里面就将 target 找到,而是想着怎么去将查找范围缩小

34. 在排序数组中查找元素的第一个和最后一个位置

public int[] searchRange(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    int[] bound = new int[2];

    // 寻找左边界
    while (left + 1 < right) {
        int mid = left + (right - left) / 2;

        if (nums[mid] == target) {
            right = mid;
        } else if (nums[mid] < target) {
            left = mid;
        } else {
            right = mid;
        }
    }

    if (nums[left] == target) {
        bound[0] = left;
    } else if (nums[right] == target) {
        bound[0] = right;
    } else {
        bound[0] = bound[1] = -1;
        return bound;
    }

    // 寻找右边界
    left = 0;
    right = nums.length - 1;

    while (left + 1 < right) {
        int mid = left + (right - left) / 2;

        if (nums[mid] == target) {
            left = mid;
        } else if (nums[mid] < target) {
            left = mid;
        } else {
            right = mid;
        }
    }

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

    return bound;
}

69. x 的平方根

  • 求 x 的平方根实际上能够使得 number^2 <= x 成立的最后一个 number
public int sqrt(int x) {
    int left = 1;
    int right = x;
    
    while (left + 1 < right) {
        int mid = left + (right - left) / 2;
        
        if (mid * mid == x) {
            return mid;
        } else if (mid * mid < x) {
            left = mid;
        } else {
            right = mid;
        }
    }
    
    if (right * right <= x) {
        return (int)right;
    }
    
    return (int)left
}
  • 如果返回值是 double 呢?
    • 如果是 double 的话,退出循环的条件就不是 left + 1 < right 了,而是 (right - left) > 1e-6
public double sqrt(int x) {
    double left = 0;
    double right = (double)x;
    
    while ((right - left) > 1e-6) {
        double mid = (left + right) / 2;  // 可以直接相加除2,因为都是 double 类型
        // ...
    }
    
    return left;  // 也可以直接返回 left 或者 right 都行,因为相差很小
}

74. 搜索二维矩阵

  • 思路1:一次二分

    • 可以看作是一个有序数组被分成了 n 段,每段就是一行。因此依然可以二分求解。 对每个数字根据其下标 i,j 进行编号,每个数字可被编号为 0~n*n-1
    • 相当于是在一个数组中的下标。然后直接像在数组中二分一样来做。取的mid要还原成二位数组中的下标,i = mid / n, j = mid % n
    • 时间复杂度为:O(logn + logm)
  • 思路2:两次二分

    • 先通过二分找到 target 在哪一行
    • 再通过二分找到在该行的哪一个
      • 时间复杂度为:O(n + m)
public boolean searchMatrix(int[][] matrix, int target) {
    if (matrix == null || matrix.length == 0) {
        return false;
    }
    if (matrix[0] == null || matrix[0].length == 0) {
        return false;
    }

    int left = 0;
    int right = matrix.length * matrix[0].length - 1;

    while (left + 1 < right) {
        int mid = left + (right - left) / 2;

        if (getMatrixValue(matrix, mid) == target) {
            return true;
        } else if (getMatrixValue(matrix, mid) < target) {
            left = mid;
        } else {
            right = mid;
        }
    }

    if (getMatrixValue(matrix, left) == target) {
        return true;
    }

    if (getMatrixValue(matrix, right) == target) {
        return true;
    }

    return false;
}

private int getMatrixValue(int[][] matrix, int index) {
    int row = index / matrix[0].length;
    int column = index % matrix[0].length;
    return matrix[row][column];
}
public boolean searchMatrix2(int[][] matrix, int target) {
    if (matrix == null || matrix.length == 0) {
        return false;
    }
    if (matrix[0] == null || matrix[0].length == 0) {
        return false;
    }

    int row = matrix.length;
    int column = matrix[0].length;

    int left = 0;
    int right = row - 1;

    while (left + 1 < right) {
        int mid = left + (right - left) / 2;

        if (matrix[mid][0] == target) {
            return true;
        } else if (matrix[mid][0] < target) {
            left = mid;
        } else {
            right = mid;
        }
    }

    // 要先对更下面的那行进行判断,保证判断 target 在哪一行不会出现问题
    if (matrix[right][0] <= target) {
        row = right;
    } else if (matrix[left][0] <= target) {
        row = left;
    } else {
        return false;
    }

    // 下面的查找逻辑就和 704 题没什么区别了
    left = 0;
    right = matrix[0].length - 1;

    while (left + 1 < right) {
        int mid = left + (right - left) / 2;

        if (matrix[row][mid] == target) {
            return true;
        } else if (matrix[row][mid] < target) {
            left = mid;
        } else {
            right = mid;
        }
    }

    if (matrix[row][left] == target) {
        return true;
    } else if (matrix[row][right] == target) {
        return true;
    }

    return false;
}

240. 搜索二维矩阵 II

  • 根据题意,每行中的整数从左到右是排序的,每一列的整数从上到下是排序的,在每一行或每一列中没有重复的整数。那么我们只要从矩阵的左下角开始向右上角找

    • 从左下角即 (m - 1,0) 处出发
    • 如果matrix[x][y] < target 下一步往右搜
    • 如果matrix[x][y] > target 下一步往上搜
    • 如果matrix[x][y] = target 下一步往 [x - 1][y + 1] 即右上角搜,因为是有序的,每一行每一列中每个数都是唯一的
  • 时间复杂度为 O(m + n)

public boolean searchMatrix(int[][] matrix, int target) {
    int m = matrix.length;
    int n = matrix[0].length;
    int count = 0;
    int x = m - 1;
    int y = 0;

    while (x >= 0 && y < n) {
        if (matrix[x][y] == target) {
            x--;
            y++;
            count++;
        } else if (matrix[x][y] < target) {
            y++;
        } else {
            x--;
        }
    }

    return count != 0;
}

35. 搜索插入位置

public int searchInsert(int[] nums, int target) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    
    int left = 0;
    int right = nums.length - 1;
    
    while (left + 1 < right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] > target) {
            left = mid;
        } else {
            right = mid;
        }
    }
    
    if (nums[left] >= target) {
        return left;
    } else if (nums[right] >= target) {
        return right;
    }
    
    return nums.length;  // target 比数组中的数都大
}

278. 第一个错误的版本

public int firstBadVersion(int n) {
    int left = 1;
    int right = n;
    
    while (left + 1 < right) {
        int mid = left + (right - left) / 2;
        
        if (isBadVersion(mid)) {
            right = mid;
        } else {
            left = mid;
        }
    }
    
    if (isBadVersion(left)) {
        return left;
    }
    
    return right;
}
  • 这道题就很好的体现了二分法的思想,二分法只是在不断的将搜索范围减小为原来的一半,在最后退出循环的几个 if 判断里面才是最终找出答案

153. 寻找旋转排序数组中的最小值

  • 思路1:使用二分法
    • 第一次分类讨论:比较nums[left]nums[right]
      • 如果 nums[left] < nums[right],说明数组没有旋转过,仍然是升序排列。我们直接 return nums[left]
      • 反之,说明数组非单调,进入到第二次分类讨论
    • 第二次分类讨论:比较 nums[left]nums[mid],其中 mid 是二分中点
      • 如果 nums[left] > nums[mid],可以证明此时数组右半边是升序的,那我们就不用考虑右半边了。最小值一定在 [left, mid] 间,令 right = mid
      • 如果 nums[left] <= nums[mid],可以证明此时数组左半边是升序的,于是我们不需要考虑左半边,令 left = mid 或者 left = mid + 1 也行
public int findMin(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    while (left + 1 < right) {
        if (nums[left] < nums[right] {
            return nums[left];
        }
        
        int mid = left + (right - left) / 2;
        if (nums[left] > nums[mid]) {
            right = mid;
        } else {
            left = mid;
        }
    }
    
    return Math.min(nums[left], nums[right]);
}
  • 思路2:使用二分法
    • 每次 nums[mid]nums[right] 比较,得到的结果也是正确的
    • 虽然遍历过程不一定会相同,但两个思路是对偶的
public int findMin(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    
    while (left + 1 < right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] > nums[right]) {
            left = mid;
        } else {
            right = mid;
        }
    }
    
    return Math.min(nums[left], nums[right]);
}

154. 寻找旋转排序数组中的最小值 II

  • 和 153 题的区别在于:会出现重复的元素,我们依旧使用旋转数组的特性,用改进后的二分查找来解决,只是因为重复元素的存在,分类讨论的条件更加精确了

  • 思路1:nums[left] 和 nums[mid] 比较

    • 第一次分类讨论:比较nums[left]nums[right]
      • 如果 nums[left] < nums[right],说明数组没有旋转过,仍然是升序排列。我们直接 return nums[left]
      • 反之,说明数组非单调,进入到第二次分类讨论
    • 第二次分类讨论:比较 nums[left]nums[mid],其中 mid 是二分中点
      • 如果 nums[left] > nums[mid],可以证明此时数组右半边是非严格升序的,那我们就不用考虑右半边了。最小值一定在 [left, mid] 间,令 right = mid
      • 如果 nums[left] < nums[mid],可以证明此时数组左半边是非严格升序的,那我们就不用考虑左半边了。最小值一定在 [mid, right] 间,令 left = mid 或者 left = mid + 1 也行
      • 如果 nums[left] == nums[mid],这就是这道题和 153 题的区别:
        • 这时我们无法确定最小元素出现在哪里,举例来说,1,1,1,3,43,3,3,1,2,最左值和中间值都相等,最小值前者出现在左半边,后者出现在右半边。所以我们无法进行二分,采用的方法是 left += 1,可以证明这种做法不会使最小值丢失。然后在新的区间继续寻找
public int findMin(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    
    while (left + 1 < right) {
        if (nums[left] < nums[right]){
            return nums[left];
        }
        
        int mid = left + (right - left) / 2;
        if (nums[left] == nums[mid]) {
            left += 1;
        }else if (nums[left] > nums[mid]) {
            right = mid; 
        } else {
            left = mid;
            // or  left = mid + 1;
        }
    }
    
    return Math.min(nums[left], nums[right]);
}
  • 思路2:nums[mid] 和 nums[right] 比较
    • 和思路1的区别在于:nums[left] == nums[mid],令 right -= 1
public int findMin(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    
    while (left + 1 < right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == nums[right]) {
            right -= 1;
        } else if (nums[mid] > nums[right]) {
            left = mid;
        } else {
            right = mid;
        }
    }
    
    return Math.min(nums[left], nums[right]);
}
  • 经过对比发现,不论是 153 题还是 154 题,都是思路2比较方便

33. 搜索旋转排序数组

image.png

  • 思路1:将 nums[mid]nums[left] 比较
public int search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    int mid = 0;
    
    while (left + 1 < right) {
        mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            return mid;
        }
        
        if (nums[left] < nums[mid]) {  // [left, mid] 是升序
            if (nums[left] <= target && target < nums[right]) {
                right = mid;
            } else {
                left = mid;
            }
        } else {
            if (nums[mid] < target && target <= nums[right]) {
                left = mid; 
            } else {
                right = mid;
            }
        }
    }
    
    if (nums[left] == target) {
        return left;
    }
    
    if (nums[right] == target) {
        return right;
    }
    
    return -1;
}
  • 思路2:将 nums[mid]nums[right] 比较
public int search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    int mid = 0;

    while (left + 1< right) {
        mid = left + (right - left) / 2;

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

        if (nums[mid] < nums[right]) {  // [mid, right] 是升序
            if (nums[mid] < target && target <= nums[right]) {
                left = mid;
            } else {
                right = mid;
            }
        } else {

            if (nums[left] <= target && target < nums[mid]) {
                right = mid;
            } else {
                left = mid;
            }
        }

    }

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

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

    return -1;
}

81. 搜索旋转排序数组 II

  • 思路1:将 nums[mid]nums[left] 比较
public boolean search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    int mid = 0;

    while (left + 1 < right) {
        mid = left + (right - left) / 2;

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

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

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

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

    return false;
}
  • 思路2:将 nums[mid]nums[right] 比较
public boolean search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    int mid = 0;
    
    while (left + 1 < right) {
        mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            return true;
        }
        
        if (nums[mid] < nums[right]) {
            if (nums[mid] < target && target <= nums[right]) {
                    left = mid; 
            } else {
                right = mid;
            }
        } else if (nums[mid] > nums[right]) {
            if (nums[left] <= target && target < nums[mid]) {
                right = mid;
            } else {
                left = mid;
            }
        } else {
            right -= 1;
        }
    }
    
    if (nums[left] == target) {
        return true;
    }
    
    if (nums[right] == target) {
        return true;
    }
    
    return false;
}

852. 山脉数组的峰顶索引

public int peakIndexInMountainArray(int[] arr) {
    int left = 0;
    int right = arr.length - 1;

    while (left + 1 < right) {
        int mid = left + (right - left) / 2;

        if (arr[mid] > arr[mid + 1]) {
            right = mid;
        } else {
            left = mid ;
        }
    }
    
    if (arr[left] < arr[right]) {
        return right;
    }
    return left;
}

1095. 山脉数组中查找目标值

  • 思路:分三个步骤完成
    • 先找到 peakIndex,将数组分为两个有序部分,左半部分是升序,右半部分是降序
    • 在左半部分通过二分查找,如果找到了则直接 return,因为是找最小的那个位置
    • 在右半部分通过二分查找,注意这里是降序查找
public int findInMountainArray(int target, MountainArray mountainArr) {
    int left = 0;
    int right = mountainArr.length() - 1;

    int peekIndex = peakIndexInMountainArray(mountainArr, left, right);

    int rightIndex = binarySearchLeftPath(mountainArr, left, peekIndex, target);
    if (rightIndex != -1) {
        return rightIndex;
    }

    int leftIndex = binarySearchRightPart(mountainArr, peekIndex + 1, right, target);
    return leftIndex;
}

public int binarySearchLeftPath(MountainArray mountainArr, int left, int right, int target) {
    while (left + 1 < right) {
        int mid = left + (right - left) / 2;

        if (mountainArr.get(mid) == target) {
            return mid; 
        } else if (mountainArr.get(mid) < target) {
            left = mid;
        } else {
            right = mid;
        }
    }

    if (mountainArr.get(left) == target) {
        return left;
    }

    if (mountainArr.get(right) == target) {
        return right;
    }

    return -1;
}

public int binarySearchRightPart(MountainArray mountainArr, int left, int right, int target) {
    while (left + 1 < right) {
        int mid = left + (right - left) / 2;

        if (mountainArr.get(mid) == target) {
            return mid; 
        } else if (mountainArr.get(mid) > target) {  // 将 < 改为 > 即可实现降序查找
            left = mid;
        } else {
            right = mid;
        }
    }

    if (mountainArr.get(left) == target) {
        return left;
    }

    if (mountainArr.get(right) == target) {
        return right;
    }

    return -1;
}

public int peakIndexInMountainArray(MountainArray mountainArr, int left, int right) {

    while (left + 1 < right) {
        int mid = left + (right - left) / 2;

        if (mountainArr.get(mid) > mountainArr.get(mid + 1) ) {
            right = mid;
        } else {
            left = mid ;
        }
    }

    if (mountainArr.get(left)  < mountainArr.get(right) ) {
        return right;
    }
    return left;
}

更多关于二分查找的题目