數據結構與算法:算法分析

1.數學基礎

Θ,讀音:theta、西塔;既是上界也是下界(tight),等於的意思。算法

Ο,讀音:big-oh、歐米可榮(大寫);表示上界(tightness unknown),小於等於的意思。數組

ο,讀音:small-oh、歐米可榮(小寫);表示上界(not tight),小於的意思。架構

Ω,讀音:big omega、歐米伽(大寫);表示下界(tightness unknown),大於等於的意思。函數

ω,讀音:small omega、歐米伽(小寫);表示下界(not tight),大於的意思。優化

定義:this

這裏比較的是函數的相對增加率。spa

2.模型

爲了在正式架構中分析算法,咱們須要一個計算模型。咱們假設在計算機中全部指令程序都是被順序的執行,且每個執行都恰好花費一個時間單位(加法、乘法、賦值等)。code

3.要分析的問題

算法要分析的最重要資源就是運行時間,運行時間是衡量一個算法優劣的有效方式。blog

4.運行時間計算

咱們經過一個簡單的例子說明分析算法運行時間表示排序

4.1.實例

其中

17行:1個時間單元(賦值)

18行:2n+1個時間單元(一個賦值,n次表,n次自增)

19行:4n個時間單元(2n次乘法,n次加法,n次賦值)

20行:1個時間單元

總時間單元6n+3,因此咱們說該算法是O(N)。

4.2.法則

for循環

一個for循環的運行時間至可能是該for循環內部運行語句的時間乘以迭代次數。

嵌套for循環

在一組嵌套for循環內部一條語句運行的總時間爲該語句的運行時間乘以改組全部for循環大小的成績。

for (int i = 0; i < n; i++)
			for (int j = 0; j < n; j++)
				k++;

順序語句

將各個語句運行時間求和。

for (int i = 0; i < n; i++)
			k++;

		for (int i = 0; i < n; i++)
			for (int j = 0; j < n; j++)
				k++;

if/else語句

分析算法運行時間的基本策略就是從內部或最深層的部分向外界展開工做。若是有方法調用,那麼要先分析這些調用。若是有遞歸調用,那麼有幾種選擇。

第一種:遞歸只是被面紗遮住的for循環,其運行時間一般都是O(N):

public static int factorial(int n) {
		if (n <= 1) {
			return 1;
		} else {
			return n * factorial(n - 1);
		}
	}

本質上就是一個循環

int n = 1;
		
		for (int i = 1; i <= 5; i++) {
			n *= i;
		}

第二種:當遞歸被正常使用時,將其轉換成循環是至關困難的。

public static int fib(int n) {
1		if (n <= 1)
2			return 1;
3		else
4			return fib(n - 1) + fib(n - 1);
	}

分析起來是十分簡單的,令T(N)爲調用fib(n)的運行時間。若是N=0或N=1則運行時間T(N)爲常數T(0)=T(1)=1。當N>2時,運行時間爲第1行運行時間加上第4行運行時間。有T(N)爲fib(n)運行時間則T(N-1)爲fib(n-1)運行時間。得出N>2時運行時間公式:T(N) = T(N-1)+T(N-1)+2

由前面咱們證實過的斐波那契數列可得

從而得知這個程序的運行時間以指數速度增加。

這個程序之因此運行慢,是因爲作了大量重複工做,如在程序第4行計算f(n-1)和f(n-2)的值,其實在f(n-1)函數內部又會從新計算f(n-2)和f(n-3)的值,這樣就致使f(n-2)進行了重複計算。因此可使用一個數組來保存中間計算的值。省去這部分計算。之後會對該遞歸進行優化。

5.最大子序列問題

問題:給定一個數列,其中可能有正數也可能有負數,咱們的任務是找出其中連續的一個子數列(不容許空序列),使它們的和儘量大。

咱們經過4種算法求解來比較不一樣算法的優劣。

第一種:窮舉全部子序列的可能,時間複雜度爲

public static int maxSubSum1(int[] a) {
		int maxSum = 0;
		for (int i = 0; i < a.length; i++) {
			for (int j = i; j < a.length; j++) {
				int thisSum = 0;
				for (int k = i; k < j; k++) {
					thisSum += a[k];
				}
				if (thisSum > maxSum) {
					maxSum = thisSum;
				}
			}
		}
		return maxSum;
	}

