二分查找算法

本文详细介绍了二分查找算法,包括递归与非递归实现,以及其局限性。文章通过多个算法题,如求平方根、查找特定元素等,展示了二分查找的应用。此外,还探讨了在旋转排序数组、山脉数组等场景中如何应用二分查找。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

二分查找

二分查找针对的是一个有序的数据集合,查找思想有点儿类似于分治思想。每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为0.
二分查找的时间复杂度为O(logn)

二分查找的递归与非递归实现

非递归实现


public int bsearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;

  while (low <= high) {
    int mid = (low + high) / 2;
    if (a[mid] == value) {
      return mid;
    } else if (a[mid] < value) {
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }

  return -1;
}

容易出错的三个地方

  1. 循环退出条件low<=high,而不是low<high;
    2.mid的取值:实际上,mid=(low+high)/2 这种写法是有问题的。因为如果 low 和 high 比较大的话,两者之和就有可能会溢出。改进的方法是将 mid 的计算方式写成 low+(high-low)/2。更进一步,如果要将性能优化到极致的话,我们可以将这里的除以 2 操作转化成位运算 low+((high-low)>>1)。因为相比除法运算来说,计算机处理位运算要快得多。
    3.low和high的更新: low=mid+1,high=mid-1。注意这里的 +1 和 -1,如果直接写成 low=mid 或者 high=mid,就可能会发生死循环。比如,当 high=3,low=3 时,如果 a[3]不等于 value,就会导致一直循环不退出。

递归实现


// 二分查找的递归实现
public int bsearch(int[] a, int n, int val) {
  return bsearchInternally(a, 0, n - 1, val);
}

private int bsearchInternally(int[] a, int low, int high, int value) {
  if (low > high) return -1;
  int mid =  low + ((high - low) >> 1);//注意不能写成low + (high - low) >> 1,需要把左移的用括号括起来,因为左移的优先级低,先加法在左移,有可能会出现死循环~~
  if (a[mid] == value) {
    return mid;
  } else if (a[mid] < value) {
    return bsearchInternally(a, mid+1, high, value);
  } else {
    return bsearchInternally(a, low, mid-1, value);
  }
}

二分查找的局限性

1.二分查找依赖的是顺序表结构,简单点说就是数组
2.二分查找针对的是有序数据
3.数据量太小不适合二分查找,数据量太大也不适合二分查找

二分查找算法题

1 求平方根

给定一个非负整数 x ,计算并返回 x 的平方根,即实现 int sqrt(int x) 函数。
正数的平方根有两个,只输出其中的正数平方根。
如果平方根不是整数,输出只保留整数的部分,小数部分将被舍去。
思路:二分查找,注意不要溢出,所以后面mid*mid 强转成long类型

 public int mySqrt(int x) {
        int left =0,right = x; 
        int mid = 0; 
        while(left <= right){
            mid = left+(right-left)/2;
            if((long)mid*mid<=x && (long)(mid+1)*(mid+1)>x){
                return mid;
            }else if((long)mid*mid < x){
                left = mid+1;
            }else{
                right = mid-1;
            }
        }
        return mid;
    }

2.求一个数的平方根,保留6位小数

需要考虑特殊情况,当x大于0小于1的情况,此时left和right需要交换,此处没有变

  public double sqrts(double x ,double per){
        double left=0,right = x;
        while(left<= right){
            double mid = left+(right-left)/2;
            System.out.println(mid);
            if(Math.abs(mid*mid-x)<=per){
                return mid;
            }else if(mid*mid >x){
                right = mid;//-0.00001;
            }else {
                left = mid;//+0.00001;
            }
        }//2.82842724685669
        return -1;
    }

3 查找第一个值等于给定值的元素

思路:最简单的思路就是先查到该值后,然后在向前一个个的遍历,直到为0或者前面的一个数不等于要找的target为止,这种效率低,对于数据量少的还可以,对于数据量大的数组来说,效率低
进阶思路,在二分查找中进行。


public int bsearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid =  low + ((high - low) >> 1);
    if (a[mid] > value) {
      high = mid - 1;
    } else if (a[mid] < value) {
      low = mid + 1;
    } else {
      if ((mid == 0) || (a[mid - 1] != value)) return mid;
      else high = mid - 1;
    }
  }
  return -1;
}

4 查找最后一个值等于给定值的元素


