時間複雜度,複雜類以及對快速排序算法的分析

時間複雜度,複雜類以及對快速排序算法的分析


有學弟問到了一個關於"快排的時間複雜度的證明方法"的問題,在這裏乾脆將其來龍去脈講清楚;在本文中我會先介紹一些基本概念,然後以快排算法爲例,講述如何應用這些概念來分析算法性能;


基本概念

定義(時間複雜度) 算法中基本操作重複執行的次數是問題規模nn的某個函數,其時間量度記作:T(n)=O(f(n))T(n)=O(f(n)),稱作算法的漸近時間複雜度(Asymptotic Time complexity),簡稱時間複雜度.一般地,常用最深層循環內的語句中的原操作的執行頻度(重複執行的次數)來表示.

更形式的定義可以這麼表述:

定義(漸進時間複雜度(asymptotic time complectiy)): 若存在函數f(n)f(n),使得當nn趨近於無窮大時,T(n)/f(n)T(n)/f(n)的極限值爲不等於零的常數,則稱f(n)f(n)T(n)T(n)的同數量級函數.記作T(n)=O(f(n))T(n)= O(f(n)),稱O(f(n))O(f(n))爲算法的漸進時間複雜度,簡稱時間複雜度.

漸進時間複雜度用大寫OO來表示,所以也被稱爲大OO表示法.

這就引出了算法分析的幾種表示法:

  • OO表示法: 算法的上緊界(tight upper bound);
  • Ω\Omega表示法: 算法的下緊界(greatest lower bound);
  • Θ\Theta表示法: 算法的平均運行時間(tight upper bound);

因此我們易得:Ω(f(n))Θ(f(n))O(f(n))\Omega(f(n)) \le \Theta(f(n)) \le O(f(n));

分治算法主定理

其實我們可以將這個定理看作一個解關於分治算法時間複雜度的遞推方程的一個分情況的定理;

定理(分治算法主定理): 分治算法通常遵守一種通用模式:即在解決規模爲nn的問題時,總是先遞歸地求解aa個規模爲n/bn/b的子問題,然後在O(nd)O(n^d)時間內將子問題的解合併起來,其中a,b,d>0a,b,d>0是一些特定的整數.分治算法的運行時間可以通過公式:

T(n)=aT(nb)+O(nd)T(n) = aT(\lceil \frac{n}{b} \rceil) + O(n^d)

得出.以下將給出這類一般遞推式的一個封閉解,以便由於遇到新的問題時,無需重新求解.如果對於常數a>0a>0,b>1b>1以及d0d \ge 0,有T(n)=aT(nb)+O(nd)T(n) = aT(\lceil \frac{n}{b} \rceil) + O(n^d)成立,則:

