遞歸問題的時間和空間複雜度分析

遞歸的時間/空間複雜度

在解決問題的過程當中,遞歸的正確使用老是能產生 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,能夠嘗試用主方法或者遞歸樹的方式來進行推導。

相關文章
相關標籤/搜索