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

【算法笔记】二分查找 红蓝染色法

目录

  • 二分查找 红蓝染色法(感谢灵神)
    • 闭区间[left, right]
    • 左闭右开区间[left, right)
    • 开区间(left, right)
    • 变式

二分查找 红蓝染色法(感谢灵神)

这里是灵神的教学视频:二分查找 红蓝染色法_哔哩哔哩_ bilibili

学了二分查找之后其实一直不是很理解,直到昨天看了灵神的讲解,有了全新的理解,有一种直冲天灵盖的感觉,写个笔记整理一下,感谢灵神

对于一个有序数组,我们如果想查找其中的某一个元素,我们可以通过从头遍历其中的所有元素,如果有匹配的元素,则返回元素的值,但是这种暴力解法不但没有利用有序数组这个特点,而且时间复杂度是O(n),不是很有效率。

这个时候我们可以采用二分查找的方式:每次排除一半的元素,这样我们每查找一个元素的时候,他的时间复杂度就是O(logn),要比暴力解法好很多。

二分查找中我们会涉及到一些变量,以及他们在每一轮循环中的变化情况:左右游标left、right,中间游标mid,以及我们要找的值target,其中左右游标用于在每次循环中缩小我们的查找区间确定区间外数值的大小,mid游标用于与target值进行比较,辅助左右游标的具体移动情况。

闭区间[left, right]

