算法导论第四版ch2中文笔记与插入排序,并归排序C++实现
作者:Claude Du
本文内容和图片基本来源于算法导论第四版第二章。
2.1.插入排序
插入排序算法思想:在已排序的子数组A[1:j-1]后,将单个元素A[j]插入子数组的适当位置,产生排序号的子数组A[1:j]。
其形式和我们打牌时洗牌的方式几乎一模一样, 如下图所示:
其C++代码实现:
// author: Claude Du
#include <iostream>
#include <vector>
using std::vector;
using std::unordered_map;
using std::pair;
class Solution {
public:
void insertionSort(vector<int>& nums) {
if (nums.size() <= 1) return;
for (int j = 1; j < nums.size(); ++j) {
int key = nums[j];
int i = j -1;
while (i >= 0 && nums[i] > key) {
nums[i + 1] = nums[i];
--i;
}
nums[i + 1] = key;
}
}
};
int main()
{
Solution sol;
vector<int> nums = {
3,2,1,2,3,4};
sol.insertionSort(nums);
for (int i = 0; i < nums.size(); ++i) {
std::cout << nums[i] << " ";
}
}
输出结果,排序成功
PS D:\WorkSpace> g++ -g insertion.cpp
PS D:\WorkSpace> ./a.exe
1 2 2 3 3 4
英文版第四版书中P19(中文版第三版书中P10)提到了循环不变式(loop invariant)的重要概念(后续章节会反复用到), 需要重点理解。
对于插入排序算法, 其循环不变式如下:
A loop invariant of insertion sort is shown here:
- at the beginning of each iteration of the for loop, which is indexed by j, the subarray nums[0: j-1] is sorted.
- at the beginning of each iteration of the for loop, which is indexed by j, the elements of the subarray nums[0: j-1] are the elements originally in positions 1 through j -1.
循环不变式可用来帮助我们证明算法的正确性。 当使用一个循环不变式时,我们要证明以下三条性质成立,即可证明算法的正确性:
- 初始化:在循环的第一次迭代前,该循环不变式为真
- 保持(maintenance):如果循环的某次迭代前,该循环不变式为真,那么下次迭代前,它依然为真。
- 终止(Termination):在循环终止时,该循环不变式提供给我们一个有用的性质,该性质可用于证明算法的正确性。
我们看看对于插入排序,以上三条性质是否成立:
-
初始化:在循环的第一次迭代前,i = 1, 子序列nums[0:0]只有一个元素nums[0], 循环不变式的两条性质都显然成立。
第一条得证
-
保持(maintenance):(非形式化的数学归纳法论证),如果循环的某次迭代前,该循环不变式为真,for循环体中将nums[j-1], nums[j-2],…nums[j-k +1]等都向右移动了一个位置(k满足nums[j-k +1] > nums[j] && nums[j-k ] <= nums[j] ),再在pos=j - k +1的位置上插入原来的nums[j], 此时nums[0: j]已完成排序,且nums[0:j]由原来nums[0:j]的元素组成,那么对于for循环下次迭代前,该循环不变式依然为真。
-
终止(Termination):在for循环终止时,j = nums.size(); 在该循环不变式中,我们将j用nums.size()替代, nums[0: nums.size() - 1]是已排序的,并且其中的所有元素都由原先的数组的元素组成, 因为此时数组已排序且所有元素都是该数组原来的元素,该算法是正确的。
这种循环不变式的方法会在本书后面的内容持续使用,必须通过刻意的练习掌握好!
2.2分析算法
该章节的具体内容还是看书吧,这里只写个提要。
本章先简要介绍了RAM模型,其中包含RAM模型的常见指令和代价,RAM模型的数据类型等信息,
之后对插入排序的复杂度进行分析, 该分析方法非常严谨,值得学习,得到插入排序最坏情况运行时间为Θ(n2)\Theta(n^2)Θ(n2) , 平均依然为Θ(n2)\Theta(n^2)Θ(n2)
2.3设计算法
之前的插入排序使用增量方法:在已排序的子数组A[1:j-1]后,将单个元素A[j]插入子数组的适当位置,产生排序号的子数组A[1:j]。
接下来看另一种分治法的设计方法,并用分治法的思想设计排序算法。
2.3.1分治法(The divide-and-conquer method)
分而治之法的思想:将原问题分解为几个规模较小单类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。
分治模式在内层递归有以下三个步骤:
- 分解 : 将原问题分解为若干子问题,这些子问题是原问题规模较小的实例。
- 解决:递归的求解个子问题。当子问题规模足够小,则直接求解。
- 合并:合并两个已排序的子序列以产生已排列的答案。
归并排序:
- 分解(divide):将待排序的子序列 $A[p:r] $ 分解成两个子相邻序列,每个分解出来的子序列的规模为原来的一半。为了完成该分解步骤,算出 A[p:r]A[p:r]A[p:r] 的中间索引q,q=⌊(p+r)/2⌋q = \lfloor (p + r)/2\rfloorq=