【Java算法】二分查找 上

   🔥个人主页: 中草药

🔥专栏:【算法工作坊】算法实战揭秘


🏟️一. 二分查找 

题目链接:704.二分查找

 算法原理

二分查找算法的基本思想是通过将搜索区间分成两半,然后根据目标值与中间值的比较结果来缩小搜索范围,从而减少搜索次数。具体步骤如下:

  1. 初始化两个指针,left 和 right,分别指向数组的起始位置和结束位置。
  2. 当 left <= right 时,执行以下操作:
    • 计算中间位置 mid = left + (right - left) / 2。这里使用 (right - left) / 2 而不是 (left + right) / 2 是为了避免在 left 和 right 都非常大的情况下发生整数溢出。
    • 检查中间位置的元素 nums[mid] 是否等于目标值 target
      • 如果 nums[mid] < target,说明目标值可能在右半部分,因此更新 left 为 mid + 1
      • 如果 nums[mid] > target,说明目标值可能在左半部分,因此更新 right 为 mid - 1
      • 如果 nums[mid] == target,则找到目标值,返回其索引 mid
  3. 如果没有找到目标值,即循环结束后,返回 -1 表示目标值不在数组中。

代码分析

  • 初始化left = 0 和 right = nums.length - 1,定义了搜索的初始范围。
  • 循环条件while (left <= right) 确保搜索不会超出范围,并且当 left 和 right 相遇或交叉时停止。
  • 中间点计算int mid = left + (right - left) / 2,避免整数溢出。
  • 比较与更新:根据 nums[mid] 与 target 的关系,更新 left 或 right,逐步缩小搜索范围。
  • 返回结果:如果找到目标值,返回其索引;否则,返回 -1 表示未找到。

二分查找的时间复杂度为 O(log n),其中 n 是数组长度,是一种高效的查找算法。

代码

 public int 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){
                left=mid+1;
            }else if (nums[mid]>target){
                right=mid-1;
            }else {
                return mid;
            }
        }
        return -1;
    }

举例

测试用例 nums=[-1,0,3,5,9,12] , target=9

当使用测试用例 nums = [-1, 0, 3, 5, 9, 12]target = 9 分析这段二分查找代码时,我们可以逐步跟踪算法的执行过程:

  1. 初始化:

    • left = 0right = nums.length - 1 = 5
  2. 第一次循环:

    • 计算 midmid = left + (right - left) / 2 = 0 + (5 - 0) / 2 = 2
    • 比较 nums[mid] 与 target:
      • nums[mid] = nums[2] = 3 小于 target = 9
    • 更新 leftleft = mid + 1 = 3
  3. 第二次循环:

    • 更新后的 left = 3right = 5
    • 计算 midmid = left + (right - left) / 2 = 3 + (5 - 3) / 2 = 4
    • 比较 nums[mid] 与 target:
      • nums[mid] = nums[4] = 9 等于 target = 9
    • 找到目标值,返回 mid = 4

因此,在这个测试用例中,算法最终会返回 4,这是目标值 9 在数组中的索引位置。

这个过程展示了二分查找如何有效地将搜索范围不断减半,直到找到目标值或确定目标值不存在于数组中。在这个例子中,我们从整个数组开始,经过两次比较就找到了目标值,这体现了二分查找算法的高效性。

🏡二.在排序数组中寻找第一个和最后一个位置

题目链接:34.在排序数组中寻找第一个和最后一个位置

算法原理 

这段代码实现了在有序数组中查找一个给定目标值的起始位置和结束位置的算法,即找出目标值出现的第一个和最后一个位置。算法主要分为两个部分:查找左端点和查找右端点,这里使用了修改版的二分查找算法。

查找左端点

为了找到目标值的最左侧位置,我们需要确保在 nums[mid] >= target 的情况下,仍然有机会向左移动,因为可能存在 nums[mid] == target 但是 mid 不是最左侧的情况。因此,这里的循环条件是 left < right,并且在 nums[mid] >= target 的情况下,将 right 设置为 mid,而不是常见的 mid - 1。这样可以确保即使 nums[mid] 等于目标值,我们也会继续在左边查找,直到找到第一个等于目标值的元素或者 leftright 相遇。

查找右端点

