你真的熟练掌握二分查找了吗?

二分查找由于思想简单,在经典算法中最容易被初学者忽视。
看懂了书上的一种写法后,就以为自己会了,而实际上是一看就会,一写就废。

不信的话,先来看一个问题:
找出升序数组中小于等于目标值的最大值?(数组中可能不包含目标值)

如果感觉很棘手,别着急,看完本文你能随手撸出两种不同的解法。如果你写出来了,也别着急,继续看下去,你会发现你写的不一定完美。好啦,不管怎样,看完绝对不亏,哈哈。

一、开门见山

不绕弯弯,针对问题:找出升序数组中小于等于目标值的最大值?
直接给出两种二分查找的典型写法,
第一种:

int binarySearch(int[] a, int target){
    int low = 0, high = a.length - 1;
    while(low <= high){
        int mid = (low + high) >>> 1;
        if(a[mid] < target) low = mid + 1;
        else if(a[mid] > target) high = mid - 1;
        else return mid;
    }
    return high; // 必须返回 high
}

第二种:

int binarySearch(int[] a, int target){
    int low = 0, high = a.length;
    while(low < high){ 
        int mid = (low + high + 1) >>> 1;
        if(a[mid] > target) high = mid - 1;
        else low = mid;
    }
    return low; // 返回 high 也可以
}

这两种写法最明显的区别是,在 while 循环里,第一种是三分支判断,第二种是两分支判断。

二分查找版,找不同游戏:

  1. high 的初始值,有 a.length 和 a.length-1 两种
  2. while 循环条件,有 low < high 和 low <= high 两种
  3. 取中位数,有 取左中位数 和 取右中位数 两种
  4. 取中位数的写法,以取左中位数为例,有 low+(high-low)/2、low+((high-low)>>1)、(low+high)>>>1(推荐这种写法)等。注意 low+high 可能溢出,最好不要写成 (low+high)/2
  5. low 和 high 的更新,以 low 为例,有 low = mid 和 low = mid+1 两种
  6. 判断分支的个数,有两分支和三分支两种。

可见二分查找的写法是多么灵活!

不过对于上面的找不同,有几点需要说明一下。
high 的初值设置和 while 循环条件有关,当循环条件为 low <= high 时,必须写 a.length-1,否则两种写法都可以。
另外,只有两分支写法需要取右中位数和边界更新为中位数(e.g. low = mid),后面会说到。
简单解释一下,为什么 (low+high)>>>1 不用担心溢出,溢出之所以出错,是因为有一部分数值进到了符号位,我们只要把符号位当成是数值位,结果就是正确的,而无符号右移,刚好就不会处理符号的问题。

二、第一种写法:三分支二分查找(推荐写法)

这种写法,除了返回值以外基本上都可以闭着眼睛写

int low = 0, high = a.length - 1;
while(low <= high){
    int mid = (low + high) >>> 1;
    if(a[mid] < target) low = mid + 1;
    else if(a[mid] > target) high = mid - 1;
    else return mid;
}

由于 while 循环条件是 low <= high,所以退出循环时,low != high,至于最终是返回 low 还是 high。记住下面这种图就 OK 了。


二分查找.png

这幅图的含义是,如果二分查找的目标值 target 不在数组中,那么最后退出循环时,high 指向数组中刚好小于 target 的值,low 指向数组中刚好大于 target 的值。

三、第二种写法:两分支二分查找

while 循环条件是 low < high,退出循环时,一定有 low == high,不用纠结返回 low 还是返回 high。
循环里只写两个分支,一个分支排除中位数,另一个分支不排除中位数,不再单独对中位数做判断。
根据“排除中位数的逻辑”,会有以下两种情况:

if(排除中位数的逻辑) low = mid + 1;
else high = mid;
if(排除中位数的逻辑) high = mid - 1;
else low = mid;

因为有一个边界更新为中位数,为了避免死循环,所以有 取左中位数 和 取右中位数 的区别。
例如下面的代码就有陷入死循环的风险:

mid = (low + high) >>> 1; // 选择左中位数
if(排除中位数的逻辑) high = mid - 1;
else low = mid;

当候选区间只剩两个元素时,此时求得的左中位数就是左边界,一旦进入左边界更新为中位数的逻辑分支,下一次循环,左边界没有变,求得的左中位数还是左边界,左边界还是不变,就会陷入死循环。
解决办法是,对于左边界更新为中位数的情况,我们取右中位数。对于右边界更新为中位数的情况,我们取左中位数。

四、具体问题

4.1 找出升序数组中小于等于目标值的最大值

前面已经给出代码,不再重复列出。

4.2 找出升序数组中大于等于目标值的最小值

第一种写法:

int binarySearch(int[] a, int target){
    int low = 0, high = a.length - 1;
    while(low <= high){
        int mid = (low + high) >>> 1;
        if(a[mid] < target) low = mid + 1;
        else if(a[mid] > target) high = mid - 1;
        else return mid;
    }
    return low;
}

第二种写法(小于目标值的肯定不要,作为排除中位数的逻辑):

int binarySearch(int[] a, int target){
    int low = 0, high = a.length;
    while(low < high){
        int mid = (low + high) >>> 1;
        if(a[mid] < target) low = mid + 1;
        else high = mid;
    }
    return low;
}
4.3 找出升序数组中小于目标值的最大值

第一种写法:

int binarySearch(int[] a, int target){
    int low = 0, high = a.length - 1;
    while(low <= high){
        int mid = (low + high) >>> 1;
        if(a[mid] < target) low = mid + 1;
        else if(a[mid] > target) high = mid - 1;
        else return mid - 1;
    }
    return high;
}

第二种写法(大于等于目标值的肯定不要,作为排除中位数的逻辑):

int binarySearch(int[] a, int target){
    int low = 0, high = a.length;
    while(low < high){
        int mid = (low + high + 1) >>> 1;
        if(a[mid] >= target) high = mid - 1;
        else low = mid;
    }
    return low;
}

4.4 找出升序数组中大于目标值的最小值

第一种写法:

int binarySearch(int[] a, int target){
    int low = 0, high = a.length - 1;
    while(low <= high){
        int mid = (low + high) >>> 1;
        if(a[mid] < target) low = mid + 1;
        else if(a[mid] > target) high = mid - 1;
        else return mid + 1;
    }
    return low;
}

第二种写法(小于等于目标值的肯定不要,作为排除中位数的逻辑):

int binarySearch(int[] a, int target){
    int low = 0, high = a.length;
    while(low < high){
        int mid = (low + high) >>> 1;
        if(a[mid] <= target) low = mid + 1;
        else high = mid;
    }
    return low;
}

五、最后

不是说第一种写法 while 循环条件一定要是 low <= high,只是 low <= high 更好。不是说第二种写法 while 循环条件一定要是 low < high,只是写 low < high 更好。
我还是推荐大家使用第一种写法,代码对称,写法固定,好记,只需要记住那张图就可以了。当然,第二种写法也不是白看的,当你看到别人用第二种写法时,能很快反应过来,他写的是什么,写的正不正确。

参考:leetcode 题解

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

推荐阅读更多精彩内容