下面我们介绍在闭区间[left, right]内查找的情况:

  1. 我们将left游标初始化为数组的起始下标0right初始化为数组末尾的下标arr.length-1,mid指向左右游标的中间值left+(right - left)/2,这样我们每次就可以排除一半的数据,是最优解。

    left+(right - left)/2这里为什么这样写,是因为防止在Java中直接进行left+right导致整型数字太大导致越界的情况

  2. 我们将 < t a r g e t < target <target的元素标记为红色, ≥ t a r g e t \geq target target的元素标记为蓝色,但是我们要清楚,区间内的元素我们是不知道它们的颜色的,这点很重要!!假定我们现在要查找的数组为:[5,7,7,8,8,10],target值为8,游标的位置如下图:

    在这里插入图片描述

  3. 经过比较之后,我们发现mid指向的值为7,满足 < t a r g e t < target <target的条件,那么我们将left更新为mid+1,原有的元素变为如下的颜色:

    在这里插入图片描述

    这里为什么不更新为mid??

    这也是我一直不太清楚的一个点,现在我来试着说一下:因为我们选择的方式是在闭区间[left, right]内查找,所以我们要保证被比较过的元素在区间外面,没有比较过的元素在区间内部,而mid是我们比较的最后一个元素,那么我们只需要将游标更新为mid+1或者是mid-1就可以保证区间内的元素都是未经比较的,并且区间外的元素都是已经确定颜色的

  4. 继续进行比较,mid指向的值为8满足 ≥ t a r g e t \geq target target的条件,此时我们不确定的区间也就是[left, mid-1],所以我们更新右游标到mid-1的位置,保证区间外的元素都已经确定过:

    在这里插入图片描述

  5. 继续进行比较,mid指向8,满足 ≥ t a r g e t \geq target target条件,再次更新右游标到mid-1的位置:

    在这里插入图片描述

  6. 到这个时候我们发现:整个数组已经全部染色,也就意味着我们确定了所有元素和目标元素的大小关系,我们要清楚一个关键点:right+1指向的元素一定是蓝色( ≥ t a r g e t \geq target target),left-1指向的元素一定是红色( < t a r g e t <target <target,每次循环的过程中这个是不会变的,为了方便记忆灵神给这个关键点取名叫:循环不变量,我们清楚这一点之后,再去看那些循环条件或者是游标移动的位置,就很容易判断应该怎么写了。那么我们这里要找的元素8的位置也就找到了,直接返回下标right+1,或者是left就可以

我们看一下代码部分:

    public static int lower_bound(int[] nums, int target){
        int left = 0;
        int right = nums.length - 1;
        while (left <= right){ // 闭区间内查找,当left > right的时候区间为空 
            int mid = left + (right -left) / 2;
            if (nums[mid] < target){
                left = mid + 1; // [mid + 1, right]是不确定的
            } else {
                right = mid - 1; // [left, mid - 1]是不确定的
            }
        }
        return left; // left所指向的位置就是元素出现的初始位置
    }

左闭右开区间[left, right)

左闭右开区间和闭区间的情况相似,我们需要修改右游标的初始位置arr.length,因为右游标不再包括在区间内,也就是说再循环中[right, arr.length-1]的元素是确定好的;还需要修改的地方有右游标更新的位置,因为是开区间,所以直接移动到mid所在的位置就可以;最后是循环的终止条件,当left == right的时候区间为空,循环停止。下面是游标的初始位置:

在这里插入图片描述

那么我们开始循环:

  1. mid所指向的值为8,满足条件 ≥ t a r g e t \geq target target,我们将右游标移动至mid位置,并更新mid

    在这里插入图片描述

  2. mid所指向的值为7,满足条件 < t a r g e t < target <target,我们将左游标移动到mid+1的位置,并更新mid

    在这里插入图片描述

  3. mid所指向的值为7,满足条件 < t a r g e t < target <target,我们将左游标移动到mid+1的位置,并更新mid

    在这里插入图片描述

  4. 此时left == right,所有元素也都知道了大小,停止循环,并返回right。

代码部分

    public static int lower_bound(int[] nums, int target){
        int left = 0;
        int right = nums.length;
        while (left < right){ // 左闭右开区间内查找[left, right)
            int mid = left + (right -left) / 2;
            if (nums[mid] < target){
                left = mid + 1; // [mid + 1, right)是不确定的
            } else {
                right = mid; // [left, mid)是不确定的
            }
        }
        return right; // right所指向的位置就是元素出现的初始位置
    }

开区间(left, right)

原理差不多,就不一一画图了,需要修改:

  1. 左游标的初始位置-1,因为是开区间上面已经解释过,我们需要保证区间内的元素是没有被确定的,而区间外的元素已经都被确定完毕
  2. 右游标的初始位置arr.length
  3. 左游标更新的位置mid
  4. 右游标更新的位置mid
  5. 循环终止的条件left + 1 == right,此时区间内已经没有元素

代码部分

    public static int lower_bound(int[] nums, int target){
        int left = -1;
        int right = nums.length;
        while (left + 1 < right){ // 开区间内查找(left, right)
            int mid = left + (right -left) / 2;
            if (nums[mid] < target){
                left = mid; // (mid, right)是不确定的
            } else {
                right = mid; // (left, mid)是不确定的
            }
            System.out.println(left + " " + right);
        }
        return right; // right所指向的位置就是元素出现的初始位置
    }

变式

通过上面最简单的查找元素的初始位置,我们可以转换为更多的变式:

  1. 找到元素的初始位置

    这就是查找到元素初始位置的情况,如果有mid指向的元素比target大,那么就不断缩小右区间

  2. 找到元素的结束位置

    这种情况我们可以转化为:找到比target大1的元素的初始位置,也就是 m i d ≥ t a r g e t + 1 mid \geq target+1 midtarget+1

  3. m i d ≤ t a r g e t mid \le target midtarget

    这种情况可以转化为找到 > t a r g e t >target >target的元素位置,然后-1

  4. m i d < t a r g e t mid < target mid<target

    这种情况可以转换为找到 ≥ t a r g e t \geq target target的元素的位置,然后-1


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

相关文章:

  • 如何进行产线高阶能耗数据的计算和可视化?
  • EXCEL延迟退休公式
  • 机器学习总结
  • 代码修改材质参数
  • 代码随想录第二十一天| 669. 修剪二叉搜索树 108.将有序数组转换为二叉搜索树 538.把二叉搜索树转换为累加树
  • C++《继承》
  • 前端——表格、列表标签
  • 【设计模式】创建型模式(三):单例模式
  • Rocky Linux 9安装mysqlclient库报错的解决方法
  • Sam Altman最新博文:智能时代将带来无限的智能和丰富的能源
  • LOGO设计新革命:5款AI工具让你秒变设计大师(必藏)
  • 16_Python的迭代器
  • 【Unity链接数据库01】Unity使用Oracle 数据库完成登录注册功能
  • Qt/C++ TCP调试助手V1.1 新增图像传输与接收功能(附发布版下载链接)
  • 每日算法1(快慢指针)
  • 实例讲解电动汽车故障分级处理策略及Simulink建模方法
  • 面试官:谈谈自己对IOC和AOP的理解? Part1
  • Unity 设计模式 之 结构型模式 -【适配器模式】【桥接模式】 【组合模式】
  • 前端读取PDF和DOCX文件(干货分享)
  • 基于深度学习的可再生能源的效率优化
  • thinkphp 做分布式服务+读写分离+分库分表+负载均衡(分区)(后续接着写)
  • 《线性代数》学渣笔记
  • ai论文写作指导有哪些?六款最火ai论文生成平台大推荐
  • AI 智能名片链动 2+1 模式商城小程序中的体验策略
  • 某文书网爬虫逆向
  • Flask建立的Web网站的can‘t open file C_Program问题的分析