二分查找

前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。

以经典题为例:

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

示例:

输入: nums = [-1,0,3,5,9,12], target = 9 输出: 4 解释: 9 出现在 nums 中并且下标为 4

方法一

定义target在左闭右闭的区间中,即[left,right]。

  • while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1

示意图

二分查找 - 图3

代码

  1. class Solution {
  2. public int search(int[] nums, int target) {
  3. int left = 0;
  4. int right = nums.length - 1;
  5. // 判断是否小于最小和大于最大
  6. if(target < nums[0] || target > nums[right]) {
  7. return -1;
  8. }
  9. // 左边界小于等于右边界
  10. while(left <= right) {
  11. int middle = (left + right) / 2;
  12. // 相等直接输出
  13. if(target == nums[middle]) {
  14. return middle;
  15. }
  16. // 小于则将右边界收缩
  17. else if(target < nums[middle]) {
  18. right = middle - 1;
  19. }
  20. // 大于则将右边界收缩
  21. else if(target > nums[middle]) {
  22. left = middle + 1;
  23. }
  24. }
  25. return -1;
  26. }
  27. }

方法二

定义target在左闭右开的区间中,即[left,right]。

  • while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
  • if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]

示意图

二分查找 - 图4

代码:

  1. class Solution {
  2. public int search(int[] nums, int target) {
  3. int left = 0;
  4. int right = nums.length;
  5. // 判断是否小于最小和大于最大
  6. if(target < nums[0] || target > nums[right-1]) {
  7. return -1;
  8. }
  9. // 左边界小于右边界
  10. while(left < right) {
  11. int middle = (left + right) / 2;
  12. // 相等直接输出
  13. if(target == nums[middle]) {
  14. return middle;
  15. }
  16. // 小于则将右边界收缩
  17. else if(target < nums[middle]) {
  18. right = middle; // 此时边界需要为[middle,right)
  19. }
  20. // 大于则将右边界收缩
  21. else if(target > nums[middle]) {
  22. left = middle + 1; // 此时边界需要为[middle+1,right)
  23. }
  24. }
  25. return -1;
  26. }
  27. }

注意:在left,right都为Integer.MAX_VALUE时会造成越界,所以可以使用int mid = left + ((right - left) / 2);代替 int middle = (left + right) / 2;

相关题目

搜索插入位置

