目录
介绍
思路
循环实现
详解
递归实现1
详解
注意
递归实现2
两个递归代码之间的区别
总结
介绍
二分查找法,也称为折半查找法,是一种在有序数组中查找特定元素的高效算法。其基本思路是将目标元素与数组中间的元素进行比较,从而可以确定目标元素可能在数组的哪一半,然后逐步缩小搜索范围,直到找到目标元素或确定其不存在为止。
思路
-
确定搜索范围: 首先,确定整个有序数组的搜索范围,即左边界和右边界。通常初始时左边界为数组的第一个元素索引,右边界为数组的最后一个元素索引。
-
计算中间元素: 计算左边界和右边界的中间索引,可以使用
(left + right) / 2
进行计算。这个中间元素将用于与目标元素进行比较。 -
比较与目标元素: 将目标元素与中间元素进行比较。如果目标元素等于中间元素,则找到了目标,返回中间元素的索引。如果目标元素小于中间元素,则说明目标可能在左半边,更新右边界为中间元素的前一个索引。如果目标元素大于中间元素,则说明目标可能在右半边,更新左边界为中间元素的后一个索引。
-
缩小搜索范围: 根据上一步的比较结果,缩小搜索范围。如果目标在左半边,就在左半边继续进行二分查找;如果目标在右半边,就在右半边继续进行二分查找。重复这个过程,不断缩小搜索范围,直到找到目标元素或搜索范围为空。
-
重复步骤: 重复执行步骤 2 到步骤 4,直到找到目标元素或搜索范围为空。如果搜索范围为空,说明目标元素不存在于数组中。
二分查找法的关键之处在于每一步都将搜索范围减半,因此它的时间复杂度为 O(log n),其中 n 是数组中元素的数量。相较于线性搜索的 O(n) 时间复杂度,二分查找法在大型有序数组中能够显著提高搜索效率。
然而,二分查找法有一定的前提条件,即数组必须是有序的。如果数组无序,需要先进行排序操作,这会增加额外的时间复杂度。另外,在特定情况下,二分查找法可能不如其他算法高效,例如对于小规模数据或者频繁插入/删除元素的数据结构。
循环实现
from typing import List
class Solution:
def search(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums)-1
while left <= right:
# 计算出中位索引
mid = (right-left)//2 + left
num = nums[mid]
if num == target:
return mid
elif num > target:
right = mid - 1
else:
left = mid + 1
return -1
详解
-
首先,我们通过
left
和right
两个指针来表示当前搜索范围的左右边界。初始时,left
指向数组的第一个元素索引,right
指向数组的最后一个元素索引。 -
在
while
循环中,我们首先计算出当前搜索范围的中间索引mid
。这是通过(right - left) // 2 + left
计算得出的。这个中间索引对应的元素num
就是我们要和目标元素进行比较的值。 -
接下来,我们将
num
与目标元素target
进行比较。有三种情况:- 如果
num
等于target
,则说明我们已经找到目标元素,可以返回mid
。 - 如果
num
大于target
,说明目标元素可能在当前中间元素的左边,所以我们将right
更新为mid - 1
,缩小搜索范围到左半部分。 - 如果
num
小于target
,说明目标元素可能在当前中间元素的右边,所以我们将left
更新为mid + 1
,缩小搜索范围到右半部分。
- 如果
-
循环会继续执行,不断更新
left
和right
,直到left
大于right
,即搜索范围为空,或者直到找到目标元素为止。 -
如果循环结束时仍未找到目标元素,那么我们返回
-1
,表示目标元素不存在于数组中。
递归实现1
def search(nums, target: int) -> int:
left = 0
right = len(nums)-1
if nums !=[]:
mid = (left + right)//2
if target > nums[mid]:
return search(nums[mid+1:],target)
elif target < nums[mid]:
return search(nums[:mid],target)
else:
return 1
else:
return -1
详解
-
首先,我们通过
left
和right
两个指针来表示当前搜索范围的左右边界。初始时,left
指向数组的第一个元素索引,right
指向数组的最后一个元素索引。 -
然后,通过判断
nums
是否为空数组来决定是否进行递归。如果nums
不为空,我们进入递归的判断过程。 -
在递归判断中,我们首先计算出当前搜索范围的中间索引
mid
,通过(left + right) // 2
计算得出。注意,这里没有加上left
,因为我们将对子数组进行递归,所以mid
是相对于子数组的索引。 -
接下来,我们将
nums[mid]
与目标元素target
进行比较。有三种情况:- 如果
target
大于nums[mid]
,则说明目标元素可能在当前中间元素的右边,所以我们对nums[mid+1:]
进行递归查找,返回递归结果。 - 如果
target
小于nums[mid]
,则说明目标元素可能在当前中间元素的左边,所以我们对nums[:mid]
进行递归查找,返回递归结果。 - 如果
target
等于nums[mid]
,则说明我们已经找到目标元素,返回1
。
- 如果
-
如果数组为空(即
nums
为空),那么直接返回-1
,表示目标元素不存在于数组中。
注意
递归的结束条件是
nums
为空,或者在递归过程中找到目标元素。这种递归实现在思想上与循环实现类似,不过它将搜索过程拆分为递归的子问题,每次通过截取子数组来缩小搜索范围
递归实现2
def search2(nums, target, left, right):
if left <= right:
mid = (left + right) // 2
if target > nums[mid]:
left = mid + 1
return search2(nums, target, left, right)
elif target < nums[mid]:
right = mid - 1
return search2(nums, target, left, right)
else:
return mid
else:
return -1
两个递归代码之间的区别
-
函数签名:
- 第一个代码片段中,函数
search
只接受nums
和target
作为参数,并在函数内部初始化left
和right
。 - 第二个代码片段中,函数
search2
接受nums
、target
、left
和right
四个参数,这些参数直接影响递归调用时的搜索范围。
- 第一个代码片段中,函数
-
返回值:
- 第一个代码片段在找到目标元素时返回了固定的值
1
,而应该返回mid
作为目标元素在数组中的索引。 - 第二个代码片段在找到目标元素时返回了
mid
,正确地表示了目标元素在数组中的索引。
- 第一个代码片段在找到目标元素时返回了固定的值
-
递归调用:
- 第一个代码片段在递归调用时通过切片来截取子数组,这会产生额外的空间和时间开销。
- 第二个代码片段通过在递归调用时更新
left
和right
来缩小搜索范围,避免了切片操作,从而提高了效率。
总结
第二个递归代码更接近标准的二分查找递归实现,正确返回目标元素的索引,而且在递归调用时通过更新参数来实现搜索范围的调整,避免了切片操作,从而更加高效。