1. 程式人生 > >第二章、演算法基礎 -- 設計演算法

第二章、演算法基礎 -- 設計演算法

分治法

分治法:將原問題分解為幾個規模較小但類似於原問題的子問題,遞迴地求解這些子問題,然後再合併這些子問題的解來建立原問題的解。

分治模式在每層遞迴時都有三個步驟:

  1. 分解原問題為若干個子問題,這些子問題是原問題的規模較小的例項。
  2. 解決這些子問題,遞迴地求解各個子問題。然而,若子問題的規模足夠小,則直接求解。
  3. 合併這些子問題的解成原問題的解。

歸併排序

歸併排序完全遵循分治模式:

  1. 分解:分解待排序的n個元素的序列成各具n/2n/2個元素的兩個子序列。
  2. 解決:使用歸併排序遞迴地排序兩個子序列。
  3. 合併:合併兩個已排序的子序列以產生答案。

下圖是歸併排序排序一個數組的過程:
圖1

先自頂向下分解問題,再自底向上解決問題。

使用一個方法來合併已排序的子序列。虛擬碼:
圖2
圖3
java程式碼實現:

	private static void merge(int[] a, int p, int q, int r) {
		int[] left = new int[q - p + 1];
		int[] right = new int[r - q];
		
		for (int i = 0; i < left.length; i++) {
			left[i] = a[p + i];
		}
		
		for (int j = 0; j < right.length; j++) {
			right[j] = a[q + j + 1];
		}
		int i = 0, j = 0;
		while (p <= r) {
			if (left[i] <= right[j]) {
				a[p++] = left[i++];
				if (i == left.length) {
					for (;j < right.length; j++) {
						a[p++] = right[j];
					}
					break;
				}
			} else {
				a[p++] = right[j++];
				if (j == right.length) {
					for (;i < left.length; i++) {
						a[p++] = left[i];
					}
					break;
				}
			}
		}
	}

接下來把問題分解成子問題並遞迴呼叫演算法:

	public static void sort(int[] a) {
		sort(a, 0, a.length - 1);
	}
	
	public static void sort(int[] a, int p, int r) {
		if (p < r) {
			int q = (p + r) / 2;
			sort(a, p, q);
			sort(a, q + 1, r);
	        if (a[q] <= a[q + 1]) {
	            return;
	        }
			merge(a, p, q, r);
		}
	}

分析分治演算法

當一個演算法包含對其自身的遞迴呼叫時,我們往往可以用遞迴方程或遞迴式來描述其執行時間,該方程根據在較少輸入上的執行時間來描述在規模為n的問題上的總執行時間。

分治演算法執行時間的遞迴式來自基本模式的三個步驟。若問題規模足夠小,如對某個常量ccncn \leq c,則直接求解需要常量時間。我們將其寫作Θ(1)\Theta(1)。假設把原問題分解成aa個子問題,每個子問題的規模是原問題的1/b1/b。為了求解一個規模為
n/bn/b的子問題,需要T(n/b)T(n/b),所以需要aT(n/b)aT(n/b)的時間來求解aa個子問題。如果分解問題成子問題需要時間D(n)D(n),合併子問題的解為原問題的解需要時間C(n)C(n),那麼得到遞迴式:

Tn={Θ(1)ncaT(n/b)+D(n)+C(n)其他T_{n}=\begin{cases}\Theta(1)&amp; \text{若}n \leq c \\aT(n/b)+D(n)+C(n)&amp; \text{其他}\end{cases}

歸併排序演算法的分析

在歸併排序中,根據分治模式進行分析:

  1. 分解:分解步驟僅僅計運算元陣列的中間位置,需要常量時間,因此D(n)=Θ(1)D(n)=\Theta(1)
  2. 解決:我們遞迴地求解兩個規模均為n/2的子問題,將需要2T(n/2)2T(n/2)的執行時間。
  3. 合併:merge方法需要Θ(n)\Theta(n)的時間,所以C(n)=Θ(n)C(n)=\Theta(n)

D(n)D(n)C(n)C(n)相加後依舊是一個線性函式,即Θ(n)\Theta(n),相當於忽略掉每次分解需要的常數時間,給出歸併排序的最壞情況執行時間T(n)T(n)的遞迴式:
Tn={Θ(1)n=12T(n/2)+Θ(n)n&gt;1T_{n}=\begin{cases}\Theta(1)&amp; \text{若}n = 1 \\2T(n/2)+\Theta(n)&amp; \text{若}n &gt; 1\end{cases}

根據該遞迴式,可以得出歸併排序演算法的時間複雜度為Θ(nlog2n)\Theta(nlog_2n),其推導過程如下:
T(n)=2T(n/2)+n=2(2T(n/4)+n/2)+n=4(2T(n/8)+n/4)+2n=......=2kT(n/2k)+kn\begin{aligned} T(n)=&amp; 2*T(n/2) + n\\ =&amp; 2*(2*T(n/4) + n/2) + n\\ =&amp; 4*(2*T(n/8) + n/4) + 2n\\ =&amp; ... ...\\ =&amp; 2^k * T(n/2^k) + k*n \end{aligned}

可知T(1)=Θ(1)T(1) = \Theta(1) ,那麼T(n/2log2n)=Θ(1)T(n/2^{log_2n}) = \Theta(1),當k=log2nk=log_2n時,
T(n)=n+nlog2nT(n) = n + nlog_2n
所以歸併排序演算法的時間複雜度為Θ(nlog2n)\Theta(nlog_2n)

練習題

圖3
答:略
圖4

答:上面的java程式碼就是這樣實現。

圖5
答:
n=2n=2時,T(2)=2log22=2T(2) = 2 log_22 = 2,結論成立。
n=2kn=2^k時,我們假設結論成立,即T(2k)=2klog22kT(2^k)=2^klog_22^k
如果n=2k+1n=2^{k+1}時,結論也成立,那麼遞迴式的解就是T(n)=nlog2nT(n)=nlog_2n
根據遞迴式:
T(2k+1)=2T(2k)+22k=2(2klog22k)+22k=2k+1log22k+2k+1=2k+1(log22k+log22)=2k+1log22k+1 \begin {aligned} T(2^{k+1})= &amp;2*T(2^k)+2*2^k\\ = &amp;2(2^klog_22^k) + 2*2^k\\ = &amp;2^{k+1}log_22^k + 2^{k+1}\\ = &amp;2^{k+1}(log_22^k + log_22)\\ = &amp;2^{k+1}log_22^{k+1} \end {aligned}
也就是當T(2k)T(2^k)成立時,T(2k+1)T(2^{k+1})也成立。所以可以得出結論T(n)=nlog2nT(n)=nlog_2n成立。

在這裡插入圖片描述

答:
Tn={Θ(1)n=1T(n1)+Θ(n)n&gt;1T_{n}=\begin{cases}\Theta(1)&amp; \text{若}n = 1 \\T(n-1)+\Theta(n)&amp; \text{若}n &gt; 1\end{cases}

計算其時間複雜度:
T(n)=T(n1)+n=T(n2)+2n=T(n3)+3n=......=T(nk)+kn \begin{aligned} T(n)= &amp;T(n-1) + n\\ = &amp;T(n-2) + 2n\\ = &amp;T(n-3) + 3n\\ = &amp;... ...\\ = &amp;T(n-k) + k*n \end{aligned}