第一章 算法在计算中的作用
生活中算法的例子:
- 最短路径:交通图中从一个路口到另一个路口可能存在多条路线,寻找出路径最短的路线
- 最长公共子序列:判定两个DNA链的基对序列是否相似,每个序列有 2n个子序列,求最长公共子序列
- 凸壳:木板上有若干个钉子,凸壳由一根拉紧的环绕所有钉子的橡皮筋来表示,如果橡皮筋因绕过某颗钉子而转弯,那么这颗钉子就是凸壳的一个顶点,寻求最小的凸壳面积
-
拓扑排序:给出若干个决策方案,我们要对方案进行排序,使得每个方案出现依赖它的方案之前,总共有 n! 种方案。
第二章 算法基础
2.1 插入排序
在打扑克的场景中,考虑当前摸了第 i 张牌,而手上已经排好序的牌为 j = 1...i − 1,此时通过将第 i 张牌倒着与手上已排好序的牌进行比较,来确定当前牌要插入的位置,代码如下:
for (int i = 1; i < n; ++i) { // 当前处理第i张牌
int key = a[i];
int j = i - 1; // 从第 j = i - 1 张牌开始比较
while(j >= 0 && a[j] > key) { // 若第 j 张牌比当前牌的值更大,则继续往前寻找要插入的位置
a[j + 1] = a[j];
--j;
}
a[j + 1] = key;
}
2.2 分析算法
算法需要的时间与输入的规模同步增长,对长度为1,000的数组排序与对长度为100,000的数组进行排序所需的时间明显是不一样的,所以通常将程序的运行时间描述成其输入规模的函数。
算法的运行时间可以简单看做每条语句的运行时间之和,对于上述的插入排序来说:
-
最好的情况是数组已经排好序,则 第 4-7行的 while 循环只需执行一次,时间复杂度为θ(n)
-
最坏情况为数组完全逆序,此时对于每个 i 来说第4 - 7行while循环要执行 i - 1次,忽略常数项以及低阶项,时间复杂度为
-
往往考虑最坏的情况,平均情况通常情况下增长量级和最坏情况是一样的
2.3 分治算法
将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,再合并这些子问题的解来得到原问题的解。
归并排序算法:
- 分解:将n个元素的排列分解为 n / 2个元素的子序列排列
- 解决:递归解决两个子序列的排序
- 合并:合并两个已排序的子序列得到原序列的解
// 将区间 [p, r] 分解为 [p, q], [q + 1, r], 其中 q = (p + r) / 2
[p, q]
[q + 1, r]
merge(A, p, q, r) {
n1 = q - p + 1;
n2 = r - q;
令 L = A[p ... q],R = A[q + 1 .. r]
int k = p;
for (int i = 0, j = 0; i < n1 || j < n2; ) {
if ( (j >= n2 || (i < n1 && L[i] <= R[j]) { // 若R数组已经被遍历完或R数组中的元素大于L数组中的元素
A[k++] = L[i++];
} else {
A[k++] = R[j++];
}
}
}
分治算法的时间复杂度计算:
T(n)=aT(n/b) + D(n) + C(n),其中a表示分解成a个子问题,T(n/b)表示每个规模为 n/b的子问题的求解时间,D(n)表示分解成子问题所需的时间,C(n)表示合并成原问题所需的时间。
将该公式应用到归并排序中:
子问题规模为 n/2,共有2个子问题,合并的时间复杂度C(n)为θ(n),得到 T(n)=2∗T(n/2) + θ(n),可得 T(n)=θ(nlgn)。
逆序对
逆序对的定义为,对于序列 A={a1, a2, ..., an} 来说,存在 i > j 且 ,则可以说存在一组逆序对 (i, j)。
插入排序和逆序对的关系:对于每个处理的数 a[i]来说,它前面有多少个逆序对,2.1节中算法的 while 循环就要执行多少次。
归并排序和逆序对的关系:可以通过归并排序在 θ(nlogn)的时间复杂度内求出逆序对的数量
Horner rule 霍纳规则
霍纳规则用于在线性时间内计算多项式 的值,这个式子展开得到
给定数组a以及x,可以写成如下代码:
for (int i = n; i >= 0; --i) {
y = a[i] + x * y;
}
欢迎关注,持续更新中 ...