算法和算法分析【緒論】(2)

算法和算法分析

在數據結構中談一個算法的時候,着眼於數據量(數據規模)不是很小且在內存中處理的問題。

我們處理的數據量(數據規模)不會太小,因爲數據量(數據規模)太小,不容易明顯的區分和分析那種算法的性能更好。數據量(數據規模)太小,算法的性能不會相差太大。

儘管數據量(數據規模)不是很小,但是這些數據在內存當中足以放得下。因爲在內存中能夠處理的問題,我們就可以集中考慮算法時間和空間的代價。否則,內存放不下,數據量過大,涉及到內存和外存之間數據的調入調出,都會對於算法的評估有一定的影響。

所以我們重申:
1.數據結構着眼於數據量(數據規模)不是很小,要有足夠的數據量(數據規模)。
2.足夠量的數據一定在內存中足夠放得下。
在這兩個前提下去研究算法以及算法分析問題。

算法的基本概念

算法—是對特定問題求解步驟的一種描述,它是指令的有限序列,其中每一條指令表示一個或多個操作。

這裏要注意什麼是特定問題?
特定問題是對於某一種抽象數據類型的操作,操作就是要解決某一具體問題。
例如:線性表這種抽象數據類型,它的操作就有查找,插入,刪除,修改等。每一種操作都是一個特定的問題,要解決這個問題就會涉及到一系列步驟的描述,也就是算法。

要解決特定問題,算法並不是只有唯一的一種。所以在寫算法的時候,前提是能解決特定問題的基礎之上然後去考慮這個算法是不是最優的。所謂最優,我們會從很多不同的角度去衡量。

算法的五個特徵

有窮性:直接來說就是不能死循環,要求算法必須在合理的時間之內能夠結束。
確定性:指令具備確切的含義,具有唯一確定的執行路徑。給定相同的輸入能夠得到相同的輸出。不能出現多次輸入相同的值,輸出的結果不一樣的情況。
可行性:描述的操作一定是可以實現的。
0個或多個輸入:算法有0個或多個輸入。
1個或多個輸出:算法要解決特定的問題,執行完之後沒有結果,那算法就沒有存在的必要性了。

算法的設計要求

正確性:設計的算法沒有語法錯誤,經得起測試,合理的輸入一定要能夠得到合法的結果。
可讀性:尤其大型軟件開發,團隊合作,每個人會負責不同的模塊,你的代碼自己能讀懂,別人也要方便能讀懂。這樣做有便於後期的維護,升級和修改。例如:詳加註釋。
健壯性:足夠的強壯,如果用戶給了非法的數據,算法要能夠做出適當的反應或者報錯,不能出現給一個非法測試,程序直接沒有任何反應了,這是不行的。
高效率:高效率是指時間方面的性能,也就是算法執行要快。執行快是有前提的,在解決相同的問題時,兩個算法一個執行快,一個執行慢,體現出來了算法的效率差距。解決不同的問題時,算法沒有可比性。

低存儲:算法的執行必然藉助於存儲空間,我們就需要考慮對於存儲空間的消耗。
解決相同的問題,需要的存儲量越小對於我們來說肯定是越好。

算法和程序

算法的含義與程序十分相似,但二者是有區別的。
1、 一個程序不一定滿足有窮性(如一個操作系統在用戶未使用前一直處於“等待” 的循環中, 直到出現新的用戶事件爲止。這樣的系統可以無休止地運行,直到系統停工。);

2、 程序中的指令必須是機器可執行的,而算法中的指令則無此限制。算法若用計算機語言來書寫,則它就可以是程序。

算法的描述

一個算法可以用自然語言、數學語言或約定符號來描述,也可以用流程圖、計算機高級程序語言(如 C 語言)或僞代碼等來描述。

算法的表現形式:僞代碼表示
冒泡排序算法:

