Java算法從入門到精通(一)

認識時間複雜度

    常數時間的操作:一個操作如果和數據量沒有關係,每次都是 固定時間內完成的操作,叫做常數操作。

    時間複雜度爲一個算法流程中,在最差的數據情況下,常數操作數量的指標。常用O (讀作big O)來表示。具體來說,在常數操作數量的表達式中, 只要高階項,不要低階項,也不要高階項的係數,剩下的部分 如果記爲f(N),那麼時間複雜度爲O(f(N))。

   評價一個算法流程的好壞,先看時間複雜度的指標,然後再分 析不同數據樣本下的實際運行時間,也就是常數項時間。


冒泡排序細節的講解與複雜度分析

時間複雜度O(N^2),額外空間複雜度O(1)

代碼實現:

 public static void bubbleSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		for (int e = arr.length - 1; e > 0; e--) {
			for (int i = 0; i < e; i++) {
				if (arr[i] > arr[i + 1]) {
					swap(arr, i, i + 1);
				}
			}
		}
	}

 public static void swap(int[] arr, int i, int j) {
		arr[i] = arr[i] ^ arr[j];
		arr[j] = arr[i] ^ arr[j];
		arr[i] = arr[i] ^ arr[j];
	}

選擇排序的細節講解與複雜度分析

時間複雜度O(N^2),額外空間複雜度O(1)

代碼實現:

public static void selectionSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		for (int i = 0; i < arr.length - 1; i++) {
			int minIndex = i;
			for (int j = i + 1; j < arr.length; j++) {
				minIndex = arr[j] < arr[minIndex] ? j : minIndex;
			}
			swap(arr, i, minIndex);
		}
	}

public static void swap(int[] arr, int i, int j) {
		int tmp = arr[i];
		arr[i] = arr[j];
		arr[j] = tmp;
	}

插入排序的細節講解與複雜度分析

時間複雜度O(N^2),額外空間複雜度O(1)

代碼實現:

public static void insertionSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		for (int i = 1; i < arr.length; i++) {
			for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
				swap(arr, j, j + 1);
			}
		}
	}

public static void swap(int[] arr, int i, int j) {
		arr[i] = arr[i] ^ arr[j];
		arr[j] = arr[i] ^ arr[j];
		arr[i] = arr[i] ^ arr[j];
	}

對數器的概念和使用

  1. 有一個你想要測的方法a;
  2. 實現一個絕對正確但是複雜度不好的方法b;
  3. 實現一個隨機樣本產生器 ;
  4. 實現比對的方法 ;
  5. 把方法a和方法b比對很多次來驗證方法a是否正確;
  6. 如果有一個樣本使得比對出錯,打印樣本分析是哪個方法出錯 ;
  7. 當樣本數量很多時比對測試依然正確,可以確定方法a已經 正確。

我們以冒泡排序爲例

代碼如下:

import java.util.Arrays;

public class BubbleSort {

	public static void bubbleSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		for (int e = arr.length - 1; e > 0; e--) {
			for (int i = 0; i < e; i++) {
				if (arr[i] > arr[i + 1]) {
					swap(arr, i, i + 1);
				}
			}
		}
	}

        public static void swap(int[] arr, int i, int j) {
		arr[i] = arr[i] ^ arr[j];
		arr[j] = arr[i] ^ arr[j];
		arr[i] = arr[i] ^ arr[j];
	}

	// for test
	public static void comparator(int[] arr) {
		Arrays.sort(arr);
	}

	// for test
	public static int[] generateRandomArray(int maxSize, int maxValue) {
		int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
		for (int i = 0; i < arr.length; i++) {
			arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
		}
		return arr;
	}

	// for test
	public static int[] copyArray(int[] arr) {
		if (arr == null) {
			return null;
		}
		int[] res = new int[arr.length];
		for (int i = 0; i < arr.length; i++) {
			res[i] = arr[i];
		}
		return res;
	}

	// for test
	public static boolean isEqual(int[] arr1, int[] arr2) {
		if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
			return false;
		}
		if (arr1 == null && arr2 == null) {
			return true;
		}
		if (arr1.length != arr2.length) {
			return false;
		}
		for (int i = 0; i < arr1.length; i++) {
			if (arr1[i] != arr2[i]) {
				return false;
			}
		}
		return true;
	}

	// for test
	public static void printArray(int[] arr) {
		if (arr == null) {
			return;
		}
		for (int i = 0; i < arr.length; i++) {
			System.out.print(arr[i] + " ");
		}
		System.out.println();
	}

	// for test
	public static void main(String[] args) {
		int testTime = 500000;
		int maxSize = 100;
		int maxValue = 100;
		boolean succeed = true;
		for (int i = 0; i < testTime; i++) {
			int[] arr1 = generateRandomArray(maxSize, maxValue);
			int[] arr2 = copyArray(arr1);
			bubbleSort(arr1);
			comparator(arr2);
			if (!isEqual(arr1, arr2)) {
				succeed = false;
				break;
			}
		}
		System.out.println(succeed ? "Nice!" : "Fucking fucked!");

		int[] arr = generateRandomArray(maxSize, maxValue);
		printArray(arr);
		bubbleSort(arr);
		printArray(arr);
	}

}

算法的複雜度與Master定理

平時設計或者閱讀一個算法的時候,必然會提到算法的複雜度(包括時間複雜度和空間複雜度)。比如我們說一個二分查找算法的平均時間複雜度爲O(log n),快速排序可能是O(n log n)。那這裏的O是什麼意思?這樣的表達是否準確呢?

今天來複習一下與算法複雜度相關的知識:函數漸進階,記號O、Ω、θ和o;Master定理。

