基于有序数组的一种查找算法,主要使用了分治的思想,在每次查找的过程后,都能缩小一半的搜索范围,比如在1到100内猜数字,在保险的情况下先说50,根据结果再分析范围是1到49、51到100还是就是50,可以大大的减少查询次数。
步骤
例如–左闭右闭
def binary_search(nums: list[int], target: int):
# 确定搜索边界
left, right = 0, len(nums) - 1
# 在右边界大于等于左边界时一直搜
while left <= right:
# 寻找中间值,这种写法是为了防止越界(在python中很少)
mid = left + (right - left) // 2
# 当中间值小于目标值时,左边界挪到中间值右边一位
if nums[mid] < target:
left = mid + 1
# 当中间值大于目标值时,右边界挪到中间值左边一位
elif nums[mid] > target:
right = mid - 1
# 找到目标值,直接返回位置
else:
return mid
# 没找到就返回-1
return -1
这种为区间左闭右闭的情况,一般来说,因为python中有很多“包左不包右”的情况,所以左闭右开的区间也很常见,当这种情况发生时,判断条件中的left=right就不可能发生了,会导致找不到目标值或者返回错误的目标,所以我们做一下修改。
例如–左闭右开
def binary_search(nums: list[int], target: int):
# 设置边界为左闭右开
left, right = 0, len(nums)
# 左右边界不会相等,去除相等的条件
while left < right:
# 寻找中间值,这种写法是为了防止越界(在python中很少)
mid = left + (right - left) // 2
# 当中间值小于目标值时,左边界挪到中间值右边一位
if nums[mid] < target:
left = mid + 1
# 当中间值小于目标值时,因为左边界取不到,左边界挪到中间值位置
elif nums[mid] > target:
right = mid
# 找到后返回索引
else:
return mid
# 没找到返回-1
return -1
左开右闭也是同理,后续的区间范围要和开始的相同(闭或者开)。
上述章节中,我们发现在元素不存在时,我们直接返回了-1,那么如果元素不存在后,我们现在想插入这个元素,该怎么用二分查找查到元素的插入位置呢。
此时我们也要分两种情况,有重复元素和没有重复元素的情况,其中无重复元素情况相较简单我们先分析。
首先最重要的是,在选择插入时,我们要找的索引是什么,此时有两个情况:
def binary_search_insert(nums: list[int], target: int):
# 确认查找的左右边界
left, right = 0, len(nums) - 1
# 在右边界大于等于左边界时一直搜
while left <= right:
# 寻找中间值,这种写法是为了防止越界(在python中很少)
mid = left + (right - left) // 2
# 当中间值小于目标值时,左边界挪到中间值右边一位
if nums[mid] < target:
left = mid + 1
# 当中间值大于目标值时,右边界挪到中间值左边一位
elif nums[mid] > target:
right = mid - 1
# 找到了目标值
else:
return mid
# 没找到,此时左边的边界就是我们要找的插入点
return left
上述是无重复元素的情况,位置比较好找,就是首个大于当前元素的位置,但是当有重复元素的时候,没有办法去确认该元素有多少个,如果是直接插入的话,则找到就行,不过此时我们想要去插入到区间的最左边,此时我们需要找到首个小于target的位置。
def binary_search_insert(nums: list[int], target: int):
# 确认查找的左右边界
left, right = 0, len(nums) - 1
while left <= right:
# 寻找中间值,这种写法是为了防止越界(在python中很少)
mid = left + (right - left) // 2
# 当中间值小于目标值时,左边界挪到中间值右边一位
if nums[mid] < target:
left = mid + 1
# 当中间值大于目标值时,右边界挪到中间值左边一位
elif nums[mid] > target:
right = mid - 1
# 缩小右边界直到找到首个小于目标值的元素
else:
right = mid - 1
# 此时左边的边界就是我们要找的最左边
return left
与上文类似,当我们插入一个值的时候,可能会出现该列表中已经有多个类似的值了,此时我们想要知道重复元素的区间范围,可以复用上述插入的思想。
查找右边界
def binary_search_right(nums: list[int], target: int):
# 确认查找的左右边界
left, right = 0, len(nums) - 1
while left <= right:
# 寻找中间值,这种写法是为了防止越界(在python中很少)
mid = left + (right - left) // 2
# 当中间值小于目标值时,左边界挪到中间值右边一位
if nums[mid] < target + 1:
left = mid + 1
# 当中间值大于目标值时,右边界挪到中间值左边一位
elif nums[mid] > target + 1:
right = mid - 1
# 缩小右边界直到找到首个小于目标值的元素
else:
right = mid - 1
# 此时左边的边界就是我们要找的首个大于目标值的插入点
index = left
# 此时的左边界-1就是我们要找的目标值的右边界
res = left - 1
# 右边界不合法说明没有元素
if res == -1 or nums[res] != target:
return -1
# 返回右边界
return res
查找左边界
def binary_search_left(nums: list[int], target: int):
# 确认查找的左右边界
left, right = 0, len(nums) - 1
while left <= right:
# 寻找中间值,这种写法是为了防止越界(在python中很少)
mid = left + (right - left) // 2
# 当中间值小于目标值时,左边界挪到中间值右边一位
if nums[mid] < target:
left = mid + 1
# 当中间值大于目标值时,右边界挪到中间值左边一位
elif nums[mid] > target:
right = mid - 1
# 缩小右边界直到找到首个小于目标值的元素
else:
right = mid - 1
# 此时左边的边界就是我们要找的目标值左边界
index = left
# 左边界不合法说明没有元素
if index == len(nums) or nums[index] != target:
return -1
# 返回左边界
return index
二分查找的效率很高,时间复杂度为O(logn),而且该算法不需要额外的内存来做排序。
不过二分查找最大的弊端就是需要时有序的序列,而排序算法的复杂度通常都在O(nlogn)以上,反而比线性查找更慢;而且二分查找只能作用于数组,不适合用于链表当中。
该查找可以用力扣第一题来理解:https://leetcode.cn/problems/two-sum/description/
这题的常规思路肯定是直接两层遍历,枚举出符合条件的数字,但是时间复杂度为O(n²),当数据量很大时,时间会非常长,此时我们就可以用哈希查找去做优化。
众所周知,哈希是键值对相互对应的,此时我们建立一个哈希表,将值和索引存到 哈希表中,后续查找的时候就可以直接利用键值对的关系找到第二个值了。
def twoSum(nums: List[int], target: int) -> List[int]:
# 列表长度
lenth = len(nums)
# 建立字典
nums_dict = {}
# 遍历列表
for i in range(lenth):
# 获得另一个值的是多少
tmp = target - nums[i]
# 如果另一个值在字典中,直接返回索引
if tmp in nums_dict:
return [nums_dict[tmp], i]
# 如果没有就加新键值对
nums_dict[nums[i]] = i
# 没有合适的两个值
return []
时间复杂度降到O(n),相对的空间复杂度从O(1)上升到O(n)。这就是典型的空间换时间,