并不简单的二分查找
文章同步发表于,知乎专栏和掘金社区
1、什么是二分查找
二分查找是一种非常有效的查找方式,我们日常生活中也经常用到。简单来说就是在有序的集合中查找目标值。注意这里有个前提的条件就是有序。
下面是二分查找中常见的一些术语
目标 Target —— 你要查找的值
索引 Index —— 你要查找的当前位置
左、右指示符 Left,Right —— 我们用来维持查找空间的指标
中间指示符 Mid —— 我们用来应用条件来确定我们应该向左查找还是向右查找的索引
from leetcode
2、复杂度分析
- 时间复杂度
由于数折半查找,每次的数据范围都会缩小一般,假设数据规模为n,执行过程就是
n 、 n / 2 、 n / 4 、 n / 8... n / 2 k n、n/2、n/4、n/8 ... n/{2^k} n、n/2、n/4、n/8...n/2k
其中k是循环次数
可以得出 k = l o g 2 n k=log_2{n} k=log2n
所以时间复杂度为 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 <= 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索引一定要在有效的区间上进行移动。
这样我们理解了指针的移动,遇到其他的问题,也能很好的使用二分查找
- 第一次出现的位置
题目的具体信息可以查看上述链接,这里的要求是查找出第一次出现的位置
给定一个排序的整数数组(升序)和一个要查找的整数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、https://www.lintcode.com/
2、https://leetcode-cn.com/explore/learn/card/binary-search/
3、https://github.com/supinyu/LintCode_python