本節內容
- 演算法定義
- 時間複雜度
- 空間複雜度
- 常用演算法例項
1.演算法定義
演算法(Algorithm)是指解題方案的準確而完整的描述,是一系列解決問題的清晰指令,演算法代表著用系統的方法描述解決問題的策略機制。也就是說,能夠對一定規範的輸入,在有限時間內獲得所要求的輸出。如果一個演算法有缺陷,或不適合於某個問題,執行這個演算法將不會解決這個問題。不同的演算法可能用不同的時間、空間或效率來完成同樣的任務。一個演算法的優劣可以用空間複雜度與時間複雜度來衡量。
一個演算法應該具有以下七個重要的特徵:
①有窮性(Finiteness):演算法的有窮性是指演算法必須能在執行有限個步驟之後終止;
②確切性(Definiteness):演算法的每一步驟必須有確切的定義;
③輸入項(Input):一個演算法有0個或多個輸入,以刻畫運算物件的初始情況,所謂0個輸 入是指演算法本身定出了初始條件;
④輸出項(Output):一個演算法有一個或多個輸出,以反映對輸入資料加工後的結果。沒 有輸出的演算法是毫無意義的;
⑤可行性(Effectiveness):演算法中執行的任何計算步驟都是可以被分解為基本的可執行 的操作步,即每個計算步都可以在有限時間內完成(也稱之為有效性);
⑥高效性(High efficiency):執行速度快,佔用資源少;
⑦健壯性(Robustness):對資料響應正確。
2. 時間複雜度
電腦科學中,演算法的時間複雜度是一個函式,它定量描述了該演算法的執行時間,時間複雜度常用大O符號(大O符號(Big O notation)是用於描述函式漸進行為的數學符號。更確切地說,它是用另一個(通常更簡單的)函式來描述一個函式數量級的漸近上界。在數學中,它一般用來刻畫被截斷的無窮級數尤其是漸近級數的剩餘項;在電腦科學中,它在分析演算法複雜性的方面非常有用。)表述,使用這種方式時,時間複雜度可被稱為是漸近的,它考察當輸入值大小趨近無窮時的情況。
大O,簡而言之可以認為它的含義是“order of”(大約是)。
無窮大漸近
大O符號在分析演算法效率的時候非常有用。舉個例子,解決一個規模為 n 的問題所花費的時間(或者所需步驟的數目)可以被求得:T(n) = 4n^2 - 2n + 2。
當 n 增大時,n^2; 項將開始占主導地位,而其他各項可以被忽略——舉例說明:當 n = 500,4n^2; 項是 2n 項的1000倍大,因此在大多數場合下,省略後者對錶達式的值的影響將是可以忽略不計的。
常數階O(1)
常數又稱定數,是指一個數值不變的常量,與之相反的是變數
為什麼下面演算法的時間複雜度不是O(3),而是O(1)。
1
2
3
|
int sum = 0,n = 100; /*執行一次*/ sum = (1+n)*n/2; /*執行一次*/ printf("%d", sum); /*行次*/ |
這個演算法的執行次數函式是f(n)=3。根據我們推導大O階的方法,第一步就是把常數項3改為1。在保留最高階項時發現,它根本沒有最高階項,所以這個演算法的時間複雜度為O(1)。
另外,我們試想一下,如果這個演算法當中的語句sum=(1+n)*n/2有10句,即:
1
2
3
4
5
6
7
8
9
10
11
12
|
int sum = 0, n = 100; /*執行1次*/ sum = (1+n)*n/2; /*執行第1次*/ sum = (1+n)*n/2; /*執行第2次*/ sum = (1+n)*n/2; /*執行第3次*/ sum = (1+n)*n/2; /*執行第4次*/ sum = (1+n)*n/2; /*執行第5次*/ sum = (1+n)*n/2; /*執行第6次*/ sum = (1+n)*n/2; /*執行第7次*/ sum = (1+n)*n/2; /*執行第8次*/ sum = (1+n)*n/2; /*執行第9次*/ sum = (1+n)*n/2; /*執行第10次*/ printf("%d",sum); /*執行1次*/ |
事實上無論n為多少,上面的兩段程式碼就是3次和12次執行的差異。這種與問題的大小無關(n的多少),執行時間恆定的演算法,我們稱之為具有O(1)的時間複雜度,又叫常數階。
注意:不管這個常數是多少,我們都記作O(1),而不能是O(3)、O(12)等其他任何數字,這是初學者常常犯的錯誤。
O(n【時間規模,也可以理解為執行次數】),當運算裡沒有最高階項(也就是次方)時,時間規模就是1,所以為o(1),
如果運算裡包含了最高階項(也就是次方)時,且次方數不為1時,時間規模就是最高階項(也就是次方數),如o(3),
推導大O階方法
1.用常數1取代執行時間中的所有加法常數
2.在修改後的執行次數函式中,只保留最高階項
3.如果最高階項存在且不是1,則去除與這個項相乘的常數
對數階O(log2n)
對數
如果a的x次方等於N(a>0,且a不等於1),那麼數x叫做以a為底N的對數(logarithm),記作x=logaN, 。其中,a叫做對數的底數,N叫做真數。
5^2 = 25 , 記作 2= log5 25
對數是一種運算,與指數是互逆的運算。例如
① 3^2=9 <==> 2=log<3>9;
② 4^(3/2)=8 <==> 3/2=log<4>8;
③ 10^n=35 <==> n=lg35。為了使用方便,人們逐漸把以10為底的常用對數記作lgN
對數階
1
2
3
4
5
6
7
8
9
|
int count = 1; while (count < n) { count = count * 2; /* 時間複雜度為O(1)的程式步驟序列 */ } |
由於每次count乘以2之後,就距離n更近了一分。
也就是說,有多少個2相乘後大於n,則會退出迴圈。
由2^x=n得到x=log2n。所以這個迴圈的時間複雜度為O(logn)。
線性階O(n)
執行時間隨問題規模增長呈正比例增長
1
2
3
4
5
|
data = [ 8,3,67,77,78,22,6,3,88,21,2] find_num = 22 for i in data: if i == 22: print("find",find_num,i ) |
線性對數階O(nlog2n)
平方階O(n^2)
1
2
3
4
|
for i in range(100): for k in range(100): print(i,k) |
立方階O(n^3)
k次方階O(n^k),
指數階O(2^n)。
隨著問題規模n的不斷增大,上述時間複雜度不斷增大,演算法的執行效率越低。
定義:如果一個問題的規模是n,解這一問題的某一演算法所需要的時間為T(n),它是n的某一函式 T(n)稱為這一演算法的“時間複雜性”。
當輸入量n逐漸加大時,時間複雜性的極限情形稱為演算法的“漸近時間複雜性”。 我們常用大O表示法表示時間複雜性,注意它是某一個演算法的時間複雜性。大O表示只是說有上界,由定義如果f(n)=O(n),那顯然成立f(n)=O(n^2),它給你一個上界,但並不是上確界,但人們在表示的時候一般都習慣表示前者。 此外,一個問題本身也有它的複雜性,如果某個演算法的複雜性到達了這個問題複雜性的下界,那就稱這樣的演算法是最佳演算法。 “大O記法”:在這種描述中使用的基本引數是 n,即問題例項的規模,把複雜性或執行時間表達為n的函式。這裡的“O”表示量級 (order),比如說“二分檢索是 O(logn)的”,也就是說它需要“通過logn量級的步驟去檢索一個規模為n的陣列”記法 O ( f(n) )表示當 n增大時,執行時間至多將以正比於 f(n)的速度增長。 這種漸進估計對演算法的理論分析和大致比較是非常有價值的,但在實踐中細節也可能造成差異。例如,一個低附加代價的O(n2)演算法在n較小的情況下可能比一個高附加代價的 O(nlogn)演算法執行得更快。當然,隨著n足夠大以後,具有較慢上升函式的演算法必然工作得更快。 O(1) Temp=i;i=j;j=temp; 以上三條單個語句的頻度均為1,該程式段的執行時間是一個與問題規模n無關的常數。演算法的時間複雜度為常數階,記作T(n)=O(1)。如果演算法的執行時間不隨著問題規模n的增加而增長,即使演算法中有上千條語句,其執行時間也不過是一個較大的常數。此類演算法的時間複雜度是O(1)。 O(n^2) 2.1. 交換i和j的內容 sum=0; (一次) for(i=1;i<=n;i++) (n次 ) for(j=1;j<=n;j++) (n^2次 ) sum++; (n^2次 ) 解:T(n)=2n^2+n+1 =O(n^2) 2.2. for (i=1;i<n;i++) { y=y+1; ① for (j=0;j<=(2*n);j++) x++; ② } 解: 語句1的頻度是n-1 語句2的頻度是(n-1)*(2n+1)=2n^2-n-1 f(n)=2n^2-n-1+(n-1)=2n^2-2 該程式的時間複雜度T(n)=O(n^2). O(n) 2.3. a=0; b=1; ① for (i=1;i<=n;i++) ② { s=a+b; ③ b=a; ④ a=s; ⑤ } 解:語句1的頻度:2, 語句2的頻度: n, 語句3的頻度: n-1, 語句4的頻度:n-1, 語句5的頻度:n-1, T(n)=2+n+3(n-1)=4n-1=O(n). O(log2n ) 2.4. i=1; ① while (i<=n) i=i*2; ② 解: 語句1的頻度是1, 設語句2的頻度是f(n), 則:2^f(n)<=n;f(n)<=log2n 取最大值f(n)= log2n, T(n)=O(log2n ) O(n^3) 2.5. for(i=0;i<n;i++) { for(j=0;j<i;j++) { for(k=0;k<j;k++) x=x+2; } } 解:當i=m, j=k的時候,內層迴圈的次數為k當i=m時, j 可以取 0,1,...,m-1 , 所以這裡最內迴圈共進行了0+1+...+m-1=(m-1)m/2次所以,i從0取到n, 則迴圈共進行了: 0+(1-1)*1/2+...+(n-1)n/2=n(n+1)(n-1)/6所以時間複雜度為O(n^3). 我們還應該區分演算法的最壞情況的行為和期望行為。如快速排序的最 壞情況執行時間是 O(n^2),但期望時間是 O(nlogn)。通過每次都仔細 地選擇基準值,我們有可能把平方情況 (即O(n^2)情況)的概率減小到幾乎等於 0。在實際中,精心實現的快速排序一般都能以 (O(nlogn)時間執行。 下面是一些常用的記法: 訪問陣列中的元素是常數時間操作,或說O(1)操作。一個演算法如 果能在每個步驟去掉一半資料元素,如二分檢索,通常它就取 O(logn)時間。用strcmp比較兩個具有n個字元的串需要O(n)時間。常規的矩陣乘演算法是O(n^3),因為算出每個元素都需要將n對 元素相乘並加到一起,所有元素的個數是n^2。 指數時間演算法通常來源於需要求出所有可能結果。例如,n個元 素的集合共有2n個子集,所以要求出所有子集的演算法將是O(2n)的。指數演算法一般說來是太複雜了,除非n的值非常小,因為,在 這個問題中增加一個元素就導致執行時間加倍。不幸的是,確實有許多問題 (如著名的“巡迴售貨員問題” ),到目前為止找到的演算法都是指數的。如果我們真的遇到這種情況,通常應該用尋找近似最佳結果的演算法替代之。 |