public int bsearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid =  low + ((high - low) >> 1);
    if (a[mid] > value) {
      high = mid - 1;
    } else if (a[mid] < value) {
      low = mid + 1;
    } else {
      if ((mid == n - 1) || (a[mid + 1] != value)) return mid;
      else low = mid + 1;
    }
  }
  return -1;
}

5 查找第一个大于等于给定值的元素


public int bsearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid =  low + ((high - low) >> 1);
    if (a[mid] >= value) {
      if ((mid == 0) || (a[mid - 1] < value)) return mid;
      else high = mid - 1;
    } else {
      low = mid + 1;
    }
  }
  return -1;
}

6 查找最后一个小于等于给定值的元素


public int bsearch7(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid =  low + ((high - low) >> 1);
    if (a[mid] > value) {
      high = mid - 1;
    } else {
      if ((mid == n - 1) || (a[mid + 1] > value)) return mid;
      else low = mid + 1;
    }
  }
  return -1;
}

7 搜索插入位置

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

请必须使用时间复杂度为 O(log n) 的算法。
思路:看到要求的时间复杂度就想到了二分查找,这个是二分查找的变形

 public int searchInsert(int[] nums, int target) {
        if(nums.length == 0){
            return 0;
        }
        int n = nums.length;
        if(target<nums[0]){
            return 0;
        }
        if(target>nums[n-1]){
            return n;
        }
        int ans = 0;
        int left = 0,right = n-1;
        while(left<=right){
            int mid = left+(right-left)/2;
            if(nums[mid]==target){
                ans = mid;
                break;
            }else if(nums[mid]<target){
                left = mid+1;
            }else{
                right = mid-1;
                ans = mid;
            }
        }
        return ans;
    }

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

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
思路:此题中的数组是互不相同的,后面是此题的进阶,可能会有数组元素相同,
注意本题中的旋转数组,分为两种状态,一种是单调递增的;一种是分成左右两个部分,每个部分都是单调递增的,但是左半部分的值都大于右半部分的值。

 public int findMin(int[] nums) {
        if(nums.length == 0){
            return -1;
        }
        int left = 0,right =nums.length-1;
        while(left<=right){
            int mid = left+(right-left)/2;
            if(nums[mid]<nums[right]){
                right = mid;
            }else{//nums[mid]>=nums[right]
                left = mid+1;
            }
        }
        return nums[right];
    }

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

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
思路:此题和第八题类似,主要区别是如果在判断左移和右移的过程中,有可能nums[mid]nums[right]的情况,如果midright,则此时就是最小值,如果right>0的情况下,nums[mid]等于nums[right],则right=right-1,因为有nums[mid]保留了可能是最小的结果。
第二次刷此题的感受:此题和上题的主要区别就是此题中的数组中的数可能是重复的,在判断哪部分数组为有序的递增的数组的前提下,还需要将左右和num[mid]重复的数据都要删除掉.
官方代码有更好的答案,可以参考154题

 public int findMin(int[] nums) {
        if(nums.length ==0){
            return -1;
        }
        int left = 0,right = nums.length-1;
        int mid =0;
        while(left<=right){
            mid = left+(right-left)/2;
            if(nums[left]==nums[mid]&&nums[mid]==nums[right]){
                left++;
                right--;
            }else if(nums[mid]<=nums[right]){
                right = mid;
            }else{
                left=mid+1;
            }
        }
        return nums[mid];
    }

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

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

思路:此题首先想到的是用二分法找到目标元素,然后在向左或者向右移动找到左右边界,如下代码所示,这种方法如果在数据量较小的情况下还可以,如果数组较大的化,会出现时间复杂度变成O(n),所以需要将条件判断添加到二分的条件中去做。

public int[] searchRange(int[] nums, int target) {
        int left = 0,n = nums.length,right = n-1;
        int[]res= {-1,-1};
        if(nums == null|| nums.length ==0){
            return res;
        }
        if(target>nums[right] || target<nums[left]){
            return res;
        }
        int mid =0;boolean flag = false;
        while(left <=right){
            mid = left+(right-left)/2;
            if(nums[mid]==target){
                flag = true;
                break;
            }else if(nums[mid]>target){
                right = mid-1;
            }else{
                left = mid+1;
            }
        }
        if(flag){
            left = mid;right=mid;
        while(left>=0 &&nums[left]==target){
            left--;
            if(left< 0 || nums[left]!=target){
                break;
            }
        }
        while(right<=n-1 && nums[right]==target){
            right++;
            if(right>n-1 || nums[right]!=target){
                break;
            }
        }
        res[0] = left+1;
        res[1] = right-1;
        }
        
        return res;
    }

