前言
衆所周知,針對某一個問題,可能有多種算法解決,每個算法的作者都認爲自己寫的是最優秀的算法,這時候就會產生議論甚至是爭吵,誰的算法纔算好呢?口說無憑,要拿出讓人信服的證據才足以服衆。因此算法效率的衡量方法算是每個程序猿必不可少的技能。
現在最主要的兩種衡量方法便是時間複雜度和空間複雜度,但用的最多的是時間複雜度,因爲隨着技術的革新,硬盤容量和內存容量也在不斷擴大,對於空間複雜度而言已經沒有以往那般重要了(想想以前的程序猿要在幾KB甚至更小的內存裏倒騰也是蠻佩服的),對於用戶而言跟軟件產品的好壞直接掛鉤的是時間,響應快的軟件總是格外受歡迎,所以時間性能是尤爲重要的,那麼啥又是時間複雜度捏?咋個計算呢?
時間複雜度
定義
(摘自嚴版《數據結構》)一般情況下,算法重基本操作重複執行的次數是問題規模的某個函數,算法的時間度量計作
它表示隨問題規模的增大,算法執行時間的增長率和的增長率相同,稱做算法的漸進時間複雜度,簡稱時間複雜度。
不是說時間度量嗎?爲什麼用的是算法基本操作執行次數來表示?
因爲不同計算機硬件不同、操作系統不同、編譯器不同等問題都會影響到算法執行的時間,在天河一號上跑算法的時間和在自己家用計算機上跑同樣算法出來的時間應該差距還蠻大的。所以就考慮用算法基本操作的次數替代具體時間,畢竟算法執行時間也就是每個基本操作執行時間的總和,重複執行的基本操作次數多了,花費的總時間肯定也相應的多了。
那爲啥說是漸進的時間複雜度呢?
因爲要程序猿一條指令一條指令的數也不太現實,尤其是有循環、遞歸等等複雜的算法,想快速數清楚基本操作難度比較大,怎麼辦呢,那就數容易數的循環就好了,因爲每個循環體裏的操作都要重複執行,誰的算法循環次數多(特別是循環嵌套),相應的花的時間肯定也多。對於只執行一次的操作而言,這種重複執行的循環體肯定次數更多一些,所以就忽略掉一部分了,如果兩個時間複雜度是不同級別的時候就可以忽略n前面的係數和低階的次數,就是所謂的漸進了。
級別
計算時間複雜度的基本步驟
-
確定算法中的基本操作以及問題的規模。多數情況下取最深層循環內的基本操作(就是嵌套循環最裏層的循環體)。循環次數和結束條件有關,一般是有一個循環參數在控制,增加到上界或者減少到下界就停止循環,這個循環參數就是所說的規模。
- 根據基本操作執行情況計算出規模的函數,並確定時間複雜度爲(中增長最快的項/此項的係數)
- 注意:算法基本操作次數不一定是固定讀,比如循環次數是由用戶輸入決定的時候,執行次數是不同的,因此,考慮時間複雜度是按照使基本操作執行次數最多的輸入來計算的,即所謂的最壞時間複雜度。
實例
題目:
最大子列和問題
給定K個整數組成的序列{ N1, N2, ..., NK },“連續子列”被定義爲{ Ni, Ni+1, ..., Nj },其中 1≤i≤j≤K。“最大子列和”則被定義爲所有連續子列元素的和中最大者。例如給定序列{ -2, 11, -4, 13, -5, -2 },其連續子列{ 11, -4, 13 }有最大的和20。現要求你編寫程序,計算給定整數序列的最大子列和。
本題旨在測試各種不同的算法在各種數據情況下的表現。各組測試數據特點如下:
- 數據1:與樣例等價,測試基本正確性;
- 數據2:102個隨機整數;
- 數據3:103個隨機整數;
- 數據4:104個隨機整數;
- 數據5:105個隨機整數;
輸入格式:
輸入第1行給出正整數K (≤100000);第2行給出K個整數,其間以空格分隔。
輸出格式:
在一行中輸出最大子列和。如果序列中所有整數皆爲負數,則輸出0。
輸入樣例:
6
-2 11 -4 13 -5 -2
輸出樣例:
20
解決算法:
1.暴力求解法
/*
* 列舉每個子序列(都是從左邊界到右邊界求和),求解完不斷更新最大子序列和
*/
void MaxSubsequSum1(int a[], int length)
{
int thisSum = 0,maxSum = 0;
int i,j,k;
for(i = 0; i < length; i++)
{
for(j = i; j < length; j++)
{
thisSum = 0;
for(k = i; k <= length; k++)
{
thisSum +=A[k];
if(thisSum > maxSum)
{
maxSum = thisSum;
}
}
}
}
printf("%d",maxSum);
}
可以看出算法裏有三層循環,循環的結束條件都是當循環控制變量增長到數組長度時結束,如果數組長度設爲n,那麼此算法規模就是,所以。
2.類N!方法
/*
* 從左邊開始求以左邊開始的所有子序列和的最大值,然後不斷將左邊界右移,重複求最大值
*/
void MaxSubsequSum1(int a[], int length)
{
int thisSum = 0,maxSum = 0;
int i,j;
for(i = 0; i < length; i++)
{
thisSum = a[i];
for(j = i+1; j < length; j++)
{
thisSum += a[j];
if(thisSum > maxSum)
{
maxSum = thisSum;
}
}
}
printf("%d",maxSum);
}
規模:
時間複雜度:
3.分而治之
/*
* 所謂分治法:將一個問題的求解過程分解爲兩個大小相等的子問題進行求解,如果分解後的子問題本身也可
* 以分解的話,則將這個分解的過程進行下去,直至最後得到的子問題不能再分解爲止。
* ->求最大子序列和,我們可以將求最大子序列和的序列分解爲兩個大小相等的子序列,然後在這兩個大小相
* 等的子序列中,分別求最大子序列和,如果由原序列分解的這兩個子序列還可以進行分解的話,進一步分
* 解,直到不能進行分解爲止,使問題逐步簡化,最後求最簡化的序列的最大子序列和,沿着分解路徑逐步回
* 退,合成爲最初問題的解。
* 1.序列的左半部分的最大子序列和
* 2.序列的右半部分的最大子序列和
* 3.橫跨序列左半部分和右半部分得到的最大子序列和
* 4.比較3者求最大值
* 算法中左半邊和右半邊最大序列和通過遞歸求得的。
* 算法中跨邊界序列和是用從中間分點向兩邊疊加,得到最大值。
*/
int MaxSubsequSum2(int a[], int left, int right)
{
if(left == right)
{
return a[left];
}
//先分
int center = (left+right)/2;
int maxLeftSum = MaxSubsequSum2(a, left, center);
int maxRigthSum = MaxSubsequSum2(a, center+1, right);
int maxLeftBorderSum = 0 , leftBorderSum = 0;
//處理左序列
for(int i = center; i >= left; --i)
{
leftBorderSum += a[i];
if(leftBorderSum > maxLeftBorderSum)
maxLeftBorderSum = leftBorderSum;
}
//處理右序列
int maxRightBorderSum = 0, rightBorderSum = 0;
for(int j=center+1; j <= right; j++)
{
rightBorderSum += a[j];
if(rightBorderSum > maxRightBorderSum)
maxRightBorderSum = rightBorderSum;
}
int temp = maxLeftSum>maxRigthSum?maxLeftSum:maxRigthSum;
if(temp >= maxLeftBorderSum+maxRightBorderSum)
return temp;
else
return maxLeftBorderSum+maxRightBorderSum;
}
分析:左半邊迭代規模是T(N/2)(因爲把左半邊的每個元素都訪問一遍),中間跨邊界序列和最大規模是T(N)(將整個序列都訪問一遍)右半邊迭代規模同左邊T(N/2)
計算: 將T(N/2)迭代進T(N)一次。
其中 忽略,所以時間複雜度爲
4.在線處理
/*
* 在線處理的思想就是從左邊界開始循環向右求和,一旦碰到當前是負數或者序列和爲負數就放棄,不斷更新正
* 的最大序列和即爲最大子序列和。
*/
int MaxSubsequSum3(int a[], int length)
{
int ThisSum = 0,MaxSum = 0;
for(int i = 0; i < length; i++)
{
ThisSum += a[i];
if(ThisSum < 0)
{
ThisSum = 0;
}else if(ThisSum > MaxSum)
{
MaxSum = ThisSum;
}
}
return MaxSum;
}
規模:n(因爲只訪問了一遍整個序列)
時間複雜度:
具體性能測試
可以看出時間複雜度不同的算法,處理問題的效率真的差別很大。
總結
時間複雜度可以很方便的看出今後的數據結構各種算法的效率,其實時間複雜度在我們平時自己寫程序的模塊函數時都能使用到,多嘗試用時間複雜度計算,一眼看出算法優劣你也可以。(以後工作Boss讓優化算法也是把算法時間複雜度朝着nlogn甚至是n努力!)