problem k: 查找某一个数_并不简单的二分查找

6efe4d819a2021de00ee1d3d1e01aef0.png

1、什么是二分查找

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

下面是二分查找中常见的一些术语

目标 Target —— 你要查找的值
索引 Index —— 你要查找的当前位置
左、右指示符 Left,Right —— 我们用来维持查找空间的指标
中间指示符 Mid —— 我们用来应用条件来确定我们应该向左查找还是向右查找的索引
from leetcode

2、复杂度分析

  • 时间复杂度

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

n、n/2、n/4、n/8...

其中k是循环次数

可以得出

所以时间复杂度为

,这是平均时间复杂度,也是最坏情况下的时间复杂度,最好的情况下时间复杂度为
  • 空间复杂度

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

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 <= end:
            mid = start + (end - start) // 2
            if nums[mid] == target:
                return mid
            if nums[mid] < target:
                start = mid + 1
            else:
                end = mid - 1 
        return -1

特点

1、循环终止时的索引大小是 right + 1 == left,循环条件是start <= end,这里说明一下为什么可以是等于符号,因为左右索引初始化的时候,索引start 和 end的取值范围是在[0, len(num) - 1] 的闭区间进行的,也就是说,无论mid怎么变化,都是在有效范围之内进行查找,所以当start == end,也属于有效区间

2、同时无需对数组的长度进行提取判断

3、无需进行后处理的相关操作

模板二

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)
        if end == 1 and nums[start] == target:
            return start
        while start < end:
            mid = start + (end - start) // 2
            if nums[mid] == target:
                return mid
            if nums[mid] < target:
                start = mid + 1
            else:
                end = mid

        return -1 

特点

1、循环的终止条件是 left == right,循环条件是start < end

2、需要对数组今天提前判断,如果数组中只存在一个数,需要单独判断

3、索引的start 和 end的取值范围是在[0, len(num) - 1),是属于左闭右开区间,所以在对end赋值的时候没有进行减一的操作,原因是此时,索引范围在[0, mid - 1] 这个区间,但是因为判断条件是start < end,所以end赋值的时候是end = mid,此时查找的范围是[0, mid ) 这个左闭右开区间

模板

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

特点:

1、循环的终止条件是start +1 == end,这个条件的含义是相邻即退出,同时保证所有的mid都在有效的范围内

2、数组里面至少有两个以上的值

3、当相邻的时候或者数组中有两个以下值的时候需要进行后处理操作,由于循环终止条件是相邻即退出,所以我们在变化指针的时候,直接使用mid进行赋值

好了,市面上大部分的二分查找的方法基本上都已经分析完了,这里我们看这三种方法,如果光靠背,肯定是行不通,我们要找到其中的共性,那就是索引mid的范围,以及初始的左右指针的范围,总之的核心就是,我们的mid索引一定要在有效的区间上进行移动

这样我们理解了指针的移动,遇到其他的问题,也能很好的使用二分查找

  • 第一次出现的位置
LintCode 领扣​www.lintcode.com

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

给定一个排序的整数数组(升序)和一个要查找的整数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 领扣​www.lintcode.com

这道题目与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

最后的的时间复杂度为

,其中Len为n段柱子中最大的长度

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

我们一定要对这个时间复杂度比较敏感,因为出现这个时间复杂度的时候,往往需要优先考虑二分查找

  • 二分法的终极思想

在循环中通过判断,不断的缩小范围,直到最后可以进行判断,所以我们要挖掘题目中隐含的有序的变量,比如最后一题中柱子的长度

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

5、参考资料

1、https://www.lintcode.com/

2、https://leetcode-cn.com/explore/learn/card/binary-search/

3、https://github.com/supinyu/LintCode_python

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值