第二章、算法基础 -- 设计算法
分治法
分治法:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。
分治模式在每层递归时都有三个步骤:
- 分解原问题为若干个子问题,这些子问题是原问题的规模较小的实例。
- 解决这些子问题,递归地求解各个子问题。然而,若子问题的规模足够小,则直接求解。
- 合并这些子问题的解成原问题的解。
归并排序
归并排序完全遵循分治模式:
- 分解:分解待排序的n个元素的序列成各具个元素的两个子序列。
- 解决:使用归并排序递归地排序两个子序列。
- 合并:合并两个已排序的子序列以产生答案。
下图是归并排序排序一个数组的过程:
先自顶向下分解问题,再自底向上解决问题。
使用一个方法来合并已排序的子序列。伪代码:
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的问题上的总运行时间。
分治算法运行时间的递归式来自基本模式的三个步骤。若问题规模足够小,如对某个常量,,则直接求解需要常量时间。我们将其写作。假设把原问题分解成个子问题,每个子问题的规模是原问题的。为了求解一个规模为
的子问题,需要,所以需要的时间来求解个子问题。如果分解问题成子问题需要时间,合并子问题的解为原问题的解需要时间,那么得到递归式:
归并排序算法的分析
在归并排序中,根据分治模式进行分析:
- 分解:分解步骤仅仅计算子数组的中间位置,需要常量时间,因此。
- 解决:我们递归地求解两个规模均为n/2的子问题,将需要的运行时间。
- 合并:merge方法需要的时间,所以。
把跟相加后依旧是一个线性函数,即,相当于忽略掉每次分解需要的常数时间,给出归并排序的最坏情况运行时间的递归式:
根据该递归式,可以得出归并排序算法的时间复杂度为,其推导过程如下:
可知 ,那么,当时,
所以归并排序算法的时间复杂度为。
练习题
答:略
答:上面的java代码就是这样实现。
答:
当时,,结论成立。
当时,我们假设结论成立,即。
如果时,结论也成立,那么递归式的解就是。
根据递归式:
也就是当成立时,也成立。所以可以得出结论成立。
答:
计算其时间复杂度:
当时,,所以递归实现的插入排序的时间复杂度为。
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;
}
}
递归二分查找的递归式:
求解该递归式:
当时,,所以二分查找的时间复杂度为。
答:不行,虽然本来需要时间去发现元素在已排序的子数组中的位置,使用二分查找可以把该时间缩短至,但是元素移动需要的时间是不变的,也就是,所以时间复杂度依旧为。
答:先使用的排序算法对集合进行排序,这需要的时间。然后循环排序后的集合,在迭代中使用减去当前循环的数得到,然后使用二分查找在剩下的元素中查找y,如若找到则S中存在两个其和为x的元素,如若找不到,则进行下个迭代,直到循环结束,则S中不存在两个其和为x的元素。二分查找需要,进行次循环需要的时间,所以总时间是要。