算法——二分查找实例详解

介绍

简介

条件:数组有序
作用:查找数组中的某个值
算法描述: 搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

基本模版

int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;

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

其中的'...'是一些细节的地方,可以根据之后题目的要求,进行求解。

其中需要注意的是,标记为1的地方,也就是求解mid值的代码,当然也可以写成int mid = (left + right) / 2;但是为了防止left + right的值过大导致的内存溢出,一般将其写为int mid = left + (right - left) / 2的形式。

基本的二分搜索

二分查找我们能想到的就是用来搜索一个值,如果存在,返回其索引,否则返回 -1。

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int 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 if (nums[mid] > target)
            right = mid - 1; // 注意
    }
    return -1;
}

下面简单介绍其中涉及到的一些细节问题:

  1. while中为什么是<=而不是<

总结来说,就是考虑区间[left ,right]还是区间[left, right),详细说明如下。

注意到其中的right取值为nums.length - 1,那么为了能够将数组中的值全部访问到,right必然需要能够被访问到,即当前的搜索区间在[left, right]之间。

那么停止搜索有两种情况,一种是找到了target值,直接返回mid,还有一种就是不满足while循环条件了,也可以理解为当前的搜索区间不存在了。考虑当left == right的时候,此时区间可以写成为[left, left],区间里面还是有值的,能够进入while循环,之后就有三种情况了,一种直接返回,一种是left = left + 1, 还有一种是right = right - 1(因为之前是left == right == mid),不过不论是哪一种,都存在left + 1 == right,也就是说最终的终止状态应该是left + 1 == right。或者可以简单的通过while(left <= right)分析出终止条件。

为了更明显的进行对比,可以考虑while(left < right)的终止条件,也就是当left == right的时候,就停止。考虑left == right的情况,此时区间为[left, left],但是此时的是无法进入循环的,所以区间形式写成[left, left)更为合适。所以此时,是少考虑一种情况的,那么此时可以尝试right = nums.length会正常运行。

更多细节方面内容可以参考博客
二分查找细节详解
二分查找的坑点与总结

实例

01-寻找旋转排序数组中的最小值

力扣153

题目描述

已知一个长度为 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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的最小元素 。

示例1

输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例2

输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

题解01 参考
考虑左侧比较

class Solution {
    public int findMin(int[] nums) {
        if(nums == null || nums.length == 0){
            return -1;
        }

        int l = 0, r = nums.length - 1;
        while(l < r){
            if(nums[l] < nums[r]){
                return nums[l];
            }
            int m =l + (r - l) / 2;
             if(nums[l] > nums[m]){
                r = m;
            }else{
                l = m + 1;
            }
        }
        return nums[l];
    }
}

题解02 参考
考虑右侧比较

class Solution {
    public int findMin(int[] nums) {
        int l = 0, h = nums.length - 1;
        while(l < h){
            int m = l + (h - l) / 2;
            if(nums[h] > nums[m]){
                h = m;
            }else if(nums[h] < nums[m]){
                l = m + 1;
            }
        }
        return nums[l];
    }
}

总结

二分查找的应用条件是有序序列,然而当前序列并非是有序的,首先想到的就是序列重排,或者以O(n)的时间复杂度线性查找,但是显然这道题的目的并非如此。

然后,通过看题解,发现用的是二分查找,然后各种不明白全都涌上来了【菜鸡的悲哀】

  1. 二分查找用到的是有序序列,为什么当前的序列只是局部有序也可以适用呢?
  2. 二分查找通常有一个target,用来比较判断哪个区间该缩小,可是该题的target没有啊
  3. 二分查找通常返回的是具体的某个查找对象,最小值在其中又是如何理解的呢?

在本题中,数组的旋转是可以看成两个有序序列的拼接,那么在搜索的过程中,可能出现的情况是前面有序或者后面有序,例如二分查找搜索局部有序数组中所描述的例子,

假如:
 nums = [4,5,6,7,0,1,2],则为情况1;
 nums = [6,7,0,1,2,4,5],则为情况2;
image.png

此外,本题还可以理解为求解两个有序数组的拼结点,因为最小值一定是这个拼结点所在的地方,如果没有拼结点的话,那么就说明这是一个完整的有序数组。

因为其比较可以选择左端或者右端的值,所以分开进行讨论一下。
首先,比较nums[left]和nums[mid]的情况。

