递归(recursion)
- 递归需要满足的三个条件:
1. 一个问题的解可以分解为几个子问题的解
2. 这个问题与分解得到的子问题,除了数据规模不同,求解思路完全一样
3. 存在递归终止条件 - 递归代码的编写:
1. 找到将大问题分解为小问题的规律,基于此写出递推公式
2. 推敲终止条件
3. 将两者结合,翻译为代码 - 两个注意点:
1. 警惕堆栈溢出:通过限制递归调用的最大深度来解决
2. 警惕重复计算:通过使用一个散列表来保存已经求解过的值 - 复杂度分析:
- 时间:当递归代码中调用的函数数量较大时,会积累成一个可观的时间成本
- 空间:递归调用一次就会在内存栈保存一次现场数据,需要额外考虑这部分的开销
- 递归与非递归
- 优点:递归代码的表达力很强,写起来非常简洁
- 不足:空间复杂度高,有堆栈溢出的风险,存在重复计算,过多的函数调用比较耗时
二分查找
二分查找针对有序的数列集合,查找思想类似于分治思想,每次都和区间的中间元素对比,将要查找的区间缩小为原来的一半,直到找到要查找的元素,或者区间缩小为0。
时间复杂度:O(logn)(堆和二叉树的操作时间复杂度也是O(logn))
二分查找的递归实现
def binary_search(nums, n, value):
"""递归实现在没有重复元素的数组中查找给定值,找到则返回元素下标,否则返回-1"""
return _binary_search_v2(nums, 0, n-1, value)
def _binary_search(nums, low, high, value):
"""二分查找实现函数,在指定区间查找给定元素"""
if low > high:
return -1
mid = low + ((high - low) >> 1)
if nums[mid] == value:
return mid
elif nums[mid] < value:
return _binary_search(nums, mid+1, high, value)
else:
return _binary_search(nums, low, mid - 1, value)
二分查找的非递归实现
def binary_search_v1(nums, n, value):
"""循环实现在没有重复元素的数组中查找给定值,找到则返回元素下标,否则返回-1
:param nums: 待查找的列表
:param n: 列表元素个数
:param value: 要查找的元素
:return:
"""
low, high = 0, n-1
while low <= high:
# mid = int((high+low)//2) # high+low可能溢出,将除法运算转换为移位运算
mid = low + ((high - low) >> 1)
if nums[mid] == value:
return mid
elif nums[mid] < value:
low = mid + 1
else:
high = mid - 1
return -1
适用场景
- 数据使用顺序表结构存储,保证随机访问
- 数据必须是有序的。对于无频繁插入、删除操作的静态数据可以一次排序多次查找;对于动态变化的数据集合,不适用
- 数据量太小可以使用顺序遍历查找,数据量太大使用二分查找需要连续内存空间
相关的面试题
- 题目:1000万个整数数据,每个占8字节,设计数据结构和算法,快速判断某个整数是否出现在这1000万个数据中?
将1000万个8字节数据存储在数组中占用内存80MB左右,然后对1000万数据从小到大排序,再使用二分查找算法。
虽然大多数情况下,用二分查找可以解决的问题,用散列表、二叉树这些支持快速查找的动态数据结构都可以解决,但是散列表和二叉树都需要比较多的额外的内存空间,而二分查找底层依赖的是数组,除了数据本身之外,不需要额外存储其他信息,是最省内存空间的快速查找算法。 - 题目:二分法求一个数的平方根,精确到小数点后六位
def sqrt(num, n=6):
"""求一个数的平方根,精确到小数点后n位
先确定整数部分,再确定小数部分,小数部分每次确定一位,最后四舍五入
:param num:
:param n:
:return:
"""
# 保留小数点后n+1位
for i in range(0, n+2):
if i == 0:
low, high = 0, num
else:
low, high = round(res+0.0/(10**i),i), round(res+10.0/(10**i),i)
res= round(_sqrt(num, low, high, i),i)
# 对最后一位四舍五入
return round(res, n)
def _sqrt(num, low, high, i):
"""对[low, high]范围内的i位数二分查找确定num的平方根
:param num: 目标数
:param low: 二分查找下界
:param high: 二分查找上界
:param i: 查找位数,为0时表示整数部分
:return:
"""
while low <= high:
if i == 0:
mid = low + ((high - low) >> 1)
else:
mid = round(low + ((high - low)/2), i)
if mid ** 2 == num:
return mid
elif mid**2 < num and (mid+1/(10**i))**2 > num:
return mid
elif mid**2 < num:
low = round(mid+1/(10**i),i)
elif mid**2 >num and (mid-1/(10**i))**2 < num:
return mid-1/(10**i)
else:
high = round(mid-1/(10**i),i)
- 题目:二分查找使用链表存储的数据的时间复杂度
假设链表长度为n,第一次指针移动n/2次,第二次移动n/4次,最多移动logn次,指针移动次数(查找次数)最多为O(n),和遍历查找相同
二分查找的变形问题
- 查找第一个值等于给定值的元素
def binary_search(nums, n, value):
"""
在包含重复元素的数组中查找第一个值等于给定值的元素,没有则返回-1
:param nums:
:param value:
:return:
"""
low, high = 0, n-1
while low < high:
mid = low + ((high - low) >> 1)
if nums[mid] >= value:
high = mid
else:
low = mid + 1
if nums[low] == value:
return low
else:
return -1
- 查找最后一个值等于给定值的元素
def binary_search(nums, n, value):
"""
在包含重复元素的数组中查找最后一个值等于给定值的元素,没有则返回-1
:param nums:
:param value:
:return:
"""
low, high = 0, n-1
while low < high:
mid = low + ((high - low + 1) >> 1) # 此处加1是为了当low和high相邻时mid取到high
if nums[mid] <= value:
low = mid
else:
high = mid - 1
if nums[low] == value:
return low
else:
return -1
- 查找第一个大于等于给定值的元素
def binary_search(nums, n, value):
"""
在包含重复元素的数组中查找第一个值大于等于给定值的元素
:param nums:
:param value:
:return:
"""
low, high = 0, n-1
while low < high:
mid = low + ((high - low) >> 1)
if nums[mid] >= value:
high = mid
else:
low = mid + 1
return low
- 查找最后一个小于等于给定值的元素
def binary_search(nums, n, value):
"""
在包含重复元素的数组中查找最后一个值小于等于给定值的元素
:param nums:
:param value:
:return:
"""
low, high = 0, n-1
while low < high:
mid = low + ((high - low + 1) >> 1) # 此处加1是为了当low和high相邻时mid取到high
if nums[mid] <= value:
low = mid
else:
high = mid - 1
return low