算法【二分查找 - BinarySearch】

本系列为 我的【我的学习历程】,实际上就是看了很多的博客,以及做Leetcode题目之后写出自己的理解,也算是我的学习笔记,出于自己的理解写的文章,如有错误可以帮我纠正,谢谢各位。包括我会在最下面贴出我看过的类似博客,以及我自己分析的LeetCode题目连接(如果有)



前言

二分查找,不难理解,实际上就是对一个集合不断地对半拆分,缩小范围直到找到指定值,就是一个 target。不过学习了之后,我发现对于二分查找的写法我总是容易搞混,比如说对于边界的判定,以及二分查找的很多变形等等,所以必须要真正理解了二分查找之后才不容易将对应的模板方法写混。


一. 简介

如果你也刷过Leetcode,那我相信你在做你的第一道 704.二分查找 的时候,一定看过这个评论。

这里是链接 👉 704.二分查找
在这里插入图片描述

保安后知后觉,这把亏大了!于是开始去抢别的图书馆的书了

在这里插入图片描述
(当然这是后话了)

实话说,当时看这个的时候感觉这也算是一种吐槽了,现在仔细想想,这其实算是一种案例!

证明二分法并不是什么时候都能够使用的。
小明借了一堆书,但是没有出借却被小明拿走的书可不止一本。
这个故事应该算是 二分查找的错误业务场景
通过这个故事,我们将小明接走的n本书看作一个集合,保卫需要找的未登记借出的书看作target,那么可以得出以下结论:

target不能为多个,只能是确定的一个值(保安不知道有多少未登记)

二分查找就像是一个猜数字大小的游戏,我们猜一个数字往往会从最小和最大的中间开始猜,如果大了就是对半切找左边,小了找右边,不断循环这一步骤直到猜到 ,小明借的书相当于未排序数组,如果靠二分法判断书在哪一堆里是没办法的(当然如果目标只有一个,按照保安这种做法倒确实可以找出未借出的书是哪一本?),所以第二个结论就是:

可以使用二分查找的集合,在逻辑上必须是有序的


二. 分析

2.1 变量名称

left :左边界下标
right:右边界下标
middle:中间值下标
target:需要查找的目标值


2.2 复杂度分析

时间复杂度
最好情况:O(1)(指 查第一遍就出来了)
平均情况:O(logn) (对半分后查出元素)
最差情况:O(logn) (再怎么差不都是对半分吗?)

空间复杂度
while算法:O(1)(所需空间为常数,不需要额外开辟空间)
递归算法: O(logn)(规模随着递归深度增大)


2.3 算法原理

以下为用我自己的话描述,可能会不严谨,请见谅

有一个有序集合,获取左边界和右边界
算出集合中间元素的下标
判断下标位元素是否为目标元素
不是的话判断大小
如果比目标元素大,说明目标元素在中间元素的左边,将集合对半切分取左半部分(右边界缩至middle - 1)
如果比目标元素小,说明目标元素在中间元素的右边,将集合对半切分取右半部分(左边界缩至middle + 1)
重复以上过程,直到找出目标元素,或左边界与右边界重合
如果重合说明不存在元素,返回一个代表元素不存在的标志(比如 -1)

二分查找与顺序查找的对比
(动态图来源)【007】 二分查找算法(Binary Search)


2.4 一般写法

while写法

左闭右闭方式
此为左闭右闭的写法 目标所在范围在 [left,right]

//这里是作为工具类写了,方便直接调用
public static int binary_search(int[] nums, int target) {
    int left = 0; // 左边界,默认为0,具体看业务情况
    int right = nums.length - 1; // 定义左闭右闭的区间
    while(left <= right){ // left = right 时 该区间有意义
        int middle = left + ((right - left)>>1);
        if(nums[middle] > target){ // 当中间值大于目标时
            right = middle - 1; // 说明目标在左半边,右边界重新赋值为middle - 1
        }else if(nums[middle] < target){ // 当中间值小于目标时
            left = middle + 1; // 说明目标在右半边,左边界重新赋值为middle + 1
        }else{ // 当中间值为目标值时,说明我们找到了目标值,返回目标值
            return middle;
        }
    }
    return -1; // 如果不存在目标值,返回 -1
}

注意点:

  1. int right = nums.length - 1 如果要用左闭右闭法,就会出现 left == right 的情况
    不进行 -1 操作,会出现越界异常 java.lang.ArrayIndexOutOfBoundsException
  2. int middle = left + ((right - left)>>1)
    这里的 >> 是位运算,二进制向右移动一位,等同于 除以2
    这么写因为如果middle按照正常写法,应该是 (left + right)/2 但是这样可能会出现int类型溢出(溢出的话就会变成负数,可以参考这篇 【int类型运算溢出】

左闭右开方式
此为左闭右开的写法 目标所在范围在 [left,right)

//这里是作为工具类写了,方便直接调用
public static int binary_search(int[] nums, int target) {
    int left = 0;
    int right = nums.length; // 定义左闭右开区间
    while(left < right){ // 当left = right时 该区间无意义
        int middle = left + ((right - left)>>1);
        if(nums[middle] > target){
            right = middle - 1;
        }else if(nums[middle] < target){
            left = middle + 1;
        }else{
            return middle;
        }
    }
    return -1; // 如果不存在目标值,返回 -1
}

递归写法

/**
 * 这里是作为工具类写了,方便直接调用
 * @param nums 有序数组
 * @param target 查找元素
 * @param left 左下标
 * @param right 右下标
 * @return
 */
public static int binary_search_recursion(int[] nums, int target,int left,int right){
    int middle = left + ((right - left) >> 1);
    // 可以先判断middle下标数字是否为查找元素,如果是的话也等同于 left = right
    // 右下标无所谓 -1
    if (nums[middle] == target){ return middle; }
   	// 因为有上方判断,所以如果此时left = right都没有找到我们需要查找的元素,那就说明不存在元素
    if (left >= right){return -1;}
    // 这就跟while部分一样了,比大小,切分,只不过用的是递归的形式
    if (nums[middle] > target){
        return binary_search_recursion(nums,target,left,middle - 1);
    }else{
        return binary_search_recursion(nums,target,middle + 1,right);
    }
}

实际业务当中,其实不推荐递归,开销不小,在数据量十分庞大的情况下,小 心 栈 溢 出


总结

二分法的效率不错,但是在实际业务当中想要使用的话,限制并不少,以及其算法的写法,模板方法一定要写正确。更加详细的总结可以看参考博客。

博客参考

  1. 【二分查找】详细图解
  2. 【int类型运算溢出】
  3. 【007】 二分查找算法(Binary Search)

我的LeetCode题目分析

暂未补充

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

heiqisang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值