算法的概念
算法(Algorithm)是計算機處理信息的本質,因爲計算機程序本質上是一個算法來告訴計算機確切的步驟來執行一個指定的任務。一般地,當算法在處理信息時,會從輸入設備或數據的存儲地址讀取數據,把結果寫入輸出設備或某個存儲地址供以後再調用。
算法是獨立存在的一種解決問題的方法和思想。
算法可以有不同的語言描述實現版本(如C、C++、Python、Java描述等),對於算法而言,實現的語言並不重要,重要的是 思想。
算法的五大特性
- 輸入: 算法具有0個或多個輸入。
- 輸出: 算法至少有1個或多個輸出。
- 有窮性: 算法在有限的步驟之後會自動結束而不會無限循環,並且每一個步驟可以在可接受的時間內完成。
- 確定性:算法中的每一步都有確定的含義,不會出現二義性。
- 可行性:算法的每一步都是可行的,也就是說每一步都能夠執行有限的次數完成。
算法設計的要求
正確性: 算法至少應該具有輸入、輸出和加工處理無歧義性、能反映問題的需求、能夠得到問題的正確答案。
可讀性: 算法設計的另一目的是爲了便於閱讀、理解和交流。
健壯性: 當輸入數據不合法時,算法也能做出相關處理,而不是產生異常或莫名其妙的結果。
時間效率高和存儲量低:
時間效率指的是算法的執行時間,存儲量需求指的是算法在執行過程中需要的存儲空間。設計算法應該儘量滿足時間效率高和存儲量低的需求。
算法效率衡量
1、事後統計法
該方法通過設計好的測試程序和數據,然後在計算機中運行,接着對運行時間進行比較,耗時少的效率高。
執行時間反應算法效率
對於同一問題,我們給出了兩種解決算法,在兩種算法的實現中,我們對程序執行的時間進行了測算,發現兩段程序執行的時間相差懸殊(214.583347秒相比於0.182897秒),由此我們可以得出結論:實現算法程序的執行時間可以反應出算法的效率,即算法的優劣。
單靠時間值絕對可信嗎?
假設我們將第二次嘗試的算法程序運行在一臺配置古老性能低下的計算機中,情況會如何?很可能運行的時間並不會比在我們的電腦中運行算法一的214.583347秒快多少。
單純依靠運行的時間來比較算法的優劣並不一定是客觀準確的!
程序的運行離不開計算機環境(包括硬件和操作系統),這些客觀原因會影響程序運行的速度並反應在程序的執行時間上。但很顯然,這種方式有很大缺陷,首先,算法的測試數據需要花時間設計,因爲不同的測試數據往往會直接影響運行時間,然後是計算機的硬件也會影響運行時間。這就造成了度量結果的不穩定。
2、事前分析法
由此,事前分析法誕生了,該方法無需運行程序,就能夠分析出一個算法的效率。
經過大量分析,前輩們總結出一個算法在計算機上運行時所消耗的時間取決於以下因素:
1.算法採用的策略、方法
2.編譯產生的代碼質量
3.問題的輸入規模
4.機器執行指定的速度
3、時間複雜度與“大O記法”
我們假定計算機執行算法每一個基本操作的時間是固定的一個時間單位,那麼有多少個基本操作就代表會花費多少時間單位。算然對於不同的機器環境而言,確切的單位時間是不同的,但是對於算法進行多少個基本操作(即花費多少時間單位)在規模數量級上卻是相同的,由此可以忽略機器環境的影響而客觀的反應算法的時間效率。
對於算法的時間效率,我們可以用“大O記法”來表示。
“大O記法”: 對於單調的整數函數 f,如果存在一個整數函數 g和實常數 c>0,使得對於充分大的 n總有 f(n)<=c*g(n),就說函數 g是 f的一個漸近函數(忽略常數),記爲 f(n)=O(g(n))。也就是說,在趨向無窮的極限意義下,函數 f 的增長速度受到函數 g 的約束,亦即函數 f 與函數 g 的特徵相似。
時間複雜度:假設存在函數g,使得算法A處理規模爲n的問題示例所用時間爲T(n)=O(g(n)),則稱O(g(n))爲算法A的漸近時間複雜度,簡稱時間複雜度,記爲T(n)。
如何理解“大O記法”
對於算法進行特別具體的細緻分析雖然很好,但在實踐中的實際價值有限。對於算法的時間性質和空間性質,最重要的是其數量級和趨勢,這些是分析算法效率的主要部分。而計量算法基本操作數量的規模函數中那些常量因子可以忽略不計。例如,可以認爲 3n²
和 100n²
屬於同一個量級,如果兩個算法處理同樣規模實例的代價分別爲這兩個函數,就認爲它們的效率“差不多”,都爲 n²
級。
4、最壞時間複雜度
分析算法時,存在幾種可能的考慮:
- 算法完成工作最少需要多少基本操作,即 最優時間複雜度
- 算法完成工作最多需要多少基本操作,即 最壞時間複雜度
- 算法完成工作平均需要多少基本操作,即 平均時間複雜度
對於最優時間複雜度,其價值不大,因爲它沒有提供什麼有用信息,其反映的只是最樂觀最理想的情況,沒有參考價值。
對於最壞時間複雜度,提供了一種保證,表明算法在此種程度的基本操作中一定能完成工作。
對於平均時間複雜度,是對算法的一個全面評價,因此它完整全面的反映了這個算法的性質。但另一方面,這種衡量並沒有保證,不是每個計算都能在這個基本操作內完成。而且,對於平均情況的計算,也會因爲應用算法的實例分佈可能並不均勻而難以計算。
因此,我們主要關注算法的最壞情況,亦即最壞時間複雜度。
時間複雜度的幾條基本計算規則
- 基本操作,即只有常數項,認爲其時間複雜度爲O(1)。
- 順序結構,時間複雜度按 加法 進行計算。
- 循環結構,時間複雜度按 乘法 進行計算。
- 分支結構,時間複雜度 取最大值。
- 判斷一個算法的效率時,往往只需要關注操作數量的最高次項,其它次要項和常數項可以忽略
- 在沒有特殊說明時,我們所分析的算法的時間複雜度都是指 最壞時間複雜度。
程序舉例
1、常數階
void main(){
int sum = 0,n = 100;
sum = (1 + n) * n / 2;
printf("%d",sum);
}
執行次數爲3,此時根據結論,用常數1代替所有加法常數,沒有最高階項,所以該算法的時間複雜度爲O(1)。
2、線性階
void main(){
int i,sum = 0,n = 100;
for(i = 1;i <= n;i++){
sum += i;
}
printf("%d",sum);
}
執行次數爲1 + (n + 1) + n + 1 = 2n + 3,根據結論,用常數1代替加法常數,3替換爲1;保留最高階項2n,去除與最高階項相乘的常數,所以該算法的時間複雜度爲O(n)。
其實在計算的時候,我們無需這樣算出每一句代碼的執行次數,對於賦值、循環條件、輸出語句,我們可以直接不考慮,所以可以直接得出該算法的時間複雜度爲O(n)。
3、對數階
void main(){
int count = 1;
while(count < n){
count *= 2;
}
}
該程序中我們只需得出循環次數即可求出時間複雜度,由於每次count乘以2之後,就距離n更近了一分,也就是說,有多少個2相乘後大於n,纔會退出循環。由2x = n得出,所以該算法的時間複雜度爲O( logn)。
4、平方階
void main(){
int i,j,n = 100;
for(i = 0;i < n;i++){
for(j = 0;j < n;j++){
printf("%d\t",n);
}
}
}
我們知道,對於內層循環,其時間複雜度爲O(n),而外層循環不過是執行n次內層循環,所以該算法的時間複雜度爲。
常見時間複雜度
執行次數函數舉例 | 階 | 非正式術語 |
---|---|---|
常數階 | ||
線性階 | ||
平方階 | ||
對數階 | ||
nlogn階 | ||
立方階 | ||
指數階 |
注意,經常將 (以2爲底的對數)簡寫成
常見時間複雜度之間的關係
常用的時間複雜度所耗費的時間從小到大依次是:
指數階 和階乘階 除非是很小很小的 n 值,否則哪怕 n 只是100,都是噩夢般的運行時間。所以這種不切實際的算法時間複雜度,一般我們都不會去討論。
空間複雜度
隨着互聯網科技的發展,早先比較貴的存儲在如今都較爲便宜,所以在某些特定的場景下,可以考慮使用 空間來換取時間。
算法的空間複雜度通過計算算法所需的存儲空間實現,空間複雜度的計算公式爲:S(n) = O(f(n)),其中,n爲問題的規模,f(n)爲語句關於n所佔存儲空間的函數。
通常情況下,我們更注重算法的時間複雜度,所以,空間複雜度只作爲一個瞭解。
小試牛刀
如果 a+b+c=1000,且 (a,b,c 爲自然數)
設計程序求出所有a、b、c可能的組合?
該程序的時間複雜度是多少?
✍ 歡迎大家來評論區解答,不要忘記點贊收藏喲。✌