void   bubble_sort(int   a[] , int  n)
{    
//將a中整數序列按從小到大的順序排序
	  for(i = n-1;i>=1; i--)
	  {
         for(j = 0; j<i;j++)
		    if(a[j] > a[j+1])
		    {
				a[j]<--->a[j+1];
		    } 
}

冒泡排序的優化算法:

void   bubble_sort(int   a[] , int  n)
{    //將a中整數序列按從小到大的順序排序
	  for(i = n-1, change = TURE;i>=1 && change; i--)
	  {
            change = FALSE;//交換標識
             for(j = 0; j<i;j++)
		       if(a[j] > a[j+1])
		       {
			a[j]<--->a[j+1];
                           change = TRUE;
                   } 
}

詳細的冒泡排序說明在點擊下面進入:
👇👇👇
詳細的冒泡排序說明

算法效率的度量

事後統計法

先運行,最終看運行結果。有一個弊端就是運行結果會依賴計算機的軟件和硬件系統。不同的計算機會有差異,同一臺計算機不同的時間段運行也會有差異。

事前分析法

事前根據算法的策略,問題的規模,實現的語言以及編譯程序所產生的機器代碼的質量和機器執行指令的速度等方面進行分析。

在前面很多條件裏面我們會發現:
算法的策略和人腦的思維有關,例如,冒泡排序算法不同的人也會寫出不同的算法。
問題規模大小不等。
實現語言的不同,C語言實現和使用其它語言實現也會有一定的差異。
不管是實現語言還是編譯程序所產生的機器代碼的質量和機器執行指令的速度,都和軟件
硬件相關,就會受很多因素的影響,這些都會影響到算法的效率。

但是我們總是要對於算法的效率進行度量,所以這個時候就想出來了一個辦法:
只考慮問題規模
考慮問題規模,只關注基本操作。那麼就會計算出一個值,這個值的大小最終能夠對於算法進行一個評定,並不是絕對的評定,但是是一個非常有效的辦法。

一般情況下,算法中基本操作重複執行的次數是問題規模n的某個函數f(n),
記作:T(n) = O(f(n)),T(n)稱爲漸進時間複雜度,它表示隨着問題規模n的增大,算法執行時間的增長率和f(n)函數的增長率相同,從數學意義上來講是一個同數量級的函數。所以說要分析一個算法的時間性能,我們首先要確定基本操作是什麼,然後計算出來基本操作重複執行的次數。是不是有點繞口,我們引入下面概念:

頻度:基本操作的執行次數。

時間複雜度:一般情況下只考慮問題的規模,算法中基本操作重複執行的次數是問題規模n的某個函數,那麼這個時候某個函數就放在O(f(n))的形式當中,這就是時間複雜度。

時間複雜度表示基本操作執行的次數隨着問題規模n的增大,趨近於一個函數f(n),那麼這裏的關鍵就是基本操作執行次數的計算上,我們換一種方式進行說明:
數學意義上的描述:

時間複雜度

所以計算漸進時間複雜度T(n)會用到頻度,計算頻度會用到基本操作執行的過程。那麼接下來我們通過實際操作進行計算:

例:

{++x; s=0;}	

基本操作的語句{++x; s=0;} 的執行次數與數據規模無關。
所以

P(n) = 1 f(n) = 1 T(n) = O(1)
包含 “x 增 1” 基本操作的語句的頻度爲1,即時間複雜度爲O(1)。O(1) 表示算法的運行時間爲常量。即:常量階。

如果P(n) = 10 和n無關只考慮問題規模,所以是一個常數項T(n) = O(1)。

例:

for(i =1; i <=n; ++i)    
{++x;  s += x;}

i = 1 基本操作的語句 {++x; s += x;} 執行1次
i = 2 基本操作的語句 {++x; s += x;} 執行1次
i = 3 基本操作的語句 {++x; s += x;} 執行1次
…………
i = n 基本操作的語句 {++x; s += x;} 執行1次

所以P(n) = n f(n) = n T(n) = O(n)

包含 “x 增 1” 基本操作的語句的頻度爲:n,其時間複雜度爲:O(n),即:線性階。

例:

for(i =1; i <= n;  ++i )
	for(j =1; j <= n; ++j )
		{++x;  s += x;}

i = 1 基本操作的語句 {++x; s += x;} 執行n次
i = 2 基本操作的語句 {++x; s += x;} 執行n次
i = 1 基本操作的語句 {++x; s += x;} 執行n次
i = 1 基本操作的語句 {++x; s += x;} 執行n次
i = 1 基本操作的語句 {++x; s += x;} 執行n次

所以P(n) = n^2 f(n) = n^2 T(n) = O(n^2)

包含 “x 增 1” 基本操作的語句的頻度爲:n2,其時間複雜度爲:O(n2),即:平方階。

那麼是不是很簡單呢,常量階,線性階,平方階。都是這樣嗎?是不是有什麼規律呢?二重循環就是O(n^2),一重循環就是O(n)。

那麼我們分析下面代碼的時間複雜度:
例:

for( i =2; i <= n; ++i )
	for( j =2; j <= i - 1; ++j )
		{++x;  a[ i, j]=x;}

i=2 j=2 2<=1 不滿足 基本操作的語句 {++x; a[ i, j]=x;} 不執行。
i=3 j=2 2<=2 滿足 基本操作的語句 {++x; a[ i, j]=x;} 執行1次。
i=4 j=2 2<=3 滿足 基本操作的語句 {++x; a[ i, j]=x;} 執行1次。
i=4 j=3 3<=3 滿足 基本操作的語句 {++x; a[ i, j]=x;} 執行1次。
所以 i = 4 基本操作的語句 {++x; a[ i, j]=x;} 執行2次。
i=5 j=2 2<=4 滿足 基本操作的語句 {++x; a[ i, j]=x;} 執行1次。
i=5 j=3 3<=4 滿足 基本操作的語句 {++x; a[ i, j]=x;} 執行1次。
i=5 j=4 4<=4 滿足 基本操作的語句 {++x; a[ i, j]=x;} 執行1次。
所以 i = 5 基本操作的語句 {++x; a[ i, j]=x;} 執行3次。

i=n j=2 ~n - 1 2 ~n - 1<=n - 1 基本操作的語句 {++x; a[ i, j]=x;} 執行n - 2次。

包含 “x 增 1” 基本操作的語句的頻度爲:
1+2+3+…+n-2 = (1+n-2)×(n-2)/2 = (n-1)(n-2)/2 是一個等差數列

等差數列運行結果爲:
等差數列求和

基本操作的 {++x; a[ i, j]=x;} 語句的頻度爲:n2,其時間複雜度爲:O(n2),即:平方階。

f(n)的求法

f(n)一般用頻度表達式中增長最快的項表示,並將其常數去掉。

例如: 假設某元操作的頻度:100 * 2^n + 8 * n * 2
T(n) = O(2^n)

時間複雜度會根據不同的算法來算出不同的值。所以同一種算法時間性能好不好,可以通過時間複雜度很形象的對比。

隨着問題規模的增加,算法的時間複雜度常見的有:
常數階 O(1),對數階 O(log n),線性階 O(n),
線性對數階 O(nlog n),平方階 O(n2),立方階 O(n3),…,
k 次方階O(nk),指數階 O(2n),階乘階 O(n!)。

常見的算法的時間 複雜度之間的關係爲:
O(1)<O(log n)<O(n)<O(nlog n)<O(n2)<O(2n)<O(n!)<O(nn)

當 n 很大時,指數階算法和多項式階算法在所需時間上非常懸殊。因此,只要有人能將現有指數階算法中的任何一個算法化簡爲多項式階算法,那就取得了一個偉大的成就。

常見函數的增長率

常見函數的增長率

當n比較小的時候,它們之間並沒有必然的規律。但是當問題規模n比較大的時候,上面函數增長率的曲線在同一個n值的情況下,它們T(n)會有很大的區別,並函數的增長線不會有交叉,都是絕對的。這也就是爲什麼數據結構研究算法,分析算法的時候數據規模不能太小。數據規模太小,算法的好壞不容易衡量。

時間複雜度的三種具體情況

void bubble-sort(int a[]int n) 
 {   //將 a 中整數序列重新排列成自小至大有序的整數序列。 
      for(i = n-1, change = TURE; i >= 1 && change; --i)
            change = false;
             for ( j = 0;  j < i; ++j)
                 if (a[ j] > a[ j +1]) {a[ j]←→a[ j +1];  change = TURE} 
 }// bubble-sort

時間複雜度分析:
如果原本是有序的,那麼時間複雜度就是最好的情況:0次
時間複雜度最壞的情況:1+2+3+…+n-1=n(n-1)/2
平均時間複雜度爲:O(n2)

我們在後面討論的所有時間複雜度,均指最壞的時間複雜度。因爲我算出來最壞情況的時間複雜度,所有情況下,只能和它一樣或者比它更好,不能比它更壞,這是一個底線。

空間複雜度

程序代碼本身所佔空間對不同算法通常不會有數量級之差別,因此在比較算法時可以不加考慮;算法的輸入數據量和問題規模有關,若輸入數據所佔空間只取決於問題本身,和算法
無關,則在比較算法時也可以不加考慮;由此只需要分析除輸入和程序之外的額外空間。

算法的空間複雜度

那麼算法的空間複雜度及就是算法在運行過程中臨時佔用的存儲空間 。
記作 S(n) = O(f(n))
其中 n 爲問題的規模。

若所需臨時空間不隨問題規模的大小而改變,也就是與問題規模無關,則稱此算法爲原地工作,記作O(1)。

若所需存儲量依賴於數據的規模,則通常按最壞情況考慮。

分析空間複雜度:

float abc ( float a, float b, float c )
{
	 return a + b + b * c;
}

上面運行的代碼只需要佔用固定的臨時空間用來存放return的數據,與問題規模n無關,所以S(n) = O(1) 也就是原地工作。

float sum ( float list [ ], int n )
{
	float tempsum = 0;
	for( int i = 0; i < n; i++ )	   tempsum += list[ i ];
	return tempsum;
}

上面運行的代碼只需要佔用固定的臨時空間用來存放tempsum的累加結果,與問題規模n無關,所以S(n) = O(1) 也就是原地工作。

float rsum ( float list [ ], int n )
{
	if(n == 0) return 0;
	 return rsum( list, n-1 ) + list[ n-1 ];
}

上面提供的是一個自己調用自己的遞歸問題。

如果要解決問題規模爲n 必須解決 n - 1,函數調用,所以繼續調用臨時空間來保存現場。
如果要解決問題規模爲n-1 必須解決 n - 2函數調用,所以繼續調用臨時空間來保存現場。

如果要解決問題規模爲n-2 必須解決 n - 3函數調用,所以繼續調用臨時空間來保存現場。
…………
直到解決問題規模 n 爲 0。

問題規模爲n,需要多次調用臨時空間來保存現場,所以空間複雜度S(n) = O(n)。

小結

緒論是爲以後其他數據結構的內容作基本知識的準備。

小結

思維導圖總結

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