排序算法应该是程序员面试的时候必然会被问到的问题之一。总结起来就下面几个问题:
- 你知道哪些排序算法?
- 具体介绍一下xx排序算法的实现原理
- xx排序算法的时间复杂度和空间复杂度分别是多少?
- xx排序算法是稳定的嘛?
面对以上几个问题,首先你得知道什么是排序,什么是时间/空间复杂度以及什么叫做稳定排序。首先,排序问题很好理解,就是将无序的一组数据变成有序的(可以是升序也可以是降序)。举个例子,< 3, 2, 5, 4, 7, 9, 0, 1>这一组数据经过你的代码处理后变成了<0, 1, 2, 3, 4, 5, 7, 9>他就完成了排序过程。其次就是时间复杂度和空间复杂度的问题。时间复杂度就是用来描述整个实现代码运行的时间,空间复杂度就是运行时消耗掉的空间(寄存器,内存)。这里不做过多的介绍,具体可以参考我的另一篇博客。最后一个就是所谓的稳定性,排序算法的稳定性是指,如果待排序数据中出现两个相同数a0 == a1,其中a0在a1的前面,排序完成之后,a0必然会和a1紧靠,但是此时a0必须还要在a1的前面,这种情况我们则称这种排序是稳定的,否则称之为不稳定排序。
有了以上基础知识后,我们先来介绍第一种排序算法:插入排序。插入排序算法是非常好理解的,想必看这篇文章的人都玩过斗地主吧(不接受抬杠,默认都玩过,没玩过的现在去买副扑克,找俩人一起玩一下)。在我们摸牌的时候,我们会将牌按照一定的次序排列,从左到右,由大到小或从右到左由大到小。我们总是摸一张牌后,和手上的牌依次做比较,直到找到这张牌的合适位置,并把它放入手中。这就是传说中的插入排序法。了解了其基本原理之后,我们可以写出如下伪代码:
void insertionSort(arr[], len){ //参数为待排序数组和数组的长度
for i from 1 to len: //遍历从第一个数开始,而不是第0个
temp = arr[i] //先将待插入数保存
for j from i to 0 and arr[j - 1] > temp: //从第i个数的前一个开始遍历如果前一个数比当前数字大,则将前一个数的值赋给当前数(搬迁)依次类推。
arr[j] = arr[j - 1]
arr[j] = temp //当上一个循环跳出之后,自然第几个数据所在的位置就是我们需要插入的位置,直接覆盖就好了。
}
同故宫上面的伪代码,我们可以确定整个算法的大致结构,那么接下来还有一个问题就是,需要比较什么类型的数据?这里可以参考C++中的template模板。有了模板之后,我们可以比较任何一个我们想比较的数据类型,包括类(只要我们重构">"操作符即可)。
代码如下:
#include<iostream>
using namespace std;
template<typename T>
void insertionSort(T arr[], int len) { //参数为待排序数组和数组的长度
for (int i = 1; i < len; i++) { //遍历每一个带插入的数
T temp = *(arr + i); //将带插入数用中间变量保存
int j;
for (j = i; j > 0 && *(arr + j - 1) > temp; j--) //从第i个数开始遍历,若i - 1个数小于第i个数,则搬迁。
//for (j = i; j > 0 && *(arr + j - 1) >= temp; j--) //不稳定排序算法
*(arr + j) = *(arr + j - 1);
*(arr + j) = temp; //最后剩余的就是要插入的地方
}
}
int main() {
char a[] = { '1','5','3','4','6','5','3', '7', '0', 'a', 'B' };
insertionSort(a, sizeof(a) / sizeof(char));
for (auto p : a) {
cout << p << "\t";
}
}
还有几个关键性的问题,这种排序算法的时间复杂度和空间复杂度是多少呢?
我们考虑两种情况:
- 最好的情况就是,数组本来就排好序了,那么就不需要再排序了,我们的代码只需要检测一遍即可完成。即上述代码中最外层循环执行,内层循环不执行,因此时间复杂度为O(n)
- 最坏的情况就是,数组是逆序,那么我们就需要将每个数依次搬迁。这样不但最外层的循环要全部执行,内层的数据也要全部执行。因此时间复杂度为O(n2).
很明显,此处的空间复杂度为O(1). 一般地,插入排序法会用在数据量较小的情形下,当数据量过大时,插入排序算法的效率就显得非常低了。
除此之外,还需要判断一下插入排序是否属于稳定排序。从上面的代码来看,插入排序属于稳定排序算法,理由如下:
假设有这么一个数组:<1, 5, 3, 4, 6, 5, 3>,此处有两处相同的数。采用插入排序法排序时我们是从后往前找,只有找到比当前数要小的数,才插入。因此当我们找到第2个5时,只会将其放在第一个5后面,而不是前面。因此,插入排序法属于稳定排序算法。当然,插入排序算法也可以变成不稳定排序算法,只要修改一处地方——将上述代码中的第二个for循环采用下一行代替,可以思考一下为什么。
接下来,再来看看冒泡排序吧,冒泡排序法也很容易理解。所谓的冒泡就是指,每次将大数沉入底下,小数浮到上方,类似于鱼吐泡泡。因此叫冒泡排序。
冒泡排序的基本思路就是,遍历所有数据,依次两两比较,若大则交换,否则继续比较,其伪代码如下:
void bubbleSort(arr[], int len){
for i from 0 to len:
forj from i + 1 to len:
if arr[i] < arr[j]:
swap(arr[i], arr[j])
}
因此通过代码实现如下:
#include<iostream>
using namespace std;
template<typename T>
void bubbleSort(T arr[], int len) { //参数为待排序数组和数组的长度
for (int i = 0; i < len; i++) { //遍历每一个带插入的数
for(int j = i + 1; j < len; j++)
if (*(arr + i) > * (arr + j)) {
T temp = *(arr + j);
*(arr + j) = *(arr + i);
*(arr + i) = temp;
}
}
}
int main() {
char a[] = { '1','5','3','4','6','5','3', '7', '0', 'a', 'B' };
bubbleSort(a, sizeof(a) / sizeof(char));
for (auto p : a) {
cout << p << "\t";
}
}
同理,冒泡排序法的时间复杂度最好情况也是O(n),最差情况也是O(n2),空间复杂度为O(1)以及属于稳定排序算法。当然也可以变成不稳定的,只需要修改一处地方即可。