利用二分法,将时间复杂度控制在O(logn),边界条件~~~

public int[] searchRange(int[] nums, int target) {
        int n = nums.length;
        int[]res= {-1,-1};
        if(nums == null|| nums.length ==0){
            return res;
        }
        if(target>nums[nums.length-1] || target<nums[0]){
            return res;
        }
       int left = search(nums,0,n-1,target,true);
       int right = search(nums,0,n-1,target,false);
       res[0]=left;res[1]=right;
        return res;
    }
    public int search(int[]nums,int left,int right,int target,boolean flag){
        while(left <=right){
            int mid = left+(right-left)/2;
            if(nums[mid]>target){
                right = mid-1;
            }else if(nums[mid]< target){
                left = mid+1;
            }else{
                if(flag){//寻找左边界
                    if(mid==0 || nums[mid-1]!=target){
                        return mid;
                    }else{
                        right = mid-1;
                    }
                }else{
                    if(mid==nums.length-1 || nums[mid+1]!=target){
                        return mid;
                    }else{
                        left = mid+1;
                    }
                }
            }
        }
        return -1;
    }
    

11.搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

思路:
二分查找总体框架不变,主要是while循环内判断上,如果恰好nums[mid]=target返回,否则,去寻找单调递增的在数组的哪一边,利用局部单调性缩小搜索范围(只有数组中的数不重复的时候才能用单调性判断,否则去找左右两侧),根据单调递增的左右两端判断target是否在这里,如果在的化取这半部分,否则的化取另半部分;

public int search(int[] nums, int target) {
        //思路1,先将旋转数组转回到正常的升序排列,然后在用二分查找进行查询
        //思路2,找到转折点,然后将数组分成两部分,分别用二分法去做【最小值】
        //思路3,二分法直接去求
        if(nums==null || nums.length ==0){
            return -1;
        }
        int left = 0,right = nums.length-1;
        while(left <= right){
            int mid = left+(right-left)/2;
            if(nums[mid]==target){
                return mid;
            }
            //找到单调的数组
            if(nums[mid]<nums[right]){
                if(nums[mid]<target&&nums[right]>=target){
                    left= mid+1;
                }else{
                    right = mid-1;
                }
            }else{
                if(nums[left]<=target&&nums[mid]>target){
                    right = mid-1;
                }else{
                    left = mid+1;
                }
            }

        }
        return -1;

    }

12.搜索旋转排序数组ii

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
思路:这个题目是11题的延伸 ,主要是此题中数组可以是相同的,所以就有可能出现[ 1,1,1,1,2,1,1]这种情况,nums[mid]和nums[left]和nums[right]是相等的,我们没有办法去判断哪一侧是单调的,所以需要将重复相等的去掉,详见下方代码

class Solution {
    public boolean search(int[] nums, int target) {
        int left = 0,right = nums.length-1;
        while(left<=right){
            int mid = left+(right-left)/2;
            if(nums[mid] == target){
                return true;
            }
            if(nums[left]==nums[mid]&&nums[mid]==nums[right]){//将相同的去掉,左右一起,直到有一个不等于nums[mid]为止
                left++;
                right--;
            }else if(nums[mid]<=nums[right]){//[mid,right]单调递增的
                if(nums[right]==target){
                    return true;
                }
                if(nums[mid]<target && target<=nums[right]){
                    left = mid+1;
                }else{
                    right = mid-1;
                }
            }else{
                if(nums[left]==target){
                    return true;
                }
                if(nums[left]<=target&&target<nums[mid]){
                    right = mid-1;
                }else{
                    left = mid+1;
                }
            }            
        }
        return false;
    }
}

更地道的解法

13.山脉数组的峰顶索引

符合下列属性的数组 arr 称为 山脉数组 :
arr.length >= 3
存在 i(0 < i < arr.length - 1)使得:
arr[0] < arr[1] < … arr[i-1] < arr[i]
arr[i] > arr[i+1] > … > arr[arr.length - 1]
给你由整数组成的山脉数组 arr ,返回任何满足 arr[0] < arr[1] < … arr[i - 1] < arr[i] > arr[i + 1] > … > arr[arr.length - 1] 的下标 i 。
思路:这个属于二分中的另一个类别的题目,这个题目中左右两侧一个是递增的序列一个是递减的序列,而不像上面的循环数组一样,都是单调递增的~所以在判断哪一部分是单调的情况下,需要借助于mid和mid+1的大小来判断,而不能直接和left或者right进行判断。

