当前位置: 首页 > article >正文

算法之二分查找优化:leetcode34:在排序数组中查找元素的第一个和最后一个位置

题干

在这里插入图片描述

分析

问题背景

给定一个已排序的数组,目标是找到一个给定的目标值 target 在数组中的 第一个位置最后一个位置。如果目标值不存在,返回 [-1, -1]。
由于题干要求的时间复杂度是 O(log n),并且数组是有序的,考虑使用二分法,接下来简单介绍二分法的思想:

二分查找是一种分治算法,它通过每次将搜索范围减少一半,快速查找目标值。在已排序的数组中,二分查找特别高效,时间复杂度是 O(log n)
如果你不了解二分法,推荐观看代码随想录的介绍视频
点击链接: 二分法

但是,这道题目要找的是 第一个位置 和 最后一个位置,因此我们不能只做一个普通的二分查找,而需要变种的二分查找来分别找出左边界(第一个位置)和 右边界(最后一个位置)。
找到一个符合条件(即nums[middle]==target)的时间复杂度为O(log n),分别找两个边界的时间复杂度约为2O(log n),依旧是O(log n)级别的。

分析题目

在普通的二分法查找中,我们通过left,right,middle指针,把数组逐渐拆分成两个部分,直至定位到符合条件(即nums[middle]==target)的下标。
但这是针对数组的的内容不重复的情况。
本题干中,数组的内容是有重复内容的,我们要算的结果,也是它的下标范围,因此当我们重新回看最经典的二分法代码:

class Solution {
    public int search(int[] nums, int target) {
        // 避免当 target 小于nums[0] nums[nums.length - 1]时多次循环运算
        if (target < nums[0] || target > nums[nums.length - 1]) {
            return -1;
        }
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left)/2;
            if (nums[mid] == target) {
                return mid;
            }
            else if (nums[mid] < target) {
                left = mid + 1;
            }
            else { // nums[mid] > target
                right = mid - 1;
            }
        }
        // 未找到目标值
        return -1;
    }
}

这是一个左闭右闭的区间,在这个区间里,每次用nums[mid]与target进行对比,再去缩小区间,在进行比较的时候,拆分成了3种情况
第一种nums[mid]==target,说明内容比对成功,是相等的,则直接返回。
第二种nums[mid]<target,说明target在middle的右边,因此要把范围缩减到右半部分,left=mid+1,继续进行循环与比较。
第三种nums[mid]>target,说明target在middle的左边,因此要把范围缩减到左半部分,right=mid-1,继续进行循环与比较。
直到不符合left<=right的条件,跳出循环。
若中间就比对成功,则直接返回下标,一直没比对成功,证明数组中没有目标值,返回-1。

而在本题中,情况略有些不同
当我们依旧使用二分法去查找目标值时,如果nums[mid]==target并不能直接返回下标,跳出循环。因为数组中可能有2个或者更多的值,我们要查找的是一个范围,而我们比对成功的那个下标可能只是这个范围中的其中一个,因此我们要从这个点继续出发,向左右两边扩展,找到它的左边界和它的右边界。

但如果我们直接使用while循环从匹配成功的nums[mid]出发,向左右寻找它的左右边界,便不符合O(log n)的时间复杂度了。
因此我们选择分别使用二分法寻找它的左右边界,而不是先定位到一点再向左右扩展。

代码思路

我们要考虑,题干具有哪些可能的情况。
情况一

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

这是正常情况,target=8在数组中有相等的值,返回下标范围3,4

情况二

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

数组中没有匹配的值,但6在数组的nums[0]nums[nums.length-1]之间

情况三

输入:nums = [5,7,7,8,8,10], target = 3 或target=12
输出:[-1,-1]

target在数组的范围之外

因此我们的代码,在最后进行结果的返回时,要考虑周全三种情况。
那么接下来,我们开始分别寻找leftBorderrightBorder

代码编写

leftBorder:寻找左边界

在经典二分法查找目标值时,我们分为了3种情况

  • nums[middle]>target
  • nums[middle]<target
  • nums[middle]==target

针对这三种情况,决定二分法选择左半区间还是右半区间还是直接返回。
而在本题目中,我们将进行改变,只分为2种情况

  • nums[middle]>=target
  • nums[middle]<target

之所以这样分,是因为我们在寻找左边界的时候,如果nums[middle]>target时,表示我们想要找的内容在左边部分,这毋庸置疑,因此我们要缩减区间的范围。而若nums[middle]==target时,我们也不能够停下,因为它的左边可能还有匹配值,我们依旧要继续缩减区间,查找是否在左侧还有匹配内容
nums[middle]<target,证明目标值在右边,所以改变left指针,继续在右边查找。
所以根据这个思路,我们稍稍改写经典的二分查找代码,得到代码如下:

        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
                right = middle - 1;
                leftBorder = right;
            } else {
                left = middle + 1;
            }
      }

