详解二分查找

Posted1 by 呼延十 on March 17, 2019 Hot:

前言

最近也在进行一些面试嘛,也见识到了很多各种各样的题目,其中就有一些和二分查找相关的.

二分查找,在有序的数组中快速找到目标值.

这个算法在上学的时候学过,之后就没有看过了,因为比较”简单”嘛~.

然而在面试过程中,我在二分查找及类似题目上栽了三次…

所以今天做一个总结.

注意:下文的代码中没有进行参数校验,实际使用时需要进行参数校验

普通写一个二分查找

class Solution:
    def binary_search(self,arr,target):
        low = 0
        high = len(arr) - 1
        while low <= high:
            mid = (low + high) // 2
            if arr[mid] == target:
                return mid
            elif arr[mid] > target:
                high = mid - 1
            else:
                low = mid + 1
        return


if __name__ == "__main__":
    c = Solution()
    print(c.binary_search([1,2,3,4,5,6],4))

数组中有重复值的问题

我大部分都载在这里了,,,,

当数组中有重复值的时候,返回该值第一个出现或者最后一个出现的下标.

比如[1,2,2,2,2,2,3],如果返回第一个出现的地方,应该返回1,如果返回最后一个,应该返回5.

首先我们可以想到,可以通过二分法拿到某一个目标值,然后向左或者向右遍历找到边界.

这个算法的时间复杂度是O(n),在最坏情况下,即整个数组的值相同时,算法时间消耗为n/2,还是O(n)是不太令人满意的.

那么有没有更好的办法呢?有的,还是二分法,二分法在找到目标值时,进行了返回,我们可以不让其返回,继续查找下一个即可.

代码实现如下:

查找最左目标值

    def binary_search2(self ,arr , target):
        low = 0
        high = len(arr) - 1
        while low < high:
            mid = (low + high) // 2
            if arr[mid] >= target:
                high = mid
            else:
                low = mid + 1
        if arr[high] == target:
            return high
        return -1 

在这个实现中,当找到某个目标值时,按照原二分法中的大于处理,因为我们想找到的是最左目标值.

注意,这个方法和上面的二分法有几处差异.

首先当找到的值大于等于目标值时,high=mid,因为此时的值可能就是最左目标值,因此需要将当前值继续保留在下一次查找的区间内.

而当小于的时候,那么肯定不是最终目标,采用low = mid + 1来处理,同时这一步可以保证不会出现死循环.(如果low= mid,在[0,1]中查找1的时候会陷入死循环.low=0,high=1,mid=0.arr[mid]<target,low=mid=0,经过一次循环,low和high无变化,陷入死循环.).

稍加修改可以实现寻找最右的值.

    def binary_search3(self ,arr , target):
        low = 0
        high = len(arr) - 1
        while low < high:
            mid = (low + high) // 2 + 1
            if arr[mid] <= target:
                low = mid
            else:
                high = mid - 1
        if arr[high] == target:
            return high
        return -1   

注意在第五行,mid = ( low + high )// 2 + 1这里和上面不一样,添加了+1,因为当在[0,1]寻找0时会陷入死循环.

合并一下,拿到一个方法.

上面两个其实实现的只是最左/最右的区别,因此可以合并一下,成为一个方法.

    # flag=0,获取最左目标,否则获取最后目标
    def binary_search4(self ,arr , target, flag):
        low = 0
        high = len(arr) - 1
        while low < high:
            mid = (low + high) // 2
            if arr[mid] == target:
                if flag == 0:
                    high = mid
                else :
                    low = mid
            elif arr[mid] < target:
                low = mid + 1
            else:
                high = mid - 1
        if arr[high] == target:
            return high
        return -1

其实我们可以发现,当想要获取最左最右的时候,区别对待的只是当前值=目标值这一个逻辑,因此我们将相等的逻辑单独写,并且根据flag的值进行不同的处理(分别任high或者low等于mid).而小于和大于的逻辑都是不变的,进行减一或者加一操作,将当前值从待查区间去掉.

总结

二叉查找大家都清楚,那么变种查最左或者最右,其实只需要将相等的逻辑考虑为大于/小于即可,之后在循环结束后比较当前的index的值是否是目标值就可.剩下的就是代码中一些小情况,尤其是死循环的处理也要注意一下.

其实不要觉得二分法查找太简单没有用,我原来也是这样想的…后来看了一些文章,才知道二叉树的应该之广泛,比如下面参考链接中的文章.

我遇到的面试题目呢,主要有两个,一个就是很直接让你查找目标值的最左或者最右,另一个就是需要你查出目标值的区间起始下标.

这个时候如果你能写出上面这个方法,调用两次即可返回结果,岂不是美滋滋.

参考链接

http://hedengcheng.com/?p=595

完。




ChangeLog

2019-03-17 完成

以上皆为个人所思所得,如有错误欢迎评论区指正。

欢迎转载,烦请署名并保留原文链接。

联系邮箱:huyanshi2580@gmail.com

更多学习笔记见个人博客——>呼延十