目录
2. 描述Fibonacci数列递归算法,并进行时间复杂度分析
3. 请设计算法,输入适当整数n,计算前n个数的全排列,对算法
4. 简要总结Karatsuba’s 大整数乘法、Strassen矩阵乘法的分治策略应用特点
4.1 Karatsuba’s 大整数乘法的分治策略应用特点
递归是一种解决问题的有效方法,在递归过程中,函数将自身作为子例程调用。递归的思想是把一个大型复杂问题层层转化为一个与原问题规模更小的问题,问题被拆解成子问题后,递归调用继续进行,直到子问题无需进一步递归就可以解决的地步为止。
1. 当问题和子问题具有递推关系,比如杨辉三角、计算阶乘。
2. 具有递归性质的数据结构,比如链表、树、图。
3. 反向性问题,比如取反。
分治法,字面意思是“分而治之”,就是把一个复杂的1问题分成两个或多个相同或相似的子问题,再把子问题分成更小的子问题直到最后子问题可以简单地直接求解,原问题的解即子问题的解的合并,这个思想是很多高效算法的基础,例如排序算法(快速排序,归并排序),傅里叶变换(快速傅里叶变换)等。
分治法的基本思想:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
分治策略:对于一个规模为n的问题,若该问题可以容易的解决(比如规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解决这些子问题,然后将各个子问题的解合并得到原问题的解。
递归是一种编程技巧,一种解决问题的思维方式;分治算法和动态规划很大程度上是递归思想基础上的。递归的基本思想是某个函数直接或者间接地调用自身,这样就把原问题的求解转换为许多性质相同但是规模更小的子问题。我们只需要关注如何把原问题划分成符合条件的子问题,而不需要去研究这个子问题是如何被解决的。分治法的基本思想:将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立且与原问题相同。递归地解这些问题,然后将各个子问题的解合并成原问题的解。
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……
在数学上,斐波纳契数列以如下递归的方法定义:
F(0)=0,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)。
- def Fibonnacci(n):
- if (n == 0):
- return 0
- elif (n == 1):
- return 1
- return Fibonnacci(n-1) + Fibonnacci(n-2)
-
- Fibonnacci(9)
第n层节点个数:2^n 个
前n层节点个数:1+2+4+……+2^n = (2 ^ n) - 1
推理:F(n) 有 n-2层
则F(n)一共有节点数 (2 ^ (n-2)) - 1 = ((2 ^n)/4)-1
计算时间复杂度规则:不看常数,不看系数,只看最高次数项
因此:O(F(n)) = O(2 ^n)
- def Fibonnacci(n):
- front = 0
- follow = 1
- result = 0
-
- if (n==0 or n==1):
- if(n==0):
- return front
- return follow
- while (n>=2):
- n = n-1
- result = front + follow
- front = follow
- follow = result
- return result
-
- Fibonnacci(9)
只关注while循环里面的次数,程序从n到2即为时间复杂度O(n-2) + O(2) = O(n)。
将查找一个全排列的过程看成是一棵N(N表示list的长度)叉树的深度优先遍历。当到达最大深度时候就往后退一步(相当于回溯一步)。
每次取一个元素之后,就可以进行递归。每个元素都可以当成树的根节点。
利用一个list来对数据进行存储。
- class FullPermutation(object):
- """
- 无重复元素的全排列
- """
- def get_full_resolution(self, store, temp_list, test_list):
- # 递归停止条件
- # temp_list用来存储一种完整的全排列
- # 如果长度已经和原始的输入test_list相等,则将这一种全排列存储下来
- # 继续找下一种全排列
- if len(temp_list) == len(test_list):
- store.append(temp_list[:])
- else:
- # 遍历原始输入中的元素,每次取出一个元素
- for i in test_list:
- if i in temp_list:
- continue
- # 如果当前遍历到的点不在temp_list中
- # 则添加到该列表中
- temp_list.append(i)
- # 添加完一个点之后,剩下的过程其实可以看成同样的过程
- self.get_full_resolution(store, temp_list, test_list)
- # 当找到一种全排列之后,就删掉一个点(往后退一步),继续判断其它的情况
- temp_list.pop()
-
- def backtrack(self, test_list):
- store = []
- temp_list = []
- self.get_full_resolution(store, temp_list, test_list)
- return store
-
-
- if __name__ == '__main__':
- test_list = [1, 2, 3, 4]
- c = FullPermutation()
- result = c.backtrack(test_list)
- for i in result:
- print(i)
- print('总共{}种全排列'.format(len(result)))
- [1, 2, 3, 4]
- [1, 2, 4, 3]
- [1, 3, 2, 4]
- [1, 3, 4, 2]
- [1, 4, 2, 3]
- [1, 4, 3, 2]
- [2, 1, 3, 4]
- [2, 1, 4, 3]
- [2, 3, 1, 4]
- [2, 3, 4, 1]
- [2, 4, 1, 3]
- [2, 4, 3, 1]
- [3, 1, 2, 4]
- [3, 1, 4, 2]
- [3, 2, 1, 4]
- [3, 2, 4, 1]
- [3, 4, 1, 2]
- [3, 4, 2, 1]
- [4, 1, 2, 3]
- [4, 1, 3, 2]
- [4, 2, 1, 3]
- [4, 2, 3, 1]
- [4, 3, 1, 2]
- [4, 3, 2, 1]
- 总共24种全排列
- # 遍历原始输入中的元素,每次取出一个元素
- for i in test_list:
- if i in temp_list:
- continue
- # 如果当前遍历到的点不在temp_list中
- # 则添加到该列表中
- temp_list.append(i)
- # 添加完一个点之后,剩下的过程其实可以看成同样的过程
- self.get_full_resolution(store, temp_list, test_list)
- # 当找到一种全排列之后,就删掉一个点(往后退一步),继续判断其它的情况
- temp_list.pop()
通过观察程序段核心代码for循环语句可知,只有一重循环,且test_list 的长度为n,故时间复杂度为O(n)。
Karatsuba乘法是一种快速乘法。用于数字比较大,相乘的结果超出了基本类型的表示范围,所以不能够直接做乘法运算的运算。此算法主要用于两个大数相乘。普通乘法的复杂度是n2,而Karatsuba算法的复杂度仅为3nlog3。
Strassen算法只有在对于维数比较大的矩阵 (N>300) ,性能上才有很大的优势,可以减少很多乘法计算。Strassen算法证明了矩阵乘法存在时间复杂度低于 Θ(N3) 的算法的存在。
我们都知道二分查找在最坏的情况下依次是n/2,n/4,n/8。。。。 一直到1为止。我们假设需要循环x次才能查找到目标数,根据观察二分查找满足如下等式:
n*(1/2)**x = 1
经过运算得:
2**x = n
左右两边同时取log以2为底的对数则有:
log2(n) = x
去掉对数函数的底数,则可以得到二分搜索算法的实际复杂度为:
O(log(n))
归并排序的核心思想其实很简单,如果要排序一个数组,我们先把数组从中间分成前后两部分,然后分别对前后两部分进行排序,再将排好序的两部分数据合并在一起就可以了。
归并排序使用的是分治思想,分治也即是分而治之,将一个大问题分解为小的子问题来解决。分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧。
归并排序是一个稳定的排序算法,在进行子数组合并的时候,我们可以设置当元素大小相等时,先将前半部分的数据放入临时数组,这样就可以保证相等元素在排序后依然保持原来的顺序。
不仅递归求解的问题可以写成递推公式,递归代码的时间复杂度也可以写成递归公式。
如果我们对 n 个元素进行归并排序所需要的时间是 T(n),那分解成两个子数组排序的时间都是 T(n/2),而合并两个子数组的时间复杂度为 O(n)。所以,归并排序的时间复杂度计算公式为:
T(1)=C
T(n)=2∗T(n2)+n,n>1
用大 O 标记法来表示,归并排序的时间复杂度为 O(nlogn)。
从我们的分析可以看出,归并排序的执行效率与原始数据的有序程度无关,其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。
归并排序有一个缺点,那就是它不是原地排序算法。在进行子数组合并的时候,我们需要临时申请一个数组来暂时存放排好序的数据。因为这个临时空间是可以重复利用的,因此归并排序的空间复杂度为 O(n),最多需要存放 n 个数据。
快速排序的思想是,如果要对数组区间 [p, r] 的数据进行排序,我们先选择其中任意一个数据作为 pivot(分支点),一般为区间最后一个元素。然后遍历数组,将小于 pivot 的数据放到左边,将大于 pivot 的数据放到右边。接着,我们再递归对左右两边的数据进行排序,直到区间缩小为 1 ,说明所有的数据都排好了序。
归并排序是由下向上的,先处理子数组然后再合并。而快速排序正好相反,它的过程是由上向下的,先分出两个子区间,再对子区间进行排序。归并排序是稳定的时间复杂度为 O(n),但它是非原地算法,而快排则是原地排序算法。
如果快速排序每次都将数据分成相等的两部分,则快排的时间复杂度和归并排序相同,也是 O(nlogn),但这种情况是很难实现的。如果数据原来已经是有序的,则每次的分区都是不均等的,我们需要进行 n 次分区才能完成整个排序,此时快排的时间复杂度就退化成了 O(n2)。
平均时间复杂度的求解也可以通过递归树来分析,这个问题留待我们以后再解决。我们现在只需要知道,在大部分情况下,快速排序的时间复杂度都可以做到 O(nlogn),只有在极端情况下,才会退化成 O(n2)。
快速排序是一个原地排序算法,是一个不稳定的排序算法,因为其在数据交换过程中可能会改变相等元素的原始位置。
归并排序和快速排序都是利用分治的思想,代码都通过递归来实现,过程非常相似。
归并排序非常稳定,时间复杂度始终都是 O(nlogn),但不是原地排序;快速排序虽然最坏情况下时间复杂度为 O(n2),但平均情况下时间复杂度为 O(nlogn),最坏情况发生的概率也比较小,而且是原地排序算法,因此应用得更加广泛。