2.递归与算法分析
目录
递归习题
-
递归设计经验
找重复(子问题)
找重复中的变化量→参数
找参数变化趋势→设计出口 -
练习策略
循环改递归
经典递归
大量练习,总结规律,掌握套路
找到感觉,挑战高难度
1.求n的阶乘
'''
*f1(n): 求n的阶乘 -->f1(n - 1)
求n - 1
的阶乘
*找重复:n * (n - 1)
的阶乘, 求n - 1
的阶乘是原问题的重复(规模更小)——子问题
*找变化:变化的量应该作为参数
*找边界:出口 '''
def f1(n):
if ( n == 1):
return 1
else:
return n * f1(n - 1)
print(f1(5))
2.打印i到j
'''
* 打印i到j
* 找重复:
* 找变化:变化的量应该作为参数
* 找边界:i大于j
'''
def f2(i,j):
if ( i>j):
return
else:
print(i)
f2(i+1,j)
f2(1,5)
3.对数组元素求和
def f(list1,begin):
if (len(list1)-1==begin):
return list1[begin]
else:
return list1[begin]+f(list1,begin+1)
list1=[1,2,3,4,5,6,7]
print(f(list1,0))
4.翻转字符串
def f(str1,end):
if (end==0):
return ''+str1[end]
else:
return str1[end]+f(str1,end-1)
str1='abcd'
print(f(str1,3))
5.斐波拉数列
# 斐波那契第n项
def fib(n):
if(n==1 or n==2):
return 1
else:
return fib(n-1)+fib(n-2)
print(fib(5))
6.辗转相除求最大公因数
def gcd(m,n):
if(n==0):
return m
else:
return gcd(n,m%n)
print(gcd(4,2))
7.递归形式插入排序
对数组0~倒数第一个排序等价于:
对数组的0~倒数第二个元素,这部分排序
然后把是后一个元素插入到这个有序的部分中
def insertSort(arr, k) :
if (k == 0) :
return;
# 对前k-1个元素排序
insertSort(arr, k - 1);
# 把位置k的元素插入到前面的部分
x = arr[k];
index = k - 1;
while (index > -1 and x < arr[index]) :
arr[index + 1] = arr[index]
index-=1
arr[index + 1] = x
8.汉诺塔
1-N从A移动到B,C作为辅助
等价于:
1、1~N-1从A移动到C,B为辅助
2、把N从A移动到B
3、1-N-1从C移动到B,A为辅助
'''
* 将N个盘子从source移动到target的路径的打印
* N 初始的N个从小到达的盘子,N是最大编号
* source 原始柱子
* target 辅助的柱子
* help 目标柱子
'''
def printHanoiTower( N, source, target, help) :
if (N == 1):
print("move " + str(N) + " from " + source + " to " + target)
else :
printHanoiTower(N - 1, source, help, target) # 先把前N-1个盘子挪到辅助空间上去
print("move " + str(N) + " from " + source + " to " + target) # N可以顺利到达target
printHanoiTower(N - 1, help, target, source) # 让N-1从辅助空间回到源空间上去
printHanoiTower(2,'A','B','C') # 从1-N从A移动到B,C为辅助
9.二分查找递归解法
全范围内二分查找
等价于三个子问题:
左边找(递归)
中间比
右边找(递归)
注意:左查找和右查找只选其一
def binarySearch(arr, low, high, key): #(排序后的数组,最小索引,最大索引,查找值),返回索引值
if low > high:
return -1
mid = low + ((high - low) >> 1)
midVal = arr[mid]
if midVal < key: # 中间值小于查找值,查找右边
return binarySearch(arr, mid + 1, high, key)
elif midVal > key: # 中间值大于查找值,查找左边
return binarySearch(arr, low, mid - 1, key)
else:
return mid
10.希尔排序
排序:冒泡 选择 插入 希尔
交换 找最大最小 挪动数组 插入排序的更改版本
思路:
- 如序列 9 8 7 6 5 4 3 2 1
- 确定一个增量序列,(length(arr)/2)4-2-1,从大到小使用增量
- 使用第一个增量4,将序列划分为若干个子序列,下标组合为0-4-8,1-5,2-6,3-7
- 依次对子序列使用直接插入排序法
- 使用第二个增量2,将序列划分为若干个子序列(0-2-4-6-8),(1-3-5-7)
- 依次对子序列使用直接插入排序法:
- 使用第三个增量1,这时子序列就是元序列(0-1-2-3-4-5-6-7-8),使用直接插入法完成排序。
在 for 循环中,由于每组的第一个元素不用进行插入排序,而它们的下标处于 0~step-1,所以从下标 step 开始遍历。
import random
def shell(nums):
n = len(nums) # 获得长度
step = n // 2 # 增量序列 (length(arr)/2) 4-2-1
while step > 0:
for i in range(step, n): # 遍历需要进行插入排序的数
while i >= step and nums[i - step] > nums[i]: # 对每组进行插入排序
nums[i - step], nums[i] = nums[i], nums[i - step] # 互换位置
i -= step
step //= 2
return nums
arr=[random.randint(0, 100) for _ in range(8)]
print(arr)
print(shell(arr))
算法分析
1.评估算法复杂度
O表示法,忽略低阶项,忽略常数因子。
- 的弱上界是,因此增长速度非常快,这意味着单位时间内可求解的问题很小,换言之,超慢
- 这样的指数函数增长非常快,这种算法可以认为超慢
- O(n2)和O(n3)增长很快,算法很慢,至少优化到nlgn,O(n2)的有冒泡排序,直接插入排序,选择排序
- nlgn可以认为是及格的算法吧,一般分治法可以缩小层数为lgn,而每层的复杂度一般为O(n),例如归并排序算法、快速排序算法
- O (n)叫做线性算法,这种算法比较优秀,或者问题本身比较简单,比如求连续求和最大子数组的线性解
- O(sqrt(n))当然比O(n)更快,不是没有,但这种很少
- 就是很优秀的算法了,比如二分查找法,但是这种算法往往对输入数据的格式是有要求的,二分查找要求输入数据有序
- 还有一种是常量,无论规模怎么扩大,都花固定时间,这是为数极少的效率最高的算法了,多数是数据很规则
2.2的幂表
3.递归算法复杂度
递归关系 | 结果 | 举例 |
---|---|---|
T(n)=T(n/2)+O(1) | T(n)=O(logn) | 二分查找,辗转相除最大公因数 |
T(n)=T(n-1)+O(1) | T(n)=O(n) | 线性查找 |
T(n)=2T(n/2)+O(1) | T(n)=O(n) | |
T(n)=2T(n/2)+O(n) | T(n)=O(nlogn) | 归并、快排 |
T(n)=2T(n/2)+O(nlogn) | T(n)=O(n(logn)^2) | |
T(n)=T(n-1)+O(n) | T(n)=O(n^2) | 选择排序、插入排序 |
T(n)=2T(n-1)+O(1) | T(n)=O(2^n) | 汉诺塔 |
T(n)=T(n-1)+T(n-2)+O(1) | T(n)=O(2^n) | 递归的斐波那契 |
4.排序算法的稳定性
- 稳定:如果a原本在b前面,而a=b ,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b ,排序之后a可能会出现在b的后面。
5.题1:小白上楼梯(递归设计)
小白正在上楼梯,楼梯有n阶台阶,小白一次可以上1阶,2阶或者3阶,实现一个方法,计算小白有多少种走完楼梯的方式。
提示:设n阶台阶的走法数为f(n)。如果只有1个台阶,走法有1种(一步上1个台阶),即f(1)=1;如果有2个台阶,走法有2种(一种是上1阶,再上1阶,另一种是一步上2阶),即f(2)=2;如果有3个台阶,走法有4种(一种每次1阶,共一种;另一种是2+1,共两种;第三种是3,共1种),即f(3)=4;
当有n个台阶(n>3)时,我们缩小问题规模,可以这样想:最后一步有三种情况,走1步(之前上了n-1个台阶,走法为f(n-1)种),走2步(之前上了n-2个台阶,走法为f(n-2)种),走3步,(之前上了n-1个台阶,走法为f(n-3)种,f(n)=f(n-1)+f(n-2)+f(n-3),n>3。
def up_stairs(n):
if(n==1):
return 1
if(n==2):
return 2
if(n==3):
return 4
else:
return up_stairs(n-1)+up_stairs(n-2)+up_stairs(n-3)
print(up_stairs(4))
6.题2 :旋转数组的最小数字(改造二分法)
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一 个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一 个旋转,该数组的最小值为1。
解题思路:利用二分法,最小值一定藏在无序的那一边
"""
3 4 /5 1 2 ——> 5 /1 2 ——> 5 1 ——> 1 只剩两个元素,最小值一定是右边元素
"""
def f(arr):
be = 0
end = len(arr) - 1
# 没有旋转直接返回第一个
if (arr[be] < arr[end]):
return arr[be]
while (be + 1 < end): # 只剩两个元素就退出循环返回右边元素
mid = be + ((end - be) >> 1)
if (arr[mid] >= arr[be]): # 左边有序,最小值在右边(无序)
be = mid
else:
end = mid
return arr[end] # 最后剩两个元素,右边的位最小值
arr=[4,5,6,2,3]
print(f(arr))
7.题3 :在有空字符串的有序字符串数组中查找
有个排序后的字符串数组,其中散布着一些空字符串,编写-一个方法,找出给定字符串(肯定不是空字符串)的索引。
-
begin(0) end(数组长度减一)
-
while(begin<end)
- 取中值
- 对于出现空串的处理(中值如果是空格就右移动)
- 比较改变begin或end
-
return -1(没找到)
def index(arr, p):
begin=0
end=len(arr)-1
while(begin<end):
# 取中值
mid=begin+((end-begin)>>1)
# 对于出现空串的处理
while(arr[mid]==''):
mid=mid+1
if(mid>end): # 防止死循环
return -1;
#比较改变begin或end
if(arr[mid]>p):
end=mid-1
elif(arr[mid]<p):
begin=mid+1
else:
return mid
return -1
arr = ["a", "", "ac", "", "ad", "b", "", "ba"]
res = index(arr, "a")
print(res)
8.题4 :最长连续递增子序列(部分有序)
(1,9,2,5,7,3,4,6,8,0)中最长的递增子序列为(3,4,6,8)。
def findLengthOfLCIS(nums):
if (len(nums) == 0):
return 0
Max = 0
count = 1
index = 0 # 记录最长连续字段的开始索引
for i in range (len(nums)-1):
if(nums[i]<nums[i+1]):
count=count+1
else:
Max=max(count,Max)
index=i+1-Max #获得索引
count=1
Max2 = max(count, Max) # 判断最后哪一个连续字段是否最长
if Max!=Max2: # 说明最长序列是最后的连续字段
index = index=len(nums)-Max2 # 得到索引
return Max2,index
nums = [1,3,2,4,7]
length,index = findLengthOfLCIS(nums)
print(length)
for i in range(length):
print(nums[index])
index=index+1
9.题5:设计一个高效的求a的n次幂的算法
def Pow(a, n):
if (n == 0):
return 1
res = a
ex = 1
# 能翻
while ((ex << 1) <= n):
res = res * res # 翻
ex <<= 1 # 指数翻倍
# 不能翻
# 差n-ex次方没有去乘到结果里面
return res * Pow(a, n - ex)
print(Pow(2,15))