今天主要学习了第二章“算法基础”。
主要包括以下几个方面:
插入排序
分析算法的方法
分治法设计算法
归并排序
下面逐条进行归纳和总结。
一.插入排序
在编程解决问题的过程中,排序使我们经常会遇到的一个不可或缺的步骤,但是这个步骤虽然看起来很简单,但是由于方法多样,他们的差异很大,因此选择不同的排序方法,对运算的成本可能会造成很大的影响。插入排序就是其中的一个效率比较低的一种方法,但是优点是易于理解。
《算法导论》中举了一个十分生动的例子来介绍插入排序。
插入排序类似于我们将我们手中的扑克牌排序,刚开始时候我们手中没有牌,之后每拿一张牌,我们就把他按大小顺序放在正确的位置上,为此我们需要从右到左一次比较,直到找到合适的位置,然后把这张牌插进去。
插入排序的工作原理与此类似,当我们拿到一组数,就把其中的第一个数拿出来,之后按次序拿剩下的数,每拿一个数出来就和已经拿出来的数从右向左依次进行比较,把他放在合适的位置上,直到将所有的数拿完,现在我们就得到了有序的数列。
c++代码实现如下:
#include<iostream>
using namespace std;
void insertionSort(int a[],int n) {
for (int i = 0; i < n; i++) {
int key = a[i];//取出第一个数
int j = i - 1;
while (j >= 0 && a[j] > key) {//已经排好序的数依次与该数进行比较
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
}
int main() {
int n;
cout << "please input the number of your array\n";
cin >> n;
int *array = new int[n];
cout << "please input " << n << " numbers\n";
for (int i = 0; i < n; i++) {
cin >> array[i];
}
insertionSort(array, n);
for (int i = 0; i < n; i++)
cout << array[i] << " ";
cout << endl;
return 0;
}
二.以插入排序为例进行算法分析
对一个算法的分析,我认为主要包括以下两个方面:
- 算法的正确性
- 算法的效率
对于第一点,其实非常重要,一个算法,不管效率如何,首先要保证正确,但是我原来在学习的时候总是默认他们都是正确的,直到今天才学到了如何证明一个算法的正确性。
在此之前,我们首先要引入一个概念,叫做循环不变式
在上述插入排序的例子中,我们在每次拿一张牌之后,会把他插入到手中已有的牌堆中,我们手中已有的牌堆,是已经排好序的牌,我们把这些牌也就是a[0..i-1]称为循环不变式
循环不变式可以帮助我们理解算法的正确性。对于循环不变式,我们要证明三条性质:
- 初始化:确保循环开始之前,它为真
- 保持:确保每次循环后,他仍然为真
- 终止:循环结束时不变式可以为我们提供一个有用的性质,帮助我们证明算法的正确性
下面我们以插入排序为例,用循环不变式的方法,证明其正确性
初始化: 第一次执行循环之前,我们手中只拿到一张牌,他已经是有序的,所以此时为真
保持:每次我们摸到一张牌,都会和手中已经排好序的牌从右向左一次比较,直到找到合适的位置才把他插进去,这时候手中的牌多了一张,但仍然是有序的,所以此时仍然为真,循环不变式保持。
终止:当牌堆中没有牌,也就是我们把所有牌都摸完了,那么循环结束,此时我们手中的牌仍然是原来排队中的牌,但已经经过排序,所以结果正确,算法的正确性得到证明。
ps:你有可能会觉得这不是废话吗,那估计是我说的不够有逻辑,参考《算法导论》第三版11页。
对于第二点,算法的效率,其实就是我们常说的时间复杂度
对于时间复杂度的计算方法,因人而异,我们平常只需要大致的估算出他的数量级就可以了,不需要进行太精确的逐步计算,关于逐步计算时间复杂度,请参考《算法导论》第三版14页,这里我贴一张图:
上图是一段关于插入排序的伪代码,每一行的末尾标出了执行此代码的代价和执行次数,把他们分别相乘再相加,然后进行化简,就可以得出插入排序的时间复杂度,是O(n^2),我们这里指的是最坏情况或者说平均情况的时间复杂度。