题目描述:(力扣链接二分查找 - 图5

解题方法

解题流程:carl哥的解题流程

贴上代码:

方法一:

自己使用的时左闭右闭的写法,此处有四种情况:

  • 目标值在数组中
  • 目标值在数组的范围
  • 目标值在数组范围的右侧
  • 目标值在数组范围的左侧

此处先使用经典的二分查找的方法,可以先判断出第一种情况,最后返回值为right + 1(注意此处包括了三种情况

  1. class Solution {
  2. public int searchInsert(int[] nums, int target) {
  3. int left = 0,right = nums.length - 1;
  4. // 循环判断
  5. while(left <= right) {
  6. int middle = (right + left) / 2;
  7. if(target == nums[middle]) {
  8. return middle;
  9. }
  10. else if(target < nums[middle]) {
  11. right = middle - 1;
  12. }
  13. else if(target > nums[middle]) {
  14. left = middle + 1;
  15. }
  16. }
  17. // 返回情况
  18. // 目标值在所有数组的后面right + 1
  19. // 目标值在数组中 middle
  20. // 目标值在数组前,
  21. // 此时经过循环之后,right在等于0时也会循环一次,所以最终循环之后right的结果为-1
  22. return right + 1;
  23. }
  24. }

方法二

暴力解法

  1. int searchInsert(vector<int>& nums, int target) {
  2. for (int i = 0; i < nums.size(); i++) {
  3. // 分别处理如下三种情况
  4. // 目标值在数组所有元素之前
  5. // 目标值等于数组中某一个元素
  6. // 目标值插入数组中的位置
  7. if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果
  8. return i;
  9. }
  10. }
  11. // 目标值在数组所有元素之后的情况
  12. return nums.size(); // 如果target是最大的,或者 nums为空,则返回nums的长度
  13. }

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

题目描述:(力扣链接

二分查找 - 图6

解题方法

解题流程:carl哥的解题流程

此处也有三种情况

  • 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
  • 情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
  • 情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
  1. class Solution {
  2. public int[] searchRange(int[] nums, int target) {
  3. int rightBorder = getRightBorder(nums, target);
  4. int leftBorder = getLeftBorder(nums, target);
  5. // 数字值范围超过数组,就是左边界在右边或者有边界在左边
  6. // 情况二
  7. if(rightBorder == -2 || leftBorder == -2) return new int[]{-1, -1};
  8. // 情况一
  9. if(rightBorder - leftBorder > 1) return new int[]{leftBorder + 1, rightBorder - 1}; // 此处最后算出的right和left多减了一和多加了一
  10. // 情况三
  11. return new int[]{-1, -1};
  12. // 数字范围在数组中,但数组中无此值
  13. }
  14. public static int getRightBorder(int[] nums, int target) {
  15. int left = 0, right = nums.length - 1;
  16. int rightBorder = -2;
  17. // 寻找右边界
  18. while (left <= right) {
  19. int middle = (right + left) / 2;
  20. if(target < nums[middle]) {
  21. right = middle - 1;
  22. }else { // 当target==nums[middle]时,此时将left等于middle+1
  23. left = middle + 1;
  24. rightBorder = left; // 将left作为右边界
  25. }
  26. }
  27. return rightBorder;
  28. }
  29. public static int getLeftBorder(int[] nums, int target) {
  30. int left = 0, right = nums.length - 1;
  31. int leftBorder = -2;
  32. // 寻找左边界
  33. while (left <= right) {
  34. int middle = (right + left) / 2;
  35. if(target <= nums[middle]) {
  36. right = middle - 1; // 当target==nums[middle]时,此时将right等于middle+1
  37. leftBorder = right; // 将right作为左边界
  38. }else {
  39. left = middle + 1;
  40. }
  41. }
  42. return leftBorder;
  43. }
  44. }

X的平方根

题目描述(力扣链接

二分查找 - 图7

解题方法:

此处也可以使用二分法,注意最后需要返回right,当middle平方等于x时直接返回,若没有等于,需要返回前一个值,此时right在最后被减了一,所以直接返回right即可。

  1. public static int mySqrt(int x) {
  2. // 判断为0,1时直接返回
  3. if (x == 0 || x == 1) {
  4. return x;
  5. }
  6. int left = 1;
  7. int right = x / 2; // 直接将right赋值为x的二分之一,减少计算
  8. while (left <= right) {
  9. int middle = (right + left) / 2;
  10. // 使用除法代替乘法,防止使用乘法溢出
  11. if (middle == (x/middle)) {
  12. return middle;
  13. }
  14. else if (middle > (x / middle)) {
  15. right = middle - 1;
  16. }
  17. else if (middle < (x / middle)) {
  18. left = middle + 1;
  19. }
  20. }
  21. return right;
  22. }

有效的完全平方数

题目描述(力扣链接

二分查找 - 图8

解题方法:

与上一题一致,但注意此处范围过大,需要使用long类型。right也可以从num/2开始查找,但是注意判断num==1的情况。

  1. class Solution {
  2. public boolean isPerfectSquare(int num) {
  3. // 数据过大,使用long类型
  4. long left = 1;
  5. long right = num;
  6. while(left <= right) {
  7. long middle = (right + left) / 2;
  8. if(middle*middle == num) {
  9. return true;
  10. }
  11. else if(middle*middle < num) {
  12. left = middle + 1;
  13. }
  14. else if(middle*middle > num) {
  15. right = middle - 1;
  16. }
  17. }
  18. return false;
  19. }
  20. }


33. 搜索旋转排序数组

题目描述

image.png

解题思路

https://leetcode.cn/problems/search-in-rotated-sorted-array/solution/sou-suo-xuan-zhuan-pai-xu-shu-zu-by-leetcode-solut/
注意要求的时间复杂度为O(log n),所以可以使用二分法。
但是直接使用二分不行,因为中间不是升序的。所以需要进行判断:
image.png
image.png

  1. public int search(int[] nums, int target) {
  2. int n = nums.length;
  3. if (n == 0) return 0;
  4. if (n == 1) return nums[0] == target ? 0 : -1;
  5. int l = 0, r = n - 1;
  6. while (l <= r) {
  7. int mid = (l + r) / 2;
  8. if (nums[mid] == target) return mid;
  9. // 左边有序
  10. if (nums[0] <= nums[mid]) {
  11. if (nums[0] <= target && target <= nums[mid]) {
  12. r = mid - 1;
  13. } else {
  14. l = mid + 1;
  15. }
  16. } else {
  17. if (nums[mid] <= target && target <= nums[n - 1]) {
  18. l = mid + 1;
  19. }else {
  20. r = mid - 1;
  21. }
  22. }
  23. }
  24. return -1;
  25. }