public int peakIndexInMountainArray(int[] arr) {
        int left = 0,right = arr.length-1;
        int mid =0;
        while(left<=right){
            mid = left+(right-left)/2;
            if(arr[mid]>arr[mid-1]&&arr[mid]>arr[mid+1]){
                return mid;
            }
            if(arr[mid]<arr[mid+1]){
                left = mid+1;
            }else{
                right = mid;
            }
        }
        return mid;
    }

官方答案很优雅

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

(这是一个 交互式问题 )

给你一个 山脉数组 mountainArr,请你返回能够使得 mountainArr.get(index) 等于 target 最小 的下标 index 值。

如果不存在这样的下标 index,就请返回 -1。

何为山脉数组?如果数组 A 是一个山脉数组的话,那它满足如下条件:

首先,A.length >= 3

其次,在 0 < i < A.length - 1 条件下,存在 i 使得:

A[0] < A[1] < … A[i-1] < A[i]
A[i] > A[i+1] > … > A[A.length - 1]

你将 不能直接访问该山脉数组,必须通过 MountainArray 接口来获取数据:
MountainArray.get(k) - 会返回数组中索引为k 的元素(下标从 0 开始)
MountainArray.length() - 会返回该数组的长度
注意:
对 MountainArray.get 发起超过 100 次调用的提交将被视为错误答案。此外,任何试图规避判题系统的解决方案都将会导致比赛资格被取消。
思路:这个题目是上个题目的延伸~先求出山顶值,然后分左右两部分分别去求是否含有target的值。

public int findInMountainArray(int target, MountainArray mountainArr) {
        //思路:先找到封顶元素,然后在分两部分去查找
        if(mountainArr.get(0)>target&&mountainArr.get(mountainArr.length()-1)>target){
            return -1;
        }
        int mid = findHigh(mountainArr);
        if(mountainArr.get(mid)<target){
            return -1;
        }
        //分成左右两组分别去判断是否含有target
        //左侧单调递增,右侧单调递减
        int left = searchLeft(mountainArr,mid,target);
        int right = searchRight(mountainArr,mid,target);
        return left == -1 ? right:left;      
    }
    public int searchLeft(MountainArray mountainArr,int middle,int target){
        int left = 0,right = middle;
        while(left<=right){
            int mid = left+(right-left)/2;
            int num = mountainArr.get(mid);
            if(num==target){
                return mid;
            }else if(num<target){
                left = mid+1;
            }else{
                right = mid-1;
            }
        }
        return -1;
    }
    public int searchRight(MountainArray mountainArr,int middle,int target){
        int left = middle,right = mountainArr.length()-1;
        while(left<=right){
            int mid = left+(right-left)/2;
            int num = mountainArr.get(mid);
            if(num==target){
                return mid;
            }else if(num<target){
                right = mid-1;
            }else{
                left = mid+1;
            }
        }
        return -1;
    }
    public int findHigh(MountainArray mountainArr){//找到最大值
        int left = 0,right = mountainArr.length()-1;
        int mid = 0;
        while(left<=right){
            mid = left+(right-left)/2;
            int num_mid = mountainArr.get(mid);
            int num_mid1 = mountainArr.get(mid+1);
            int num_mid_1 = mountainArr.get(mid-1);
            if(num_mid>num_mid1 && num_mid>num_mid_1){
                return mid;
                }
            if(num_mid>num_mid1){
                right = mid;
            }else{
                left = mid+1;
            }
        }
        return mid;
    }

二分法做题感受:
1.普通的二分查找:
left<=right;
left=mid+1,right=mid-1;
mid = left+((right-left)>>1)
2.循环数组做题思路:
需要根据mid和left和right的值进行判断,判断哪一部分是单调的,判断所要找的数是否在单调数组内,在决定left和right的位置变化。
遇到数组内的数可以有重复的情况下,需要将所有的nums[mid]和 nums[left] 和nums[right]都相等的数给删除掉,left++,right–,再继续去做。

二分查找力扣集合

二分查找题目集合

待做题目:
动态规划+二分查找

寻找重复数

找到k个最近接的元素

转变数组后最接近目标值的数组和

题型三 放弃~没做

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值