究竟為什么,快速排序的時間復(fù)雜度是n*lg(n)?
最煩面試官問,“為什么XX算法的時間復(fù)雜度是OO”,今后,不再懼怕這類問題。
快速排序分為這么幾步:
第一步,先做一次partition;
partition使用第一個元素t=arr[low]為哨兵,把數(shù)組分成了兩個半?yún)^(qū):
- 左半?yún)^(qū)比t大
- 右半?yún)^(qū)比t小
第二步,左半?yún)^(qū)遞歸;
第三步,右半?yún)^(qū)遞歸;
偽代碼為:
- 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);
- }
為啥,快速排序,時間復(fù)雜度是O(n*lg(n))呢?
今天和大家聊聊時間復(fù)雜度。
畫外音:往下看,第三類方法很牛逼。
第一大類,簡單規(guī)則
為方便記憶,先總結(jié)幾條簡單規(guī)則,熱熱身。
規(guī)則一:“有限次操作”的時間復(fù)雜度往往是O(1)。
例子:交換兩個數(shù)a和b的值。
- void swap(int& a, int& b){
- int t=a;
- a=b;
- b=t;
- }
分析:通過了一個中間變量t,進行了3次操作,交換了a和b的值,swap的時間復(fù)雜度是O(1)。
畫外音:這里的有限次操作,是指不隨數(shù)據(jù)量的增加,操作次數(shù)增加。
規(guī)則二:“for循環(huán)”的時間復(fù)雜度往往是O(n)。
例子:n個數(shù)中找到最大值。
- 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循環(huán),將數(shù)據(jù)集遍歷,每次遍歷,都只執(zhí)行“有限次操作”,計算的總次數(shù),和輸入數(shù)據(jù)量n呈線性關(guān)系。
規(guī)則三:“樹的高度”的時間復(fù)雜度往往是O(lg(n))。
分析:樹的總節(jié)點個數(shù)是n,則樹的高度是lg(n)。
在一棵包含n個元素二分查找樹上進行二分查找,其時間復(fù)雜度是O(lg(n))。
對一個包含n個元素的堆頂元素彈出后,調(diào)整成一個新的堆,其時間復(fù)雜度也是O(lg(n))。
第二大類:組合規(guī)則
通過簡單規(guī)則的時間復(fù)雜度,來求解組合規(guī)則的時間復(fù)雜度。
例如:n個數(shù)冒泡排序。
- 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]);
- }
分析:冒泡排序,可以看成三個規(guī)則的組合:
- 外層for循環(huán)
- 內(nèi)層for循環(huán)
- 最內(nèi)層的swap
故,冒泡排序的時間復(fù)雜度為:
O(n) * O(n) * O(1) = O(n^2)
又例如:TopK問題,通過建立k元素的堆,來從n個數(shù)中求解最大的k個數(shù)。
先用前k個元素生成一個小頂堆,這個小頂堆用于存儲,當(dāng)前最大的k個元素。
接著,從第k+1個元素開始掃描,和堆頂(堆中最小的元素)比較,如果被掃描的元素大于堆頂,則替換堆頂?shù)脑?,并調(diào)整堆,以保證堆內(nèi)的k個元素,總是當(dāng)前最大的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];
分析:可以看成三個規(guī)則的組合:
- 新建堆
- for循環(huán)
- 調(diào)整堆
故,用堆求解TopK,時間復(fù)雜度為:
O(k) + O(n) * O(lg(k)) = O(n*lg(k))
畫外音:注意哪些地方用加,哪些地方用乘;哪些地方是n,哪些地方是k。
第三大類,遞歸求解
簡單規(guī)則和組合規(guī)則可以用來求解非遞歸的算法的時間復(fù)雜度。對于遞歸的算法,該怎么分析呢?
接下來,通過幾個案例,來說明如何通分析遞歸式,來分析遞歸算法的時間復(fù)雜度。
案例一:計算 1到n的和,時間復(fù)雜度分析。
如果用非遞歸的算法:
- int sum(int n){
- int result=0;
- for(int i=0;i<n;i++)
- result += i;
- return result;
- }
根據(jù)簡單規(guī)則,for循環(huán),sum的時間復(fù)雜度是O(n)。
但如果是遞歸算法,就沒有這么直觀了:
- int sum(int n){
- if (n==1) return 1;
- return n+sum(n-1);
- }
如何來進行時間復(fù)雜度分析呢?
用f(n)來表示數(shù)據(jù)量為n時,算法的計算次數(shù),很容易知道:
(1) 當(dāng)n=1時,sum函數(shù)只計算1次
畫外音:if (n==1) return 1;
即:
f(1)=1【式子A】
(2) 不難發(fā)現(xiàn),當(dāng)n不等于1時:
f(n)的計算次數(shù),等于f(n-1)的計算次數(shù),再加1次計算
畫外音:return n+sum(n-1);
即:
f(n)=f(n-1)+1【式子B】
【式子B】不斷的展開,再配合【式子A】:
畫外音:這一句話,是分析這個算法的關(guān)鍵。
- f(n)=f(n-1)+1
- f(n-1)=f(n-2)+1
- …
- f(2)=f(1)+1
- f(1)=1
上面共n個等式,左側(cè)和右側(cè)分別相加:
- f(n)+f(n-1)+…+f(2)+f(1)
- =
- [f(n-1)+1]+[f(n-2)+1]+…+[f(1)+1]+[1]
即得到:
- f(n)=n
已經(jīng)有那么點意思了哈,再來個復(fù)雜點的算法。
案例二:二分查找binary_search,時間復(fù)雜度分析。
- 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);
- }
二分查找,單純從遞歸算法來分析,怎能知道其時間復(fù)雜度是O(lg(n))呢?
仍用f(n)來表示數(shù)據(jù)量為n時,算法的計算次數(shù),很容易知道:
(1) 當(dāng)n=1時,bs函數(shù)只計算1次
畫外音:不用糾結(jié)是1次還是1.5次,還是2.7次,是一個常數(shù)次。
即:
f(1)=1【式子A】
(2) 在n很大時,二分會進行一次比較,然后進行左側(cè)或者右側(cè)的遞歸,以減少一半的數(shù)據(jù)量:
f(n)的計算次數(shù),等于f(n/2)的計算次數(shù),再加1次計算
畫外音:計算arr[mid]>target,再減少一半數(shù)據(jù)量迭代
即:
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個等式,左側(cè)和右側(cè)分別相加:
- 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), 這一步,這是分析這個算法的關(guān)鍵。
將m=lg(n)帶入,得到:
f(n)=1+lg(n)
神奇不神奇?
最后,大boss,快速排序遞歸算法,時間復(fù)雜度的分析過程。
案例三:快速排序quick_sort,時間復(fù)雜度分析。
- 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)來表示數(shù)據(jù)量為n時,算法的計算次數(shù),很容易知道:
當(dāng)n=1時,quick_sort函數(shù)只計算1次
f(1)=1【式子A】
在n很大時:
- 第一步,先做一次partition;
- 第二步,左半?yún)^(qū)遞歸;
- 第三步,右半?yún)^(qū)遞歸;
即:
f(n)=n+f(n/2)+f(n/2)=n+2*f(n/2)【式子B】
畫外音:
(1)partition本質(zhì)是一個for,計算次數(shù)是n;
(2)二分查找只需要遞歸一個半?yún)^(qū),而快速排序左半?yún)^(qū)和右半?yún)^(qū)都要遞歸,這一點在分治法與減治法一章節(jié)已經(jīng)詳細(xì)講述過;
【式子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), 這一步,這是分析這個算法的關(guān)鍵。
將m=lg(n)帶入,得到:
- f(n)=lg(n)*n+2^(lg(n))*f(1)=n*lg(n)+n
故,快速排序的時間復(fù)雜度是n*lg(n)。
wacalei,有點意思哈!
畫外音:額,估計83%的同學(xué)沒有仔細(xì)看,花5分鐘細(xì)思上述過程,一定有收獲。
總結(jié)
- for循環(huán)的時間復(fù)雜度往往是O(n)
- 樹的高度的時間復(fù)雜度往往是O(lg(n))
- 二分查找的時間復(fù)雜度是O(lg(n)),快速排序的時間復(fù)雜度n*(lg(n))
- 遞歸求解,未來再問時間復(fù)雜度,通殺
知其然,知其所以然。思路比結(jié)論重要。
【本文為51CTO專欄作者“58沈劍”原創(chuàng)稿件,轉(zhuǎn)載請聯(lián)系原作者】