究竟為什么,快速排序的時間復雜度是n*lg(n)?
最煩面試官問,“為什么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]);
- }
分析:冒泡排序,可以看成三個規則的組合:
- 外層for循環
- 內層for循環
- 最內層的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];
分析:可以看成三個規則的組合:
- 新建堆
- for循環
- 調整堆
故,用堆求解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時,算法的計算次數,很容易知道:
(1) 當n=1時,sum函數只計算1次
畫外音:if (n==1) return 1;
即:
f(1)=1【式子A】
(2) 不難發現,當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時,算法的計算次數,很容易知道:
(1) 當n=1時,bs函數只計算1次
畫外音:不用糾結是1次還是1.5次,還是2.7次,是一個常數次。
即:
f(1)=1【式子A】
(2) 在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))
- 遞歸求解,未來再問時間復雜度,通殺
知其然,知其所以然。思路比結論重要。
【本文為51CTO專欄作者“58沈劍”原創稿件,轉載請聯系原作者】