拜託,面試別再問我時間複雜度了!!!
最煩面試官問,“ 為什麼XX演算法的時間複雜度是OO ”,今後,不再懼怕這類問題。
快速排序 分為這麼幾步:
第一步 ,先做一次partition;
partition使用第一個元素t=arr[low]為哨兵,把陣列分成了兩個半區:、
● 左半區比t大
● 右半區比t小
第二步 ,左半區遞迴;
第三步 ,右半區遞迴;
虛擬碼為:
void quick_sort(int[]arr, int low, int high){
if(low== high) return;
int i = partition (arr, low, high);
quick_sort (arr, low, i-1);
quick_sort (arr, i+1, high);
}
為啥,快速排序,時間複雜度是O(n*lg(n))呢?
今天和大家聊聊時間複雜度。
畫外音:往下看,第三類方法很牛逼。
第一大類,簡單規則
為方便記憶,先總結幾條簡單規則,熱熱身。
規則一:“有限次操作”的時間複雜度往往是O(1)。
例子:交換兩個數a和b的值。
void swap(int& a, int& b){
int t=a;
a=b;
b=t;
}
分析 :通過了一箇中間變數t,進行了3次操作,交換了a和b的值,swap的時間複雜度是O(1)。
畫外音:這裡的有限次操作,是指不隨資料量的增加,操作次數增加。
規則二:“for迴圈”的時間複雜度往往是O(n)。
例子:n個數中找到最大值。
int max(int[] arr, int n){
int temp = -MAX;
for(int i=0;i<n;++i)
if(arr[i]>temp) temp=arr[i];
return temp;
}
分析 :通過一個for迴圈,將資料集遍歷,每次遍歷,都只執行“有限次操作”,計算的總次數,和輸入資料量n呈 線性關係 。
規則三:“樹的高度”的時間複雜度往往是O(lg(n))。
分析 :樹的總節點個數是n,則樹的高度是lg(n)。
在一棵包含n個元素二分查詢樹上進行 二分查詢 ,其時間複雜度是 O(lg(n)) 。
對一個包含n個元素的堆頂元素彈出後, 調整成一個新的堆 ,其時間複雜度也是 O(lg(n)) 。
第二大類:組合規則
通過簡單規則的時間複雜度,來求解 組合 規則的時間複雜度。
例如:n個數氣泡排序。
void bubble_sort(int[] arr, int n){
for(int i=0;i<n;i++)
for(int j=0;j<n-i-1;j++)
if(arr[j]>arr[j+1])
swap(arr[j], arr[j+1]);
}
分析 :氣泡排序,可以看成三個規則的組合:
1. 外層for迴圈
2. 內層for迴圈
3. 最內層的swap
故, 氣泡排序 的時間複雜度為:
O(n) * O(n) * O(1) = O(n^2)
又例如:TopK問題,通過建立k元素的堆,來從n個數中求解最大的k個數。
先用前k個元素生成一個小頂堆,這個小頂堆用於儲存,當前最大的k個元素。
接著,從第k+1個元素開始掃描,和堆頂(堆中最小的元素)比較,如果被掃描的元素大於堆頂,則替換堆頂的元素,並調整堆,以保證堆內的k個元素,總是當前最大的k個元素。
直到,掃描完所有n-k個元素,最終堆中的k個元素,就是為所求的TopK。
虛擬碼 :
heap[k] = make_heap(arr[1, k]);
for(i=k+1 to n){
adjust_heap(heep[k],arr[i]);
}
return heap[k];
分析 :可以看成三個規則的組合:
1. 新建堆
2. for迴圈
3. 調整堆
故,用 堆求解TopK ,時間複雜度為:
O(k) + O(n) * O(lg(k)) = O(n*lg(k))
畫外音:注意哪些地方用加,哪些地方用乘;哪些地方是n,哪些地方是k。
第三大類,遞迴求解
簡單規則和組合規則可以用來求解非遞迴的演算法的時間複雜度。 對於遞迴的演算法,該怎麼分析呢?
接下來,通過幾個案例,來說明如何通分析 遞迴式 ,來分析 遞迴演算法 的 時間複雜度 。
案例一:計算 1到n的和,時間複雜度分析。
如果用 非遞迴的演算法 :
int sum(int n){
int result=0;
for(int i=0;i<n;i++)
result += i;
return result;
}
根據簡單規則,for迴圈,sum的時間複雜度是O(n)。
但如果是 遞迴演算法 ,就沒有這麼直觀了:
int sum(int n){
if (n==1) return 1;
return n+sum(n-1);
}
如何來進行時間複雜度分析呢?
用f(n)來表示資料量為n時,演算法的計算次數 ,很容易知道:
● 當n=1時,sum函式只計算1次
畫外音:if (n==1) return 1;
即:
f(1)=1 【式子A】
不難發現,當n不等於1時:
● f(n)的計算次數,等於f(n-1)的計算次數,再加1次計算
畫外音:return n+sum(n-1);
即:
f(n)=f(n-1)+1 【式子B】
【式子B】不斷的展開,再配合【式子A】 :
畫外音:這一句話,是分析這個演算法的關鍵。
f(n)=f(n-1)+1
f(n-1)=f(n-2)+1
…
f(2)=f(1)+1
f(1)=1
上面共n個等式,左側和右側分別相加:
f(n)+f(n-1)+…+f(2)+f(1)
=
[f(n-1)+1]+[f(n-2)+1]+…+[f(1)+1]+[1]
即得到 :
f(n)=n
已經有那麼點意思了哈,再來個複雜點的演算法。
案例二:二分查詢binary_search,時間複雜度分析。
int BS(int[] arr, int low, int high, int target){
if (low>high) return -1;
mid = (low+high)/2;
if (arr[mid]== target) return mid;
if (arr[mid]> target)
return BS(arr, low, mid-1, target);
else
return BS(arr, mid+1, high, target);
}
二分查詢,單純從遞迴演算法來分析,怎能知道其時間複雜度是O(lg(n))呢?
仍用 f(n)來表示資料量為n時,演算法的計算次數 ,很容易知道:
● 當n=1時,bs函式只計算1次
畫外音:不用糾結是1次還是1.5次,還是2.7次,是一個常數次。
即:
f(1)=1 【式子A】
在n很大時,二分會進行一次比較,然後進行左側 或者 右側的遞迴,以減少一半的資料量:
● f(n)的計算次數,等於f(n/2)的計算次數,再加1次計算
畫外音:計算arr[mid]>target,再減少一半資料量迭代
即:
f(n)=f(n/2)+1 【式子B】
【式子B】不斷的展開,
f(n)=f(n/2)+1
f(n/2)=f(n/4)+1
f(n/4)=f(n/8)+1
…
f(n/2^(m-1))=f(n/2^m)+1
上面共 m個等式 ,左側和右側分別相加:
f(n)+f(n/2)+…+f(n/2^(m-1))
=
[f(n/2)+1]+[f(n/4)+1]+…+[f(n/2^m)]+[1]
即得到 :
f(n)=f(n/2^m)+m
再配合【式子A】:
f(1)=1
即,n/2^m=1時, f(n/2^m)=1, 此時m=lg(n), 這一步,這是分析這個演算法的關鍵。
將m=lg(n)帶入, 得到 :
f(n)=1+lg(n)
神奇不神奇?
最後,大boss,快速排序遞迴演算法,時間複雜度的分析過程。
案例三:快速排序quick_sort,時間複雜度分析。
void quick_sort(int[]arr, int low, inthigh){
if (low==high) return;
int i = partition(arr, low, high);
quick_sort(arr, low, i-1);
quick_sort(arr, i+1, high);
}
仍用 f(n)來表示資料量為n時,演算法的計算次數 ,很容易知道:
● 當n=1時,quick_sort函式只計算1次
f(1)=1 【式子A】
在n很大時:
第一步,先做一次partition;
第二步,左半區遞迴;
第三步,右半區遞迴;
即:
f(n)=n+f(n/2)+f(n/2)=n+2*f(n/2) 【式子B】
畫外音:
(1)partition本質是一個for,計算次數是n;
(2)二分查詢只需要遞迴一個半區,而快速排序左半區和右半區都要遞迴,這一點在 分治法 與 減治法 一章節已經詳細講述過;
【式子B】不斷的展開,
f(n)=n+2*f(n/2)
f(n/2)=n/2+2*f(n/4)
f(n/4)=n/4+2*f(n/8)
…
f(n/2^(m-1))=n/2^(m-1)+2f(n/2^m)
上面共 m個等式 , 逐步帶入 ,於是得到:
f(n)=n+2*f(n/2)
=n+2*[n/2+2*f(n/4)]=2n+4*f(n/4)
=2n+4*[n/4+2*f(n/8)]=3n+8f(n/8)
=…
=m*n+2^m*f(n/2^m)
再配合【式子A】:
f(1)=1
即,n/2^m=1時, f(n/2^m)=1, 此時m=lg(n), 這一步,這是分析這個演算法的關鍵。
將m=lg(n)帶入, 得到:
f(n)=lg(n)*n+2^(lg(n))*f(1)=n*lg(n)+n
故,快速排序的時間複雜度是n*lg(n)。
wacalei,有點意思哈!
畫外音:額,估計83%的同學沒有細究看,花5分鐘細思上述過程,一定有收穫。
總結
● for迴圈 的時間複雜度往往是 O(n)
● 樹的高度 的時間複雜度往往是 O(lg(n))
● 二分查詢 的時間複雜度是 O(lg(n)) , 快速排序 的時間複雜度n *(lg(n))
● 遞迴求解 ,未來再問時間複雜度,通殺
知其然,知其所以然。
思路 比結論重要。
原文釋出時間為:2018-10-8
本文作者:58沈劍