第二章、算法基础 -- 设计算法

分治法

分治法:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。

分治模式在每层递归时都有三个步骤:

  1. 分解原问题为若干个子问题,这些子问题是原问题的规模较小的实例。
  2. 解决这些子问题,递归地求解各个子问题。然而,若子问题的规模足够小,则直接求解。
  3. 合并这些子问题的解成原问题的解。

归并排序

归并排序完全遵循分治模式:

  1. 分解:分解待排序的n个元素的序列成各具n/2n/2个元素的两个子序列。
  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)

练习题

第二章、算法基础 -- 设计算法
答:略
第二章、算法基础 -- 设计算法

答:上面的java代码就是这样实现。

第二章、算法基础 -- 设计算法
答:
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}
k=nk=n时,T(n)=1+n2T(n)=1+n^2,所以递归实现的插入排序的时间复杂度为Θ(n2)\Theta(n^2)

java实现:

	public static void sort(int[] a, int tail) {
		if (tail > 0) {
			sort(a, tail - 1);
			int key = a[tail];
			int j = tail - 1;
			for (; j >= 0 && key < a[j]; j--) {
				a[j + 1] = a[j];
			}
			a[j + 1] = key;
		}
	}

第二章、算法基础 -- 设计算法

答:

伪代码:

search(a, lo, hi, k)
if lo > hi 
	return -1
int mi = (lo + hi) / 2
if k < a[mi] 
	return search(a, lo, mi - 1, k);
else if k > a[mi]
	return search(a, mi + 1, hi, k);
else 
	return mi;

java实现:

public static int search(int[] a, int k) {
	return search(a, 0, a.length - 1, k);
}

private static int search(int[] a, int lo, int hi, int k) {
	if (lo > hi) return -1;
	int mi = (lo + hi) / 2;
	if (k < a[mi]) {
		return search(a, lo, mi - 1, k);
	} else if (k > a[mi]) {
		return search(a, mi + 1, hi, k);
	} else {
		return mi;
	}
}

递归二分查找的递归式:

Tn={Θ(1)n=1T(n/2)+Θ(1)n&gt;1T_{n}=\begin{cases}\Theta(1)&amp; \text{若}n = 1 \\T(n/2)+\Theta(1)&amp; \text{若}n &gt; 1\end{cases}

求解该递归式:
T(n)=T(n/2)+1=T(n/4)+2=T(n/8)+3=......=T(n/2k)+k \begin{aligned} T(n)= &amp;T(n/2) + 1\\ = &amp;T(n/4) + 2\\ = &amp;T(n/8) + 3\\ = &amp;... ...\\ = &amp;T(n/2^k) + k \end{aligned}
k=log2nk = log_2n时,T(n)=T(1)+log2nT(n) = T(1) + log_2n,所以二分查找的时间复杂度为Θ(lgn)\Theta(lgn)

第二章、算法基础 -- 设计算法

答:不行,虽然本来需要Θ(n)\Theta(n)时间去发现元素在已排序的子数组中的位置,使用二分查找可以把该时间缩短至Θ(lgn)\Theta(lgn),但是元素移动需要的时间是不变的,也就是Θ(n)\Theta(n),所以时间复杂度依旧为Θ(n2)\Theta(n^2)

第二章、算法基础 -- 设计算法

答:先使用Θ(nlog2n)\Theta(nlog_2n)的排序算法对集合进行排序,这需要Θ(nlog2n)\Theta(nlog_2n)的时间。然后循环排序后的集合,在迭代中使用xx减去当前循环的数得到yy,然后使用二分查找在剩下的元素中查找y,如若找到则S中存在两个其和为x的元素,如若找不到,则进行下个迭代,直到循环结束,则S中不存在两个其和为x的元素。二分查找需要Θ(log2n)\Theta(log_2n),进行nn次循环需要Θ(nlog2n)\Theta(nlog_2n)的时间,所以总时间是要Θ(nlog2n)\Theta(nlog_2n)