在比较之前,可以先行对整个区间进行判断,如果当前区间存在nums[left] < nums[right]的情况,那么说明收敛的区间当前有序,那么直接返回收敛的最小值nums[left]即可。而这一步在与左侧进行比较的时候是必须的,因为只有在此基础上,才可以谈当nums[left] == nums[mid]的情况。

  1. 情况一
    假设nums[mid] < nums[left],说明最小值应该在[left, mid]之间,也可以说明拼结点就在其中,此时右侧的[mid,right]一定是有序的。此外,考虑到nums[mid]可能为最小值的情况,所以right 被赋值为mid。

  2. 情况二
    假设nums[mid] > nums[left], 如果当前是拼接,说明最小值(mid, right]之间,可以看出此时[left, mid]中间是连续的,所以拼结点必在后一段。那么此时,nums[mid]肯定不是最小值了,所以可以越过mid,将left赋值为mid + 1。

  3. 情况三
    假设nums[mid] == nums[left] ,虽然说题目有说不包含重复数字,这种情况通常是不需要考虑的,但是如果真正运行的时候是通过不了的,例如{2,1}这种情况,mid在开始的时候与left的值相同,都是0,而且在之后的循环中,是不满足上面的任何一种情况的,也就是说l和r的值都是无法更改的,那么就会进入到死循环的状态了。
    综上,从左端考虑的时候,因为mid求值会偏向左方,所以考虑mid与left相等的情况,首先在考虑完情况二的第一种情况后,也就是保证了该序列一定不是完整的有序数组的情况下,可以肯定拼结点在后面,所以此时的left赋值mid + 1。

此外,除了上述的情况讨论,还有二分查找特有的细节难点还需要仔细考虑一下,如:
1. while循环是否需要添加等号?

可以看出while的结束条件是left == right。按照之前的分析,此时缺少了一种情况,也就是当left == right == mid的时候,如果假设此时存在这种情况,考虑数组[1],此时进入状态三,也就是left = mid + 1,之后就跳出循环,返回nums[left],但是此时left == 1,超出数组大小,报错。

考虑当left == right的情况下,也就是将搜索区间已经缩小到了left和right之间,如果继续的话,left会超出区间范围,即left == right的时候已经确定了一个最小值,所以此时不用再往下继续。与之前不同,也是因为题目的目的不同,该题的最终目标是求最小值,除了在中途查找到目标值之外,就是当区间收敛到只有一个值的时候,该值就是目标值,而不用像查找具体值那样,还要看最后left == right的值是否就是最终的target。

2. 返回的值为什么是nums[left]?

因为最终的终止条件是left == right,所以返回nums[right]也是一样的

下面就是从端点的右侧进行比较,同样从一下几种情况进行考虑:

  1. 情况一
    假设nums[mid] < nums[right],说明后一段连续递增,拼结点应该在[left, mid)之间,所以将right赋值为mid;

  2. 情况二
    假设nums[mid] > nums[right],说明前一段连续递增,拼结点应该在(mid, right]之间,所以将left赋值为mid + 1;

  3. 情况三
    假设nums[mid] == nums[right],说明只可能在left == right == mid的情况下出现,但是在此时,这么比较是没有意义的。所以不考虑这种情况。

{\color{red}{综上,可以得出一下几点结论:} }

二分查找一般的比较原则有:

  • 如果有目标值target,那么直接让arr[mid] 和 target 比较即可。
  • 如果没有目标值,一般可以考虑 端点

二分查找适合的场景:

  • 数组有序和局部有序

局部有序也可以使用二分查找的原因,个人猜想可能在于其中求解的目的是不同的,此时并不是去找到一个具体的target,而是利用二分查找去查找例如最值这类的值,主要的是利用二分查找能够缩小查找区间的特性。

02-寻找旋转排序数组中的最小值2

剑值offer——牛客网
154. 寻找旋转排序数组中的最小值 II

本题和上题的区别在于非递减排序的数组,所以可能会有重复数据的情况,比如

  • [2, 1, 2, 2, 3, 3, 3],最小值在[left, mid]中间
  • [2, 2, 2, 2, 3, 1, 2], 最小值在[mid, right]中间
    所以此时并不能够定位到拼结点是在哪一块,那么此时最好的方法就是将right值减一,防止错过最小值的区间。
class Solution {
    public int findMin(int[] nums) {
        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[left] < nums[right]){
                return nums[left];
            }
            if(nums[right] < nums[mid]){
                left = mid + 1;
            }else if(nums[right] > nums[mid]){
                right = mid;
            }else if(nums[right] == nums[mid]){
                right = right - 1;
            }
        }
        return nums[left];
    }
}

这边的nums[left] < nums[right]并非是必须的,只是能够加快判断速度的作用。因为在相等的情况下具有不确定性。

最后,本文如果有什么思考错误的地方,欢迎指出!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容