T(n)={O(nd)d>logbaO(ndlog2n)d=logbaO(nlogba)d<logba T(n) = \begin{cases} O(n^d) & d > log_ba \\ O(n^dlog_2n) & d = log_ba \\ O(n^{log_ba}) & d < log_ba \end{cases}

example(歸併排序): 歸併排序的時間複雜度爲T(n)=2T(n2)+O(n1)T(n) = 2T(\lceil \frac{n}{2} \rceil) + O(n^1),那麼a=2a=2,b=2b=2,d=1d=1,則d=1=logba=1d=1=\log_ba=1,由主定理可知T(n)=O(ndlog2n)=O(nlog2n)T(n) = O(n^dlog_2n) = O(nlog_2n);

常見的時間複雜度
時間複雜度 名稱 例如
O(1)O(1) 常數時間 鏈表的元素添加;
O(logn)O(logn) 對數時間 在二叉搜索樹中查找元素;
O(n)O(n) 線性時間 在無序數組中找特定元素;
O(nlogn)O(nlogn) 線性對數時間 歸併排序;
O(n2)O(n^2) 平方時間 給出圖中\forall兩個節點的最短路徑;
O(n)O(n) 立方時間 矩陣乘法;
O(2n)O(2^n) 指數時間 漢諾塔問題;
O(n!)O(n!) 階乘時間 九連環問題;
什麼是複雜度類

這是一個初學者容易和時間複雜度混淆的概念,但是其中確實也是有聯繫的;在計算複雜度理論中,一個複雜度類指的是一羣複雜度類似的問題的集合.一個典型的複雜度類的定義有以下形式:可以被同一個抽象機器M使用O(f(n))O(f(n))的資源R所解決的問題的集合(nn是輸入數據的大小),複雜度類的類型有如下幾種:

  • P: 能在多項式時間內被確定型圖靈機(deterministic machine)解決的問題.
  • NP: 不能在多項式時間內解決或不確定能不能在多項式時間內解決,但能在多項式時間驗證的問題.
  • NPC: NP完全問題,所有NP問題在多項式時間內都能約化(Reducibility)到它的NP問題,即解決了此NPC問題,所有NP問題也都得到解決.
  • NP-hard: NP難問題,所有NP問題在多項式時間內都能約化(Reducibility)到它的問題(不一定是NP問題).

對快速排序算法的分析

(basic idea): 通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列.(更多快排本身的細節可見這篇blog)

現在我們來分析快速排序算法,我們假設T(n)T(n)是快排算法QUICKSORT在輸入規模爲nn的數據集合上所花費的時間,那麼有遞歸式:

T(n)=max0qn1(T(q)+T(nq1)+Θ(n))T(n) = \max_{0\le q \le n-1}(T(q)+T(n-q-1)+\Theta(n))

在這裏我們對快排算法QUICKSORT每一次的規模拆分的設計是q:nq1q:n-q-1;

我們不妨設T(n)cn2T(n)\le cn^2成立(廢話…最爛爛不過插入排序對吧)則帶入上式:

T(n)cmax0qn1(q2+(nq1)2)+Θ(n)T(n) \le c \cdot \max_{0\le q \le n-1}(q^2+(n-q-1)^2) +\Theta(n)

q2+(nq1)2q^2+(n-q-1)^2在區間0qn10\le q \le n-1的端點上取最大值,由於該表達式二階導數爲正,因此可以知道當拆分是對半拆分時也是最佳的時間情形;綜上所述:

  • 最優情況下時間複雜度: O(nlogn)O(nlogn)(根據分治算法主定理);
  • 最差情況下時間複雜度: O(n2)O(n^2)(根據分治算法主定理);
  • 最優的情況下空間複雜度爲: O(logn)O(logn) 每一次都平分數組的情況;
  • 最差的情況下空間複雜度爲: O(n)O(n) 退化爲冒泡排序的情況;

問題解答

張:這麼說,快排並不是什麼"最優的排序方法"?

答曰: 是的,但是它是在現實實踐中表現最佳的算法,這不是定理給出的,而是實驗給出的(當我們每次以n2\lceil \frac{n}{2} \rceil拆分規模時);因此你的"怎麼證明它比其他任何排序都優異,以至於不需要再需要構思其他排序算法了"也就不需要回答了;

張:你講的根本不清楚啊/講的沒有意義啊/根本沒有回答我的問題啊/你不行啊…

答曰: 是的,但你仍然可以自己鑽研<<算法導論>>,書就在我桌子上,我相信憑藉你更加聰明的頭腦可以從第七章找出答案;

張:那當然…


代碼實現

#include<stdio.h>  

// 打印數組  
void print_array(int *array, int length)  
{  
    int index = 0;  
    printf("array:\n");  
    for(; index < length; index++){  
        printf(" %d,", *(array+index));  
    }     
    printf("\n\n");  
}  

void quickSort(int array[], int length)  
{  
    int start = 0;  
    int end = length-1;  
    int value = array[start];// 得到"哨兵"元素  
    
    if (1 > length) return;// 遞歸出口  
    
    while(start < end){// 以"哨兵"元素爲標準,分成大於它和小於它的兩列元素  
    
        while(start < end){// 從數組尾部往前循環得到小於"哨兵"元素的一個元素  
            if ( array[end--] < value ){  
                array[start++] = array[++end];  
                break;  
            }     
        }     
    
        while( start < end ){// 從數組頭部往後循環得到大於"哨兵"元素的一個元素  
            if( array[start++] > value){  
                array[end--] = array[--start];  
                break;  
            }     
        }     
    }     

    array[start] = value;// 放置"哨兵"元素  
    printf("\nstart:%d, end:%d\n", start, end);// 這個是測試下start和end是否一樣  
    quickSort(array, start);// 遞歸排序小於"哨兵"元素的那一列元素  
    quickSort(array + start + 1, length - start - 1);// 遞歸排序大於"哨兵"元素的那一列  
}  


int main(void)  
{  
    int array[12] = {1,11,12,4,2,6,9,0,3,7,8,2};  
    print_array(array, 12);// 開始前打印下  
    quickSort(array, 12);// 快速排序  
    print_array(array, 12);// 排序後打印下  
    return 0;  
}  

編譯及運行(實現細節可見這篇blog):

root@hanss-OptiPlex-9010:# gcc qsort.c -o a.out
root@hanss-OptiPlex-9010:# ./a.out 
array:
 1, 11, 12, 4, 2, 6, 9, 0, 3, 7, 8, 2,
start:1, end:1
start:0, end:0
start:9, end:9
start:0, end:0
start:2, end:2
start:1, end:1
start:0, end:0
start:3, end:3
start:2, end:2
start:0, end:0
start:0, end:0
start:0, end:0
array:
 0, 1, 2, 2, 3, 4, 6, 7, 8, 9, 11, 12,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章