最煩面試官問,「爲何XX算法的時間複雜度是OO」,從此,再也不害怕這類問題。web
快速排序分爲這麼幾步:面試
第一步,先作一次partition;算法
partition使用第一個元素t=arr[low]爲哨兵,把數組分紅了兩個半區:數組
左半區比t大微信
右半區比t小架構
第二步,左半區遞歸;app
第三步,右半區遞歸;dom
僞代碼爲:函數
void quick_sort(int[]arr, int low, int high){ui
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))
遞歸求解,將來再問時間複雜度,通殺
知其然,知其因此然。
思路比結論重要。
架構師之路-分享可落地的技術文章
推薦閱讀:
做業:使用隨機選擇randomized_select來找到n個數中第k大元素。
// 找arr[low, high]中第k大元素
int RS(arr, low, high, k){
if (low== high) return arr[low];
i = partition(arr, low, high);
t = i-low; //數組前半部分元素個數
if (t>=k)
return RS(arr, low, i-1, k); //前半部分第k大
else
return RS(arr, i+1, high, k-t); //後半部分第k-t大
}
問:randomized_select的時間複雜度是多少?
謝轉。
本文分享自微信公衆號 - 架構師之路(road5858)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。