查找右端点的过程与查找左端点类似,但是有几个关键区别:

  • 循环条件同样为 left < right,但计算 mid 时加上 1mid = left + (right - left + 1) / 2。这是因为我们要找的是目标值的最右侧位置,如果不加 1,可能会陷入无限循环,尤其是在数组中目标值重复的情况下。
  • 在 nums[mid] <= target 的情况下,我们将 left 设置为 mid,而不是常见的 mid + 1。这样可以确保即使 nums[mid] 等于目标值,我们也会继续在右边查找,直到找到最后一个等于目标值的元素或者 left 和 right 相遇。

代码分析

  1. 初始化

    • 定义返回数组 ret 初始值为 [-1, -1]
    • 检查边界情况,如果数组为空,则直接返回 ret
  2. 查找左端点

    • 使用二分查找,不断更新 left 和 right,直到找到目标值的第一个位置,或确认目标值不存在。
    • 如果找到目标值,更新 ret[0] 为该位置。
  3. 查找右端点

    • 重新初始化 left 和 right,再次进行二分查找,但这次是为了找到目标值的最后一个位置。
    • 更新 ret[1] 为该位置。

这种算法的时间复杂度为 O(log n),其中 n 是数组的长度,因为它对每个端点都进行了独立的二分查找。这种方法适用于处理需要同时找到目标值起止位置的问题,例如在统计目标值出现次数或确定目标值的范围等场景。

代码

 public int[] searchRange(int[] nums, int target) {
        int[] ret={-1,-1};
        //判断边界情况
        if(nums.length==0){
            return ret;
        }
        //寻找左端点
        int left=0,right=nums.length-1;
        while(left<right){
            int mid=left+(right-left)/2;
            if(nums[mid]<target){
                left=mid+1;
            }else{
                right=mid;
            }
        }
        //判断是否有符合要求的值
        if(nums[left]==target){
            ret[0]=left;
        }else{
            return ret;
        }
        //寻找右端点
        left=0;right=nums.length-1;
        while(left<right){
            int mid=left+(right-left+1)/2;//寻找右端点防止死循环
            if(nums[mid]<=target){
                left=mid;
            }else{
                right=mid-1;
            }
        }

        ret[1]=left;
        return ret;
    }

举例

测试用例 nums=[5,7,7,8,8,10],targer=8

使用测试用例 nums = [5, 7, 7, 8, 8, 10]target = 8 来分析上述代码,我们可以详细地跟踪代码的执行流程,以便理解它是如何找到目标值 8 的起始位置和结束位置的。

查找左端点

  1. 初始化:
    • left = 0right = nums.length - 1 = 5
  2. 第一次循环:
    • 计算 midmid = left + (right - left) / 2 = 0 + (5 - 0) / 2 = 2
    • 比较 nums[mid] 与 target:
      • nums[mid] = nums[2] = 7 小于 target = 8
    • 更新 leftleft = mid + 1 = 3
  3. 第二次循环:
    • 更新后的 left = 3right = 5
    • 计算 midmid = left + (right - left) / 2 = 3 + (5 - 3) / 2 = 4
    • 比较 nums[mid] 与 target:
      • nums[mid] = nums[4] = 8 等于 target = 8
    • 更新 rightright = mid = 4
  4. 第三次循环:
    • 更新后的 left = 3right = 4
    • 因为 left < right 不再成立,循环结束
    • 检查 nums[left]:
      • nums[left] = nums[3] = 8 等于 target = 8
    • 更新 ret[0] 为 left = 3

查找右端点

  1. 初始化:
    • 再次设置 left = 0right = nums.length - 1 = 5
  2. 第一次循环:
    • 计算 midmid = left + (right - left + 1) / 2 = 0 + (5 - 0 + 1) / 2 = 3
    • 比较 nums[mid] 与 target:
      • nums[mid] = nums[3] = 8 小于等于 target = 8
    • 更新 leftleft = mid = 3
  3. 第二次循环:
    • 更新后的 left = 3right = 5
    • 计算 midmid = left + (right - left + 1) / 2 = 3 + (5 - 3 + 1) / 2 = 4
    • 比较 nums[mid] 与 target:
      • nums[mid] = nums[4] = 8 小于等于 target = 8
    • 更新 leftleft = mid = 4
  4. 第三次循环:
    • 更新后的 left = 4right = 5
    • 计算 midmid = left + (right - left + 1) / 2 = 4 + (5 - 4 + 1) / 2 = 5
    • 比较 nums[mid] 与 target:
      • nums[mid] = nums[5] = 10 大于 target = 8
    • 更新 rightright = mid - 1 = 4
  5. 第四次循环:
    • 更新后的 left = 4right = 4
    • 因为 left < right 不再成立,循环结束

