C++ 資料結構與演算法
資料結構分為邏輯結構和物理結構
四大邏輯結構:
集合結構,集合結構中的資料元素除了同屬於一個集合外,他們之間沒有其他不三不四的關係。
線性結構,線性結構中的資料元素之間是一對一的關係。
樹形結構,樹形結構中,資料元素之間存在一種一對多的層次關係。
圖形結構,圖形結構的資料元素是多對多的關係。
資料元素的儲存形式:順序儲存和鏈式儲存
順序儲存:是把資料元素存放在地址連續的儲存單元裡,其資料間的邏輯關係和物理關係是一致的。
鏈式儲存:是把資料元素存放在任意的儲存單元裡,這組儲存單元可以是連續的,也可以是不連續的。需要一個指標存放資料元素的地址。這樣通過地址可以找到相關聯資料的位置。
演算法效率的度量方法
事後統計方法:這種方法主要是通過設計好的測試程式和資料,利用計算機計時器對不同演算法編制的程式的執行時間進行比較,從而確定演算法效率的高低。
缺陷:必須依據演算法事先編制好測試程式,通常需要花費大量時間和精力,完了發覺測試的是糟糕的演算法,那就功虧一簣。但不同的測試環境差別一般不大。
事前分析估算方法:在計算機程式編寫前,依據統計方法對演算法進行估算。經過總結,我們發現一個高階語言編寫的程式在計算機上執行時所消耗的時間取決於下列因素:
演算法採用的策略,方案
編譯產生的程式碼質量
問題的輸入規模
機器執行指令的速度
由此可見,拋開這些與計算機硬體、軟體有關的因素,一個程式的執行時間依賴於演算法的好壞和問題的輸入規模。(所謂的問題輸入規模是指輸入量的多少)
看一個例子:
第一種演算法:
int i, sum = 0, n = 100; // 執行1次
for( i=1; i <= n; i++ ) // 執行了n+1次
{
sum = sum + i; // 執行n次
}
第二種演算法:
int sum = 0, n = 100; // 執行1次
sum = (1+n)*n/2; // 執行1次
第一種演算法執行了1+(n+1)+n=2n+2次。
第二種演算法,是1+1=2次
如果我們把迴圈看做一個整體,忽略頭尾判斷的開銷,那麼這兩個演算法其實就是n和1的差距。
再看一下下面的這個迴圈:
int i, j, x=0, sum=0, n=100;
for( i=1; i <= n; i++ )
{
for( j=1; j <= n; j++ )
{
x++;
sum = sum + x;
}
}
我們研究演算法的複雜度,側重的是研究演算法隨著輸入規模擴大增長量的一個抽象,而不是精確地定位需要執行多少次。
函式的漸近增長:給定兩個函式f(n)
和g(n)
,如果存在一個整數N,使得對於所有的n>N,f(n)
總是比g(n)
大,那麼,我們說f(n)
的增長漸近快於g(n)
。
判斷一個演算法的效率時,函式中的常數和其他次要項常常可以忽略,而更應該關注主項(最高項)的階數。
演算法時間複雜度的定義:在進行演算法分析時,語句總的執行次數T(n)
是關於問題規模n的函式,進而分析T(n)
隨n的變化情況並確定T(n)
的數量級。演算法的時間複雜度,也就是演算法的時間量度,記作:T(n)= O(f(n))
。它表示隨問題規模n的增大,演算法執行時間的增長率和f(n)
的增長率相同,稱作演算法的漸近時間複雜度,簡稱為時間複雜度。其中f(n)
是問題規模n的某個函式。需要知道執行次數==時間。
這樣用大寫O()
來體現演算法時間複雜度的記法,我們稱之為大O記法。一般情況下,隨著輸入規模n的增大,T(n)
增長最慢的演算法為最優演算法。
那麼如何分析一個演算法的時間複雜度呢?即如何推導大O階呢:
用常數1取代執行時間中的所有加法常數。
在修改後的執行次數函式中,只保留最高階項。
如果最高階項存在且不是1,則去除與這個項相乘的常數。
得到的最後結果就是大O階。
看一個例子:
int sum = 0, n = 100;
printf(“I love fishc.com\n”);
printf(“I love Fishc.com\n”);
printf(“I love fishC.com\n”);
printf(“I love fIshc.com\n”);
printf(“I love FishC.com\n”);
printf(“I love fishc.com\n”);
sum = (1+n)*n/2;
這段程式碼的大O是多少?
O(8)?
線性階: 一般含有非巢狀迴圈涉及線性階,線性階就是隨著問題規模n的擴大,對應計算次數呈直線增長。
int i , n = 100, sum = 0;
for( i=0; i < n; i++ )
{
sum = sum + i;
}
上面這段程式碼,它的迴圈的時間複雜度為O(n)
,因為迴圈體中的程式碼需要執行n次。
平方階:
int i, j, n = 100;
for( i=0; i < n; i++ )
{
for( j=0; j < n; j++ )
{
printf(“I love FishC.com\n”);
}
}
n等於100,也就是說外層迴圈每執行一次,內層迴圈就執行100次,那總共程式想要從這兩個迴圈出來,需要執行100*100
次,也就是n的平方。所以這段程式碼的時間複雜度為O(n^2)
。
int i, j, n = 100;
for( i=0; i < n; i++ )
{
for( j=i; j < n; j++ )
{
printf(“I love FishC.com\n”);
}
}
分析下,由於當i=0
時,內迴圈執行了n次,當i=1
時,內迴圈則執行n-1
次……當i=n-1
時,內迴圈執行1次,所以總的執行次數應該是:
n+(n-1)+(n-2)+…+1 = n(n+1)/2 = n^2/2+n/2
用我們推導大O的攻略,第一條忽略,因為沒有常數相加。第二條只保留最高項,所以n/2這項去掉。第三條,去除與最高項相乘的常數,最終得O(n^2)
。
重要結論:迴圈的時間複雜度等於迴圈體的複雜度乘以該迴圈執行的次數。
時間複雜度 | 術語 |
---|---|
O(1) | 常數階 |
O(n) | 線性階 |
O(n^2) | 平方階 |
O(logn) | 對數階 |
O(nlogn) | nlogn階 |
O(n^3) | 立方階 |
O(2^n) | 指數階 |
常用的時間複雜度所耗費的時間從小到大依次是:O(1) < O(logn) < (n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
演算法的分析也是類似,我們查詢一個有n個隨機數字陣列中的某個數字,最好的情況是第一個數字就是,那麼演算法的時間複雜度為O(1)
,但也有可能這個數字就在最後一個位置,那麼時間複雜度為O(n)
。
我們在寫程式碼時,完全可以用空間來換去時間。
舉個例子說,要判斷某年是不是閏年,你可能會花一點心思來寫一個演算法,每給一個年份,就可以通過這個演算法計算得到是否閏年的結果。
另外一種方法是,事先建立一個有2050個元素的陣列,然後把所有的年份按下標的數字對應,如果是閏年,則此陣列元素的值是1,如果不是元素的值則為0。這樣,所謂的判斷某一年是否為閏年就變成了查詢這個陣列某一個元素的值的問題。
演算法的空間複雜度通過計算演算法所需的儲存空間實現,演算法的空間複雜度的計算公式記作:S(n)=O(f(n))
,其中,n為問題的規模,f(n)
為語句關於n所佔儲存空間的函式。
通常,我們都是用“時間複雜度”來指執行時間的需求,是用“空間複雜度”指空間需求。
當直接要讓我們求“複雜度”時,通常指的是時間複雜度。