第二種:通過改良版時間複雜度

public static int maxSubSum2(int[] a) {
		int maxSum = 0;
		for (int i = 0; i < a.length; i++) {
			int thisSum = 0;
			for (int j = i; j < a.length; j++) {
				thisSum += a[j];
				if (thisSum > maxSum) {
					maxSum = thisSum;
				}
			}
		}
		return maxSum;
	}

第三種:該方法採用「分治」策略,將系列分爲兩部分,前半部分與後半部分,分析最大子序列出現的可能性就又三種,第一種出如今前半部分,第二種出如今後半部分,還有一種就是橫跨前半部分和後半部分。

public static int maxSubSumRec(int[] a, int left, int right) {

		if (left == right) {  //基準狀況
			if (a[left] > 0) {
				return a[left];
			} else {
				return 0;
			}
		}
		
		int center = (left + right)/2;
		int maxLeftSum = maxSubSumRec(a, left, center);
		int maxRightSum = maxSubSumRec(a, center, 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 i=center;i<right;i++) { //從中間向右遍歷
			rightBorderSum += a[i];
			if(rightBorderSum>maxRightBorderSum) {
				maxRightBorderSum = rightBorderSum;
			}
		}
		//求最大值
		return max(maxLeftSum, maxRightSum,maxLeftBorderSum+maxRightBorderSum);
	}

設執行該方法的時間爲T(N);

方法前面部分執行判斷,時間爲常量。

中間部分執行兩個遞歸調用,每一個遞歸執行N的通常的計算執行時間爲T(N/2)。

再後面兩個循環,一共遍歷N數量,執行時間爲O(N)。

則獲得方程組:T(1) = 1 和  T(N) = 2T(N/2) + O(N)

這個分析的假設是N爲2的冪時成立。

第四種:這個算法是聰明的,正確性不那麼容易看出來。

public static int maxSubSum4(int[] a ) {
		int maxSum = 0, thisSum = 0;
		for(int i=0;i<a.length;i++) {
			thisSum += a[i];
			if(thisSum > maxSum) {
				maxSum = thisSum;
			}else if(thisSum <0){
				thisSum = 0;
			}
		}
		return maxSum;
	}

比較其中的每個子序列和,當子序列和爲負數時初始化爲0從新計算。

6.運行時間中對數

分析算法運行時間最混亂的方面大概就是對數問題。咱們已經看到分治算法將以對數時間運行O(N log N)。

此外還有可能出現對數的狀況:當經過常數時間的計算能夠將問題大小縮減爲其一部分(一般1/2),那麼該算法就是O(log N)。

下面具體舉例三種時間複雜度爲對數的狀況。

折半查找

算法實現策略:咱們能夠經過驗證X是不是劇中元素,若是是則找到;若是X小於劇中元素由於系列已經排序,咱們能夠經過相同的方式驗證左側系列;若是X大於劇中元素則一樣的方式驗證右側系列。

public static int binarySearch(int[] a,int x) {
		
		int low =0, high = a.length-1;
		while(low <= high) {
			int mid = (low + high)/2;
			if(a[mid] < x) {
				low = mid+1;
			}else if(a[mid] > x) {
				high = mid -1;
			}else {
				return a[mid];
			}
		
		}
		return -1;
	}

歐裏幾德算法

第二個例子是計算最大公因數的歐裏幾德算法。兩個整數的最大公因數(gcd)是同時整除兩者的最大整數。

定理:兩個整數的最大公約數等於其中較小的那個數和兩數相除餘數的最大公約數。

public static long gcd(long m,long n) {
		while(n != 0) {
			
			long rem = m % n; //算法中首先m要大於n,然後求餘數,將較小的替換爲大數,餘數替換爲小數。知道餘數爲0爲止。
			m = n; 
			n = rem;
		}
		return m;
	}

冪運算

注意:該遞歸的推動的原則就是n逐漸減少,向n=1或n=0推動。

//求解x的n次冪
	public static long pow(long x,int n) {
		if(n == 0) {
			return 1;
		}
		if(n == 1) {
			return x;
		}
		if(isEven(n)) {//n爲偶數時
			return pow(x*x,n/2);  //該遞歸推動的原則就是n中間減少,向0或1基準情形靠近
		}else {
			return pow(x*x,n/2)*x;
		}
	}
相關文章
相關標籤/搜索