此时,left 的值即为右端点的索引值。

结果

最终,ret[0] 被设置为 3ret[1] 被设置为 4,表示目标值 8 在数组中的起始位置为 3,结束位置为 4。因此,函数返回 [3, 4]

🏣三.x的平方根

题目链接:69.x的平方根

算法原理

二分查找通常用于在有序数组中查找特定元素,但在这里被创造性地用来查找一个数值。算法通过不断地将搜索空间减半来缩小目标值的范围,直到找到满足条件的解。

查找平方根的适应

对于查找平方根问题,搜索空间是所有可能的整数,从 0x(包括 x)。算法初始化两个指针 leftright,分别指向搜索区间的两端。每次迭代中,算法计算中间点 mid,并检查 mid * mid 是否小于等于 x。基于比较结果,算法会更新搜索区间的一端,从而缩小搜索范围。

特殊处理

  • 初始条件:如果 x 小于 1,直接返回 0,因为 0 和 1 的平方根都是它们自身。
  • 计算中间值mid = left + (right - left + 1) / 2,这里加 1 是为了防止在某些情况下陷入死循环,确保在 left 和 right 相邻时能够正确地选择较大的那个作为结果。
  • 终止条件:循环直到 left 不再小于 right,这时 left 指向的就是满足条件的最大整数。

代码分析

  1. 边界条件处理:如果输入 x 小于 1,立即返回 0
  2. 初始化:设置 left = 0 和 right = x,定义搜索范围。
  3. 循环:只要 left 小于 right,就继续循环。
    • 计算 midmid = left + (right - left + 1) / 2,确保 mid 始终偏向右侧,避免死循环。
    • 检查 mid 的平方是否小于等于 x
      • 如果是,说明 mid 可能是解的一部分,更新 left = mid
      • 如果不是,说明 mid 太大,更新 right = mid - 1
  4. 返回结果:循环结束时,left 指向的就是满足条件的最大整数,将其转换为 int 类型并返回。

这种算法的时间复杂度为 O(log x),因为每次迭代都将搜索空间减半。它是一种高效且精确的方法来计算整数平方根。

代码

 public int mySqrt(int x) {
        //处理细节
        if(x<1){
            return 0;
        }
        long left=0,right=x;
        while(left<right){
            long mid=left+(right-left+1)/2;
            if(mid*mid<=x){//由于如果数据很大,mid*mid很容易溢出 所以用long类型
                left=mid;
            }else{
                right=mid-1;
            }
        }
        return (int)left;//为了符合要求,最后再进行强转
    }

举例 

测试用例 x = 8

使用测试用例 x = 8 来分析上述代码,我们可以追踪二分查找算法在寻找整数平方根时的具体步骤。

初始化

  • left = 0
  • right = x = 8

第一次循环

  • 计算 mid:
    • mid = left + (right - left + 1) / 2 = 0 + (8 - 0 + 1) / 2 = 4.5,但由于 mid 是长整型变量,实际取值为 5
  • 比较 mid * mid 与 x:
    • mid * mid = 5 * 5 = 25,大于 x = 8
  • 更新指针:
    • right = mid - 1 = 4

第二次循环

  • 计算 mid:
    • mid = left + (right - left + 1) / 2 = 0 + (4 - 0 + 1) / 2 = 2.5,实际取值为 3
  • 比较 mid * mid 与 x:
    • mid * mid = 3 * 3 = 9,大于 x = 8
  • 更新指针:
    • right = mid - 1 = 2

第三次循环

  • 计算 mid:
    • mid = left + (right - left + 1) / 2 = 0 + (2 - 0 + 1) / 2 = 1.5,实际取值为 2
  • 比较 mid * mid 与 x:
    • mid * mid = 2 * 2 = 4,小于等于 x = 8
  • 更新指针:
    • left = mid = 2

第四次循环

  • 此时,left = 2 和 right = 2,即 left 和 right 已经相等。
  • 计算 mid:
    • mid = left + (right - left + 1) / 2 = 2 + (2 - 2 + 1) / 2 = 2.5,实际取值为 3
  • 比较 mid * mid 与 x:
    • mid * mid = 3 * 3 = 9,大于 x = 8
  • 更新指针:
    • right = mid - 1 = 2,但实际上 right 的值不会改变,因为 right 和 left 已经相等。