先插一句,在算法複雜度分析中,log通常表示以2爲底的對數。

算法複雜度(算法複雜性)是用來衡量算法運行所需要的計算機資源(時間、空間)的量。通常我們利用漸進性態來描述算法的複雜度。

用n表示問題的規模,T(n)表示某個給定算法的複雜度。所謂漸進性態就是令n→∞時,T(n)中增長最快的那部分。嚴格的定義是:如果存在T˜(n)T~(n),當n→∞時,有

 

比如T(n) = 2 * n ^ 2 + n log n + 3,那麼顯然它的漸進性態是 2 * n ^ 2,因爲當n→∞時,後兩項的增長速度要慢的多,可以忽略掉。引入漸進性態是爲了簡化算法複雜度的表達式,只考慮其中的主要因素。當比較兩個算法複雜度的時候,如果他們的漸進複雜度的階不相同,那只需要比較彼此的階(忽略常數係數)就可以了。

總之,分析算法複雜度的時候,並不用嚴格演算出一個具體的公式,而是只需要分析當問題規模充分大的時候,複雜度在漸進意義下的階。記號O、Ω、θ和o可以幫助我們瞭解函數漸進階的大小。

假設有兩個函數f(n)和g(n),都是定義在正整數集上的正函數。上述四個記號的含義分別是:

可見,記號O給出了函數f(n)在漸進意義下的上界(但不一定是最小的),相反,記號Ω給出的是下界(不一定是最大的)。如果上界與下界相同,表示f(n)和g(n)在漸進意義下是同階的(θ),亦即複雜度一樣。

列舉一些常見的函數之間的漸進階的關係:

有些人可能會把這幾個記號跟算法的最壞、最好、平均情況複雜度混淆,它們有區別,也有一定的聯繫。

即使問題的規模相同,隨着輸入數據本身屬性的不同,算法的處理時間也可能會不同。於是就有了最壞情況、最好情況和平均情況下算法複雜度的區別。它們從不同的角度反映了算法的效率,各有用處,也各有侷限。

有時候也可以利用最壞情況、最好情況下算法複雜度來粗略地估計算法的性能。比如某個算法在最壞情況下時間複雜度爲θ(n ^ 2),最好情況下爲θ(n),那這個算法的複雜度一定是O(n ^ 2)、Ω(n)的。也就是說n ^ 2是該算法複雜度的上界,n是其下界。

接下來看看Master定理。

有些算法在處理一個較大規模的問題時,往往會把問題拆分成幾個子問題,對其中的一個或多個問題遞歸地處理,並在分治之前或之後進行一些預處理、彙總處理。這時候我們可以得到關於這個算法複雜度的一個遞推方程,求解此方程便能得到算法的複雜度。其中很常見的一種遞推方程就是這樣的:

設常數a >= 1,b > 1,f(n)爲函數,T(n)爲非負整數,T(n) = a T(n / b) + f(n),則有:

比如常見的二分查找算法,時間複雜度的遞推方程爲T(n) = T(n / 2) + θ(1),顯然有nlogba=n0=Θ(1)nlogb⁡a=n0=Θ(1),滿足Master定理第二條,可以得到其時間複雜度爲T(n) = θ(log n)。

再看一個例子,T(n) = 9 T(n / 3) + n,可知nlogba=n2nlogb⁡a=n2,令ε取1,顯然滿足Master定理第一條,可以得到T(n) = θ(n ^ 2)。

來一個稍微複雜一點兒例子,T(n) = 3 T(n / 4) + n log n。nlogba=O(n0.793)nlogb⁡a=O(n0.793),取ε = 0.2,顯然當c = 3 / 4時,對於充分大的n可以滿足a * f(n / b) = 3 * (n / 4) * log(n / 4) <= (3 / 4) * n * log n = c * f(n),符合Master定理第三條,因此求得T(n) = θ(n log n)。

運用Master定理的時候,有一點一定要特別注意,就是第一條和第三條中的ε必須大於零。如果無法找到大於零的ε,就不能使用這兩條規則。

舉個例子,T(n) = 2 T(n / 2) + n log n。可知nlogba=n1nlogb⁡a=n1,而f(n) = n log n,顯然不滿足Master定理第二條。但對於第一條和第三條,也無法找到大於零的ε使得nlogn=O(n1−ε)nlog⁡n=O(n1−ε)或者nlogn=Ω(n1+ε)nlog⁡n=Ω(n1+ε),因此不能用Master定理求解,只能尋求別的方式求解。比如可以利用遞歸樹求出該算法的複雜度爲T(n)=O(nlog2n)T(n)=O(nlog2⁡n)。簡單的說一下計算過程:

遞歸樹的建立過程,就像是模擬算法的遞推過程。樹根對應的是輸入的規模爲n的問題,在遞歸處理子問題之外,還需要n log n的處理時間。然後根據遞推公式給根節點添加子節點,每個子節點對應一個子問題。這裏需要兩個子節點,每個節點處理規模爲n / 2的問題,分別需要(n / 2) * log(n / 2)的時間。因此在第二層一共需要n * (log n - 1)的時間。第三層節點就是將第二層的兩個節點繼續分裂開,得到四個各需要(n / 4) * log(n / 4)時間的節點,總的時間消耗爲n * (log n - 2)。依此類推,第k(設樹根爲k = 0)層有2 ^ k的節點,總的時間爲n * (log n - k)。而且可以知道,這棵樹總共有log n層(最後一層每個節點只處理規模爲1的子問題,無須再分治)。最後將每一層消耗的時間累加起來,得到:

 

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