递归问题的时间和空间复杂度分析

递归的时间/空间复杂度

在解决问题的过程当中,递归的正确使用老是能产生 subtle code, 但追踪实际的递归调用序列一般是很是困难的,但当咱们了解递归的设计法则后,咱们知道,咱们通常没有必要知道这些细节,这正体现了使用递归的好处,由于计算机能计算出复杂的细节。java

递归的基本法则

  1. 基准情形。 无须递归就能解决的case。
  2. 不断推动。 确保每一次递归调用都将问题规模缩小,向基准情形推动。
  3. 设计法则。 假设全部的递归调用都能运行。
  4. 合成效益法则。 切勿在不一样的递归调用中作重复性的工做。该条法则能够引出记忆化递归

几个常见的递归算法

树的遍历

void printInorder(TreeNode root){
	if(root == null) return;
    printInorder(root.left);
    System.out.println(root.val);
    printInorder(root.right);
}
复制代码

该算法是一个很简单的递归算法,也是解决树的相关问题的一个常见patternpython

很显然,它处理了基本情形,而且不断向基本情形,空结点,推动。每一个节点只访问一次,递归深度为树的高度, 所以:算法

Time: T(n) = 2 * T(n / 2) + O(1) --> T(n) = O(n)数组

Space: O(logn) --> O(h) h--> the height of the tree函数

二分查找

def binary_search(a, l, r):
	m = (l + r) / 2
	if(f(m)):
		binary_search(a, l, m)
	else:
		binary_search(a, m + 1, r)
复制代码

Time: T(n) = T(n / 2) + O(1) --> T(n) = O(logn)性能

Space: O(logn)ui

快速排序

def qucik_sort(a, l, r):
	pivot = patition(a, l, r)	# Time: O(r - l)
	quick_sort(a, l, p)
	quick_sort(a, p + 1, r)
复制代码

因为快速排序的性能依赖于枢纽元pivot的选取,所以就存在最坏的情形最好的情形。 Best case: T(n) = 2 * T(n / 2) + O(n)spa

根据主方法(master method)T(n) = O(nlogn)设计

Worst case: T(n) = T(n - 1) + T(1) + O(n) --> T(n) = O(n ^ 2)code

Space: O(logn) --> O(n)

归并排序

def merge_sort(a, l, r):
	m = (l + r) / 2
	merge_sort(a, l, m)
	merge_sort(a, m + 1, r)
	merge(a, l, m, r) 	# O(r - l)
复制代码

和快速排序相似, 但它没有所谓的最好和最坏情形,由于它老是将问题的规模缩小一半。

但由于归并须要对数组进行拷贝操做,快排对系统的利用更高,而且worst case 不多出现,快排的使用更加的普遍。

Time: T(n) = 2 \* T(n / 2) + O(n) --> T(n) = O(nlogn)

Space: O(logn + n) --> 递归深度O(logn), 拷贝数组 O(n)

Combination

def conbination(d, s):
	if(d == n):
		return func()	#O(1)
	for i in range(d + 1, n):
		combination(d + 1, i + 1)
复制代码

Time: T(n) = T(n - 1) + T(n - 2) + ... + T(1) --> O(2^n)

Space: O(n)

Permutation

def permutation(d, used):
	if(d == n):
		return func()	#O(1)
	for i in range(0, n):
		if i in used: continue
		used.add(i)
		permutation(d + 1, used)
		used.remove(i)
复制代码

Time: T(n) = n * T(n - 1) --> O(n!)

Space: O(n)

总结表格

Equation Time Space Examples
T(n) = 2 * T(n / 2) + O(n) O(nlogn) O(logn) qucik_sort
T(n) = 2 * T(n / 2) + O(n) O(nlogn) O(logn + n) merge_sort
T(n) = T(n / 2) + O(1) O(logn) O(logn) binary_search
T(n) = 2 * T(n / 2) + O(1) O(nlogn) O(logn) binary tree
T(n) = T(n - 1) + O(1) O(n^2) O(n) quick_sort (worst case)
T(n) = n * T(n - 1) O(n!) O(n) permutation
T(n) = T(n - 1) + T(n - 2) + ... + T(1) O(2^n) O(n) combination

记忆化递归/Memorization Recursion

根据上述的递归基本法则第四条,合成效益法则,咱们再来看看这个斐波那契数列的问题。

def fib(n):
	if n < 3 : return 1
	return fib(n - 1) + fib(n - 2)
复制代码

Time: T(n) = T(n - 1) + T(n - 2) + ... + T(1) = O(2^n) = O(1.618^n)

它实际上重复求解了许多的子问题,那么其实能够设置一个记忆体来保存已经求结果的子问题的解。

def fib(n):
	if(n < 3): return 1
	if memo[n] > 0: return memo[n]
	memo[n] = fib(n - 1) + fib(n - 2)
	return memo[n]
复制代码

其中记忆体memo能够存储在全局变量, 也能够看成函数的参数传递。对记忆化递归的时间空间复杂度分析,一般只须要看它包含有多少个子问题。空间也和记忆体的大小成正比。

Time: O(n)

Space: O(n)

对于更加复杂的case,能够尝试用主方法或者递归树的方式来进行推导。