结束循环

由于 leftright 相等,下一次循环条件 left < right 不再满足,循环结束。

返回结果

  • 最终 left 的值为 2,即 x = 8 的整数平方根最大值,满足 y * y <= x 的条件。
  • 函数返回 2 作为结果。

因此,对于输入 x = 8,代码返回的整数平方根是 2,这是正确的结果,因为 2 * 2 = 4 小于等于 8,而 3 * 3 = 9 大于 8

🏪四.搜索插入位置

题目链接:35.搜索插入位置

算法原理

标准二分查找

基本思路与标准二分查找类似,初始化两个指针 leftright,分别指向数组的起始位置和结束位置。在每一步中,算法计算中间位置 mid,并根据 nums[mid]target 的大小关系调整搜索范围,直到找到目标值或确定目标值应该插入的位置。

查找插入位置

对于查找插入位置问题,关键在于如何处理 nums[mid]target 相等的情况。在标准二分查找中,我们通常会在找到目标值后立即返回。但在查找插入位置的场景下,即使 nums[mid] 等于 target,我们也需要继续在左侧查找,以确定 target 的最左侧插入位置(即第一个等于 target 的位置,或第一个大于 target 的位置之前的那个位置)。

边界情况处理

  • 目标值大于数组中所有元素:在进入循环前,代码首先检查如果 target 大于数组中的最大值 (nums[right]),则直接返回数组长度作为插入位置,这是数组末尾的下一个位置。
  • 循环终止条件:循环在 left 不小于 right 时结束,此时 left 指向的位置就是 target 应该插入的位置。

代码分析

  1. 边界条件处理:如果 target 大于数组中的最大值,直接返回数组长度作为插入位置。
  2. 初始化:设置 left = 0 和 right = nums.length - 1,定义搜索范围。
  3. 循环:只要 left < right,就继续循环。
    • 计算 midmid = left + (right - left) / 2,避免整数溢出。
    • 检查 nums[mid] 与 target 的大小关系:
      • 如果 nums[mid] 小于 target,说明 target 可能位于 mid 的右侧,更新 left = mid + 1
      • 如果 nums[mid] 不小于 target,说明 target 可能位于 mid 的左侧或等于 mid,更新 right = mid
  4. 返回结果:循环结束时,left 指向的就是 target 应该插入的位置,直接返回 left

这种算法的时间复杂度为 O(log n),其中 n 是数组的长度,因为它每次迭代都将搜索空间减半。这是一个非常高效的解决方案,尤其适用于大型数据集。

代码

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

举例 

测试用例 nums = [1,3,5,6], target = 5

初始化

  • left = 0
  • right = nums.length - 1 = 3

第一次循环

  • 计算 mid:
    • mid = left + (right - left) / 2 = 0 + (3 - 0) / 2 = 1.5,向下取整为 1
  • 比较 nums[mid] 与 target:
    • nums[mid] = nums[1] = 3,小于 target = 5
  • 更新指针:
    • left = mid + 1 = 2

第二次循环

  • 计算 mid:
    • mid = left + (right - left) / 2 = 2 + (3 - 2) / 2 = 2.5,向下取整为 2
  • 比较 nums[mid] 与 target:
    • nums[mid] = nums[2] = 5,等于 target = 5
  • 更新指针:
    • right = mid = 2

第三次循环

在第二次循环中,由于 nums[mid] 等于 target,算法将 right 设置为 mid。但在下一次循环开始时,leftright 的值相同,即 left = right = 2。这导致循环条件 left < right 不再满足,循环终止。

返回结果

  • 循环结束后,left 的值为 2,这是 target = 5 应该插入的位置,因为在 nums 数组中,5 正好位于第 2 个位置(基于 0 索引)。

总结

  对于输入 nums = [1, 3, 5, 6]target = 5,代码正确地返回了 2,这是 5 在数组中的位置,同时也是如果数组中不存在 5 时,5 应该被插入的位置以保持数组的升序状态。这个算法有效地利用了二分查找的特性,以对数时间复杂度 O(log n) 确定了目标值的正确位置。


🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀

以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐

  制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值