并不简单的二分查找

395 阅读5分钟

文章同步发表于,知乎专栏和CSDN

算法与数据结构

算法与数据结构

1、什么是二分查找

二分查找是一种非常有效的查找方式,我们日常生活中也经常用到。简单来说就是在有序的集合中查找目标值。注意这里有个前提的条件就是有序。 下面是二分查找中常见的一些术语

目标 Target —— 你要查找的值

索引 Index —— 你要查找的当前位置 左、右指示符 Left,Right —— 我们用来维持查找空间的指标

中间指示符 Mid —— 我们用来应用条件来确定我们应该向左查找还是向右查找的索引

from leetcode

2、复杂度分析

  • 时间复杂度 由于数折半查找,每次的数据范围都会缩小一般,假设数据规模为n,执行过程就是

n、n/2、n/4、n/8 ...  n/{2^k}

其中k是循环次数 可以得出k=log_2{n} 所以时间复杂度为 O(log{n}) ,这是平均时间复杂度,也是最坏情况下的时间复杂度,最好的情况下时间复杂度为 O(1)

  • 空间复杂度 由于在查找过程中只需要存储一个变量,所以空间复杂度为O(1)

3、习题讲解

  • 经典模版

对于二分查找而言,有几个常见的坑,在有些时候会进入死循环的情况,常见的难点有,最后的终止条件是什么,中间过程中的index到底怎么变化,下面介绍一种常见的二分法的模版

class Solution:
    """
    @param nums: An integer array sorted in ascending order
    @param target: An integer
    @return: An integer
    """
    def findPosition(self, nums, target):
        # write your code here
        if nums == None or len(nums) < 1: 
            return -1
        start = 0 
        end = len(nums) - 1
        while start + 1 < end:
            mid = start + (end - start)//2
            if nums[mid] == target:
                return mid
            elif nums[mid] > target:
                end = mid
            else:  
                start = mid 
        if nums[start]  == target:
            return start
        if nums[end] == target:
            return end 
        return -1

这里已python代码为例,我们发现上面的模版有几个特点

循环终止条件 start +1 < end ,那这个条件是什么意思呢,跟我们平常常见的二分法的条件好像不太一样,这里的终止条件的意思是,相邻即退出,同时start和end都在有效的区间范围内,并不会超出数组的范围

这种写法的好处在哪,能避免大部分死循环的出现

指针的变化 start = mid 或者 end = mid 因为我们的循环终止条件是相邻即退出,所以我们在变化指针的时候,直接使用mid进行赋值

边界验证 由于是相邻即退出,我们这里并没有对最后的边界情况进行验证,所以最后需要对边界情况进行验证

  • 第一次出现的位置

第一次出现的位置

题目的具体信息可以查看上述链接,这里的要求是查找出第一次出现的位置

给定一个排序的整数数组(升序)和一个要查找的整数target, 用O(logn)的时间查找到target第一次出现的下标(从0开始),如果target不存在于数组中,返回-1。

这里我们看跟上一题的差距,这道题中的数据有可能是重复的,而且是第一次出现的位置,那么我们要寻找的是,target第一次出现的那个位置边界,我们看一下题解

class Solution:
    """
    @param nums: The integer array.
    @param target: Target to find.
    @return: The first position of target. Position starts from 0.
    """
    def binarySearch(self, nums, target):
        # write your code here
        if nums == None: 
            return -1
        start = 0 
        end = len(nums) - 1
        while start + 1 < end:
            mid = start + (end - start)//2
            if nums[mid] < target:
                start = mid
            else:  
                end = mid 
        if nums[start]  == target:
            return start
        if nums[end] == target:
            return end 
        return -1

我们到题解中,如果 nums[mid] < target 此时还没有到达target的位置,所以index往右边移动,而对于其他情况index一律往左边移动,这就对应题目中的我们要找的第一次出现的位置

  • 建庙

建庙

这道题目与Lintcode上的木头加工题目一样

你是一名建造寺庙的建筑师。 寺庙的柱子是由木头制成。每根柱子必须是一节完整的木头而且不能是被连接得到的。 给出n段具有不同长度的木头。你的寺庙有m根高度严格相同的柱子。那么你寺庙最大高度是多少。 (m根柱子的高度)

首先,这里我们看对哪个数据进行二分,二分应用的条件之一就是有序,那么上述的条件中那个变量是有序的呢? 答案就是最终柱子的高度 柱子高度的范围是1,2,3... max(len),我们要在这个范围内进行二分查找,找到满足条件的长度

class Solution:
    """
    @param m: m pillars of your temple.
    @param woods: length of n different wood
    @return: return the maximum height of the temple.
    """
    def buildTemple(self, k, L):
        # write your code here
        if k <= 0 or L == None or len(L) == 0:
            return 0
        end = max(L)
        start = 1 
        while start + 1 < end:
            mid = start + (end - start)//2
            if self.cut(L ,mid) >= k:
                start = mid
            else:
                end = mid
        if self.cut(L, end) >= k:
            return end
        elif self.cut(L, start) >= k:
            return start
        return 0
    
    def cut(self, L , cut_len):
        num = 0
        for item in L:
            num = num + item // cut_len
        return num

最后的的时间复杂度为 O(nlogLen) ,其中Len为n段柱子中最大的长度

4、使用二分查找注意的点

  • O(logn) 我们一定要对这个时间复杂度比较敏感,因为出现这个时间复杂度的时候,往往需要优先考虑二分查找
  • 二分法的终极思想 在循环中通过判断,不断的缩小范围,直到最后可以进行判断,所以我们要挖掘题目中隐含的有序的变量,比如最后一题中柱子的长度

好了今天的内容就这些,下次我们说一说关于树的相关知识

5、参考资料

1、www.lintcode.com/

2、leetcode-cn.com/explore/lea…

3、github.com/supinyu/Lin…