在原本的二分法代码里,将原本的3种情况改为两种
nums[middle]>=target时,证明左边界一定在middle的左边,这时right=middle-1,然后将right赋值给leftBorder
之所以这样做,是因为当我们找到匹配值的时候,依旧要继续right指针改变,缩减区间,继续探索左边界。
这时候需要将匹配值左边的那个下标保存下来,如果它是左边界,当退出循环时,我们就得到了左边界。
若它不是做边界,例如左边还是target,那么它将继续二分,查找左边界,直到退出循环。

rightBorder:寻找右边界

寻找右边界的方法与寻找左边界的方法类似,代码如下:

        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] > target) {
                right = middle - 1;
            } else { // 寻找右边界,nums[middle] == target的时候更新left
                left = middle + 1;
                rightBorder = left;
            }
        }

这样,我们先定义leftBorder和rightBorder,并赋予他们一个初始值,若查找到疑似左/右边界的,他们将被赋值,若未被赋值,则证明情况为target在数组的范围之外:

例如nums = [5,7,7,8,8,10], target = 3
nums[middle]将永远大于target=3
在查找右边界的时候,rightBorder未被赋值,为初始值

例如nums = [5,7,7,8,8,10], target = 12
nums[middle]将永远小于target=12
在查找左边界的时候,leftBorder未被赋值,为初始值
可通过判断是否被赋值来判断是否target超出范围

情况分析及完整代码

综上,我们知道了如何处理变种的二分法。但所有的情况还没有考虑。
我们将leftBorderrightBorder赋予初始值-2
情况一

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

根据前面的代码,我们已经知道leftBorderrightBorder指向的是左/右范围的上一位/下一位
所以在这种情况下,leftBorder=2,rightBorder=5
由于若想查找到目标值,则需要至少一个目标值在数组之中,那么rightBorder减去leftBorder要大于1
因此条件为rightBorder-leftBorder>1

情况三

输入:nums = [5,7,7,8,8,10], target = 3 或target=12
输出:[-1,-1]

前面已经分析过,leftBorderrightBorder为初始值时候,代表超过范围

情况二

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

这两种情况以外的情况

由此我们的完整代码如下:

class Solution {
    int[] searchRange(int[] nums, int target) {
        int leftBorder = getLeftBorder(nums, target);
        int rightBorder = getRightBorder(nums, target);
        // 情况一
        if (leftBorder == -2 || rightBorder == -2) return new int[]{-1, -1};
        // 情况三
        if (rightBorder - leftBorder > 1) return new int[]{leftBorder + 1, rightBorder - 1};
        // 情况二
        return new int[]{-1, -1};
    }

    int getRightBorder(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] > target) {
                right = middle - 1;
            } else { // 寻找右边界,nums[middle] == target的时候更新left
                left = middle + 1;
                rightBorder = left;
            }
        }
        return rightBorder;
    }

    int getLeftBorder(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
                right = middle - 1;
                leftBorder = right;
            } else {
                left = middle + 1;
            }
        }
        return leftBorder;
    }

http://www.kler.cn/a/396864.html

相关文章:

  • c++ 类和对象(中)
  • AlphaFold3中文使用说明
  • Python Web 应用开发基础知识
  • 【环境配置】macOS配置jdk与maven
  • 使用python-Spark使用的场景案例具体代码分析
  • 游戏引擎学习第10天
  • 用 Python 从零开始创建神经网络(七):梯度下降(Gradient Descent)/导数(Derivatives)
  • 27-压力测试
  • ASFSSA-VMD多策略改进的麻雀搜索算法优化变分模态分解
  • JavaWeb之AJAX
  • 操作系统——虚拟存储器(含思维导图)
  • 使用pytest+openpyxl做接口自动化遇到的问题
  • 丹摩征文活动 |【前端开发】HTML+CSS+JavaScript前端三剑客的基础知识体系了解
  • rust并发
  • 6. Keepalived配置Nginx自动重启,实现7x24提供服务
  • 高效灵活的Django URL配置与反向URL实现方案
  • Git 分⽀规范 Git Flow 模型
  • 【新华妙笔-注册/登录安全分析报告-无验证方式导致安全隐患】
  • 如何在 Ubuntu 上配置 Kotlin 应用环境 ?
  • 计算机使用常用工具(持续更新)
  • Javascript 高级事件编程 - Axios fetch
  • 智能工厂的设计软件 为了监管控一体化的全能Supervisor 的监督学习 之 序5 架构for认知系统 总述 (架构全图)
  • AI 产品的四层架构:开启智能未来的密码
  • spark性能优化调优指导性文件
  • 管家婆财贸ERP BR033.请购单明细表采购周期预警
  • Mac终端字体高亮、提示插件