一、你真的了解二分法吗?
二分搜索是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
举个栗子
- 给定一个排好序(升序)的列表与待查找的关键字,成功则返回其索引,失败则返回-1
def search(list, key):
left = 0 # 左边界
right = len(list) - 1 # 右边界
while left <= right:
mid = (left + right) // 2 # 取得中间索引
if key > list[mid]:
left = mid + 1
elif key < list[mid]:
right = mid - 1
else:
return mid
else:
return -1
list = [2, 5, 13, 21, 26, 33, 37]
print(search(list, 5))
代码如上,在查找某个数时,将查找的条件分为三种,每种不同的讨论即可得到最终的查找结果。但是二分法真的这么简单么?答案并不是,如果很好的理解二分法仍然需要掌握如下的一些知识,下面我来将对二分法进行剖析。
二、二分查找需要注意的地方
我们平时在网上看各位大佬的解题思路的时候,不同的大佬在写二分法的时候,题解各不同,如下我罗列了一些大佬的解法:
首先是对while条件的判断可分为如下三种:
1.while left <= right:
2.while left < right:
3.while left + 1 < right:
这些都是出于从不同的角度来理解二分法,然后会使用不同的判断条件来进行约束,甚至跳出循环以后继续打补丁,来确保查找正确。
如下是我标准的二分之查找的模板,当然标注的地方是需要我们在写二分法的时候要注意的:
def search(list, key):
left = 0
right = ... #1.注意
while (...): #2.注意
mid = (left + right) // 2
if key > list[mid]:
left = ... #3.注意
elif key < list[mid]:
right = ... #4.注意
else:
return ... #5.注意
else:
return ... #6.注意
1.while left <= right:
当使用这个判断条件的时候,查找区间就会在[left, right] 注意左右都是闭区间。这个时候在我们定义初始值left和right的时候要特别留心,初始化应该是left =0,而right = len(list)-1,即最后一个元素的索引,而不是len(list) 。不然会发生越界,导致bug的出现。
对于该条件的终止条件,在循环内如果找到了目标值,直接返回索引即可。当在循环内找不到目标值的时候,退出循环的条件是,left在右,right在左,即left+1=right。这个时候要查找的区间为空,即退出循环的时候不需要在判断边界条件,直接返回-1即可。
2.while left < right:
当使用这个判断条件的时候,查找区间就会在[left, right) 注意左区间是闭,右区间是开。初始化应该是left =0,而right = len(list)
对于该条件的终止条件,left == right,在此处循环体内的条件是没有参与判断结束时指针所指的值和目标值的关系,所以如果直接返回-1,则会报错,所以需要在循环结束的时候对该代码打个补丁。
// target 比所有数都大
if (left == nums.length)
return -1;
if nums[left] == target:
return left
else:
return -1
3. 条件的书写
在我们平时遇见的代码中有的对于条件语句的书写是left =mid+1
,而有的则是right =mid
亦或者left =mid
对左右边界的书写可以说是琳琅满目,那么为什么有这么多写法呢,其实都是由于边界问题的判断问题带来的,所以他们都是针对不同的判断语句会得到不同的书写方法,最后得到的答案当然是没问题的,有问题的则是我们,琳琅满目的写法,很容易让人搞混。
这个很好解释,因为我们的「搜索区间」是 [left, right)
左闭右开,所以当 nums[mid]
被检测之后,下一步的搜索区间应该去掉 mid
分割成两个区间,即 [left, mid)
或 [mid + 1, right)
4.对于边界的寻找
对于最经典的二分法稍作修改就能具有寻找边界的功能,
如下是寻找左边界的二分法:
def search(list, key):
left = 0 # 左边界
right = len(list) # 右边界
while left < right:
mid = (left + right) // 2 # 取得中间索引
if key > list[mid]:
left = mid +1
elif key < list[mid]:
right = mid
else:
right = mid
else:
return left
相比于经典二分,在条件语句中找到目标值的话直接return,而找边界的话则是让rigtht=mid。不断的缩小右边界,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid)
中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
最后函数的返回值是left或者是right,因为当while left < right
的时候,退出判断条件right和left重合。
对于右边界的寻找
def search(list, key):
left = 0 # 左边界
right = len(list) # 右边界
while left < right:
mid = (left + right) // 2 # 取得中间索引
if key > list[mid]:
left = mid +1
elif key < list[mid]:
right = mid
else:
left = mid + 1
else:
return left-1
当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的下界 left,在区间 [mid+1, right)
中继续搜索使得区间不断向右收缩,达到锁定右侧边界的目的。
当判断条件结束时,返回的是left-1。当找到key = list[mid]时候,left =mid +1 也就是向右偏移了一位,来查找右边界,当目标值只有一个时,这个时候查找到如果也向右偏移一位,那么就会找不到该目标值,而导致程序错误。
总结:
第一个,最基本的二分查找算法:
因为我们初始化 right = nums.length - 1
所以决定了我们的「搜索区间」是 [left, right]
所以决定了 while (left <= right)
同时也决定了 left = mid+1 和 right = mid-1
因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回
第二个,寻找左侧边界的二分查找:
因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid
因为我们需找到 target 的最左侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧右侧边界以锁定左侧边界
第三个,寻找右侧边界的二分查找:
因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid
因为我们需找到 target 的最右侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧左侧边界以锁定右侧边界
又因为收紧左侧边界时必须 left = mid + 1
所以最后无论返回 left 还是 right,必须减一