排序算法复习

更新中,未完待续

考研凉凉的结果就是不仅失去了应届生走校招的机会,而且还因为疫情走不了,僵。因为接下来准备找一份安全相关的C/C++开发工作,所以在刷了段时间题后,把排序算法重新复习整理一下。之前虽然学习过,但是现在一些不经常用的东西很快就忘了细结了,只能偶尔复习一下。

本文主要以十大经典排序算法为参考文章来进行复习,使用C++实现,代码都经过了测试。

冒泡排序(Bubble Sort)

冒泡排序,选择排序和插入排序算是我使用最多的三种排序方法了,因为实现起来简单,惭愧惭愧。

流程就是比较相邻的元素,如果前面的比后面的大,就把他们交换一下。

这样一轮交换结束后,最后一个元素是所有数字里最大的,最坏情况下即数组是逆序的情况下,所有第一轮需要比较和交换n-1次,第二轮是n-2次,以此类推,最后总的比较和交换的次数是1+2+..+n-1=n(n-1)/2次,时间复杂度是O($n^2$),每次对换需要一个temp,所以空间复杂度是O(1),由于对换的时候可以对相同元素选择不对换,所以是稳定的排序。

i = n - 2
j = n - 1
代码实现:

class Sort{
public:
vector<int> BubbleSort(vector<int> array){
int n = array.size();
for(int i = 0; i < n - 1; i ++){
for(int j = 0; j < n - i - 1; j ++){
if(array[j] > array[j + 1]){
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
return array;
}
};

选择排序(Selection Sort)

找到最小的一个元素放在第一个位置,然后循环剩下的元素,或者找最大的放最后一个位置。

比较次数和冒泡实际上是一样的,也是n-1 + n-2 + … + 1 = n * (n - 1) / 2次,时间复杂度是O($n^2$),空间复杂度是O(1)

class Sort{
public:
vector<int> SelectionSort(vector<int> array){
int n = array.size();
for(int i = 0; i < n; i ++){
int min = i;
for(int j = i + 1; j < n; j ++){
if(array[j] < array[i]){
min = j;
}
}
int temp = array[i];
array[i] = array[min];
array[min] = temp;
}
return array;
}
};

插入排序(Insertion Sort)

插入排序是对每一个元素,从开头开始扫描,找到合适的位置插入

由于对每个元素,都需要向后移动1+2+..+n-1=n*(n-1) / 2次数据,所以时间复杂度也是O($n^2$),空间复杂度是O(1)

class Sort{
public:
vector<int> InsertionSort(vector<int> array) {
int n = array.size();
for (int i = 1; i < n; i++) {
int temp = array[i];
int front = i - 1;
while (front >= 0 && array[front] > temp) {
array[front + 1] = array[front];
front--;
}
array[front + 1] = temp;
}
return array;
}
}

希尔排序(Shell Sort)

插入排序的改进,优先比较距离近的元素,所以又叫缩小增量排序,说白了就是把插入排序中,一个一个比较改成从第n个开始比较,作为增量,直到这个增量变为1,因此增量序列是$t_1,t_2,t_3…t_n$,当i < j时有$t_i > t_j$,$t_n = 1$

目前为止还没有一个最好的增量序列,希尔提出的序列是$t1=n/2$, $t{i+1}=t_i/2$,最坏情况下的时间复杂度是O($n^2$),n在特定范围的时候,时间复杂度是O($n^1.3$),空间复杂度是O(1)

class Sort{
public:
vector<int> ShellSort(vector<int> array){
int n = array.size();
for(int t = n / 2; t > 0; t /= 2){
for(int i = 0; i < t; i ++){// 循环每一组
for(int j = i + t; j < n; j += t){
if(array[j] < array[j - t]){
int temp = array[j], k;
for(k = j - t; k >= 0 && array[k] > temp; k -= t){
array[k + t] = array[k];
}
array[k + t] = temp;
}
}
}
}
return array;
}
};

快速排序(Quick Sort)

快速排序是对冒泡排序的改进,基于分治法思想,先选取一个元素作为基准,通过一趟排序,把整个列表划分为独立的两部分,前半部分均小于基准,后半部分大于基准,这时,基准所在的位置是最终结果的位置,整个过程称为一趟快速排序,接下来分别对前后两个子表递归重复上述过程,直至每个部分只有一个元素或者为空为止。

快速排序的空间复杂度取决于递归调用栈的深度,最好情况下是$\lceil\log_2(n+1)\rceil$,最坏情况下需要进行n-1次递归,栈深度为O(n),因此空间复杂度在最坏情况下为O(n),评价情况下为O($\log_2n$)。

时间复杂度在最坏情况下,划分的两个部分完全不对称,即一部分是n-1个元素,另一部分是0个元素,在数组完全逆序或者基本有序的情况下,得到最坏情况的时间复杂度O($n^2$)。
在理想状态下,每次划分都是最平衡时,得到的两个子排序的时间复杂度都不超过n/2,此时排序的时间复杂度为O($n\log_2n$)。
若存在两个相同元素在右端区间,且均小于基准,在这两个元素移动到左边区间时,相对位置会发生替换,因此是不稳定的排序。

有很多方法可以提高快速排序的效率。第一种方法是在子序列的规模较小的时候就换用直接插入排序。第二方法是尽量选择一个可以均匀划分序列的基准,比如从头尾及中间选取三个元素,然后取其中间值作为基准,或者随机从序列中选择一个基准。

class Sort{
public:
/* 保持和前面排序函数相同的接口 */
vector<int> QuickSort(vector<int> array) {
QuickSortRecursion(array, 0, array.size() - 1);
return array;
}

private:
/* 用来递归的函数 */
void QuickSortRecursion(vector<int> &array, int low, int high) {
if (low < high) { // 递归结束条件
int pivotpos = Partition(array, low, high);
QuickSortRecursion(array, low, pivotpos - 1);
QuickSortRecursion(array, pivotpos + 1, high);
}
}
/* 用来划分的函数 */
int Partition(vector<int> &array, int low, int high) {
int pivot = array[low]; // 第一个元素设置为基准
while (low < high) {
while (low < high && array[high] >= pivot) --high; // 从后往前找一个比基准小的元素
array[low] = array[high]; // 放在最前面
while (low < high && array[low] <= pivot) ++low; // 从前往后找一个比基准大的元素
array[high] = array[low]; // 放在最后面
}
array[low] = pivot; // 基准放到最终位置
return low;
}
}

堆排序(Heap Sort)

堆排序是一种树形的选择排序,在排序过程中,把待排序的序列看作一颗完全二叉树的顺序存储结构,利用二叉树来选择值最大或最小的元素。

堆存在两种情况:

  1. 根结点小于左右子结点,称为小根堆
  2. 根结点大于左右子结点,称为大根堆

首先要建立一个大根堆,方法就是先找到最后一个结点的父结点,由于堆是一个完全二叉树,所以最后一个结点的下标的父结点应该是$\lfloor length/2 - 1 \rfloor$(下标从0开始),接着比较最后一个结点和其父结点的值,若大于父结点则交换。对这个父结点和它前面的结点重复上述过程,就完成了初始建堆。

建堆完成后,所有的数据组成一个大根堆,根结点是最大的元素,将其放在数组末尾,也就是把根结点和最后一个结点交换,交换完成后,堆的结构被破坏,需要重新调整。

除去已经排好序的最大的结点,其他结点作为一个堆重新进行调整直至成为大根堆。由于只修改了根结点,所以只需要对根结点进行一次向下调整即可。重复把最大元素调整到数组末尾,即可得到一个有序序列。

建堆的时间复杂度为O(n),所以可以利用建堆找出很多数中的最大的几个数字。建好堆后对堆调整了n-1次,每次调整的时间复杂度取决于二叉树的高度,所以堆排序的时间复杂度为O($n\log_2(n)$),由于使用了常数个辅助单位,所以空间复杂度是O(1),由于在筛选的时候可能把值相同的元素互相调换,所以是一种不稳定的排序。

class Sort{
public:
vector<int> HeapSort(vector<int> array) {
int len = array.size();
BuildMaxHeap(array, len);
for (int i = len - 1; i > 0; i--) {
int temp = array[i];
array[i] = array[0];
array[0] = temp;
AdjustDown(array, 0, i);
}
return array;
}
private:
void BuildMaxHeap(vector<int> &array, int len) {
for (int i = len / 2 - 1; i >= 0; i--) {
AdjustDown(array, i, len);
}
}
void AdjustDown(vector<int> &array, int k, int len) {
int temp = array[k];
for (int i = k * 2 + 1; i < len; i = i * 2 + 1) {
if (i + 1< len && array[i] < array[i + 1]) { // 判断子结点哪个大
i++;
}
if (temp >= array[i]) { // 比较根结点和较大的子结点
break;
}
else {
array[k] = array[i]; // 把较大的子结点放在根结点的位置
k = i;
}
}
array[k] = temp;
}
};

归并排序(Merge Sort)

归并排序基于一种全新的思想,即将两个或两个以上的有序表组合成一个新的有序表,假定有n个元素待排序,可以看作是n个有序表,然后两个两个归并,得到$\lceil n/2 \rceil$个长度为1或2的有序表,继续两两归并直到合并为一个长度为n的有序表,这种方法称为二路归并,如果是多个有序表归并则称为n路归并。

由于需要用到n个辅助空间,所以空间复杂度是O(n),每趟归并的时间复杂度是O(n),进行$\lceil log_2n \rceil$次归并,所以时间复杂度是O($n\log_2n$)$,因为排序不会改变相同元素的相对位置,所以是稳定的排序。

class Sort{
private:
/*
Merge用来合并两个相邻的有序表
一个是temp,一个是array
合并结果存入结果数组array中
*/
void Merge(vector<int> &temp, vector<int> &array, int low, int mid, int high){
int i = 0, j = 0, k = 0;
for(i = low; i <= high; i ++){
temp[i] = array[i];
}
for(i = low, j = mid + 1, k = i; i <= mid && j <= high; k ++){
if(temp[i] <= temp[j]){
array[k] = temp[i ++];
}else{
array[k] = temp[j ++];
}
}
while(i <= mid){
array[k ++] = temp[i ++];
}
while(j <= high){
array[k ++] = temp[j ++];
}
}
/*
实际的排序函数,用于递归
*/
void MergeSortTemp(vector<int> &temp, vector<int> &array, int low, int high){
if(low < high){
int mid = (low + high) / 2;
MergeSortTemp(temp, array, low, mid);
MergeSortTemp(temp, array, mid + 1, high);
Merge(temp, array, low, mid, high);
}
}
public:
/*
排序函数的接口
*/
vector<int> MergeSort(vector<int> array){
vector<int> temp(array.size(), 0); // 创建一个辅助数组,初始化为0
MergeSortTemp(temp, array, 0, array.size() - 1);
}
};

计数排序(Counting Sort)

桶排(Bucket Sort)

基数排序(Radix Sort)