拜託,面試別再問我時間複雜度了!!!

最煩面試官問,「爲何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))

  • 遞歸求解,將來再問時間複雜度,通殺


知其然,知其因此然。

思路比結論重要。

架構師之路-分享可落地的技術文章


推薦閱讀:

拜託,面試別再問我TopK了!

拜託,面試別再讓我數1了!

拜託,面試別再問我斐波那契數列了!


做業:使用隨機選擇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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索