一.排序的概念
1.排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
2.排序的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。(相同的值相对顺序不变)
**3.**内部排序:数据元素全部放在内存中的排序。
4.外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断地在内外存之间(如磁盘间)移动数据的排序。
二.常见的排序算法
1.冒泡排序
思路:两个相邻的元素进行比较,升序情况下,前一个大于后一个,就进行交换 一趟之后,大的数被交换到最后。

代码实现:
cpp
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
// 冒泡排序
void BubbleSort(int* a, int n)
{
int i = 0;
int j = 0;
for (i = 0; i < n - 1; i++)
{
//假设有序
int flag = 1;
for (j = 0; j < n - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = 0;
}
}
//已经有序
if (flag == 1)
{
break;
}
}
}
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序,适用于教学。
- 时间复杂度:O(N^2)。
- 空间复杂度:O(1)。
- 稳定性:稳定。
2.插入排序
思路:认为[0,end]有序,end+1位置的值存到tmp中,比tmp大的数往后挪,最后将tmp插入到[0,end]中,保持有序 。即把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。


代码实现:
cpp
// 插入排序
void InsertSort(int* a, int n)
{
// [0, n-1]
for (int i = 0; i < n - 1; i++)
{
// [0, n-2]是最后一组
// [0,end]有序 end+1位置的值插入[0,end],保持有序
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];//往后挪
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高。
- 时间复杂度:O(N^2)。
- 空间复杂度:O(1),它是一种稳定的排序算法。
- 稳定性:稳定。
3. 希尔排序(缩小增量排序)
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后重复上述分组和排序的工作。当到达分组=1时,所有记录在统一组内排好序。
这里我们引入一个gap的概念:

希尔排序是对直接插入排序的优化,当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了。
希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此不同的书中给出的希尔排序的时间复杂度不固定。

空间复杂度:O(1)。
稳定性:不稳定。
所以希尔排序=预排序(使数据接近有序)+插入排序 。
代码实现:
cpp
void ShellSort(int* a, int n)
{
//多组并着走
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
//+1保证最后一个gap一定是1
//gap > 1 时是预排序
//gap == 1 时是插入排序
//超过n-gap越界
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
4.选择排序
基本思想: 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。选出最小的数放到左边,最大的放到右边。需要特殊处理begin==maxi的情况。
代码实现:
cpp
void SelectSort(int* a, int n)
{
int begin = 0,end = n - 1;
while (begin < end)
{
int mini = begin, maxi = begin;
for (int i = begin + 1; i <= end; ++i)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
if (begin == maxi)
maxi = mini;
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用。
- 时间复杂度:O(N^2)。
- 空间复杂度:O(1)。
- 稳定性:不稳定。
5.堆排序
前面的文章中有提及过,堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
代码实现:
cpp
void AdjustDown(int* a, int n, int parent)
{
// 先假设左孩子小
int child = parent * 2 + 1;
while (child < n) // child >= n说明孩子不存在,调整到叶子了
{
// 找出小的那个孩子
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
// 向下调整建堆 O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
// O(N*logN)
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
直接选择排序(堆排序)的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)。
- 空间复杂度:O(1)。
- 稳定性:不稳定。
6.快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值**,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右****子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止**。
cpp
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = partion(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(array, left, div);
// 递归排[div+1, right)
QuickSort(array, div+1, right);
}
(1)hoare版本


代码实现:
cpp
void QuickSortPart1(int* a, int left, int right)
{
if(left>=right)
return;
int keyi = left;
int begin = left, end = right;
while (begin < end)
{
//右边找小
while (begin < end && a[end] >= a[keyi])
{
--end;
}
//左边找大
while (begin < end && a[begin] <= a[keyi])
{
++begin;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[keyi], &a[begin]);
keyi = begin;
//[left ,keyi-1] keyi [keyi+1,right]
//递归
QuickSortPart1(a, left, keyi - 1);
QuickSortPart1(a, keyi + 1, right);
}
优化:
避免有序情况下,效率退化:1、随机选key 2、三数取中 3、小区间优化。
cpp
//三数区中
int GetMidi(int* a, int left, int right)
{
int midi = (left + right) / 2;
// left midi right
if (a[left] < a[midi])
{
if (a[midi] < a[right])
{
return midi;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else // a[left] > a[midi]
{
if (a[midi] > a[right])
{
return midi;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
// 小区间优化,不再递归分割排序,减少递归的次数
if ((right - left + 1) < 10)
{
InsertSort(a+left, right - left + 1);
}
else
{
// 三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
int begin = left, end = right;
while (begin < end)
{
// 右边找小
while (begin < end && a[end] >= a[keyi])
{
--end;
}
// 左边找大
while (begin < end && a[begin] <= a[keyi])
{
++begin;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[keyi], &a[begin]);
keyi = begin;
// [left, keyi-1] keyi [keyi+1, right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
}
(2)挖坑法

代码实现:
cpp
void QuickSortPart2(int* a, int left, int right)
{
if (left >= right)
return;
int key = a[left];
int hole = left;
int begin = left;
int end = right;
while (begin < end)
{
while (begin < end && a[end] >= key)
{
--end;
}
a[hole] = a[end];
hole = end;
while (begin < end && a[begin] <= key)
{
++begin;
}
a[hole] = a[begin];
hole = begin;
}
//填坑
a[hole] = key;
QuickSortPart2(a, left, hole - 1);
QuickSortPart2(a, hole+1, right);
}
(3)前后指针法

思路:cur找小,++prev,交换cur和prev的值 。
代码实现:
cpp
void QuickSortPart3(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = left;
int prev = left;
int cur = prev+1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
QuickSortPart3(a, left, keyi- 1);
QuickSortPart3(a,keyi+1, right);
}
7.快速排序非递归
这里非递归的写法可以利用栈或者队列来模拟快排的递归过程。我们需要知道的是,在递归过程中本质变化的是左右区间。
这个是快排递归的过程:

然后用栈模拟划分左区间 key值 右区间,之后右 左区间入栈。

代码实现:
Stack.h
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
// 初始化和销毁
void STInit(ST* pst);
void STDestroy(ST* pst);
// 入栈 出栈
void STPush(ST* pst, STDataType x);
void STPop(ST* pst);
// 取栈顶数据
STDataType STTop(ST* pst);
// 判空
bool STEmpty(ST* pst);
// 获取数据个数
int STSize(ST* pst);
Stack.c
cpp
#include"Stack.h"
// 初始化和销毁
void STInit(ST* pst)
{
assert(pst);
pst->a = NULL;
// top指向栈顶数据的下一个位置
pst->top = 0;
pst->capacity = 0;
}
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = pst->capacity = 0;
}
// 入栈 出栈
void STPush(ST* pst, STDataType x)
{
assert(pst);
// 扩容
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
void STPop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
// 取栈顶数据
STDataType STTop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
// 判空
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0;
}
// 获取数据个数
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
Sort.c
cpp
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//前后指针的快排
int PartSort2(int* a, int left, int right)
{
int keyi = left;
int prev = left;
int cur = prev+1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
#include"Stack.h"
void QuickSortNonR(int* a, int left, int right)
{
ST st;
STInit(&st);
//先入右再入左
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st))
{
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
int keyi = PartSort2(a, begin, end);
// 分成了[begin, keyi-1] keyi [keyi+1, end]
//依旧先入右再入左
if (keyi + 1 < end)
{
STPush(&st, end);
STPush(&st, keyi+1);
}
if (begin < keyi-1)
{
STPush(&st, keyi-1);
STPush(&st, begin);
}
}
STDestroy(&st);
}
总结:循环每走一次(相当与递归一次),取栈顶区间,单趟排序,先右再左子区间入栈。
快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序。
- 时间复杂度:O(N*logN)。
- 空间复杂度:O(logN)。
- 稳定性:不稳定。
8.归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

代码实现:
cpp
void _MergeSort(int* a, int* tmp, int begin, int end)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
_MergeSort(a, tmp, begin, mid);
_MergeSort(a, tmp, mid + 1, end);
//[begin,mid] [mid+1,end]
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
//左右都有序 小的拿到tmp中尾插
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if(a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//如果比较之后 左右区间还有数 直接拿下来尾插到tmp中
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//将tmp中数据移动到a中
memcpy(a+begin, tmp+begin, (end - begin+1) * sizeof(int));
}
//归并排序
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail!");
return;
}
//子函数 完成函数功能的一部分
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
tmp = NULL;
}
需要注意的是:如果区间划分为【begin,mid-1】【mid,end】可能会导致死循环,栈溢出等问题。
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)。
- 空间复杂度:O(N)。
- 稳定性:稳定。
9.归并排序非递归
把递归改成非递归一般就两种,一是借助栈和队列,二就是改成循环,这里要实现归并非递归改成循环的方式会比较好,同样也是模拟归并的过程。


代码实现:
cpp
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
//gap指的是每组归并数据的数据个数
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
// [begin1, end1][begin2, end2]
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
//printf("[%d,%d][%d,%d] ", begin1, end1, begin2, end2);
//注意边界问题
// 第二组都越界不存在,这一组就不需要归并
if (begin2 >= n)
break;
// 第二的组begin2没越界,end2越界了,需要修正一下,继续归并
if (end2 >= n)
end2 = n - 1;
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//细节:边归并边copy
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
//printf("\n");
gap *= 2;
}
free(tmp);
tmp = NULL;
}
10.计数排序
思想:1. 统计相同元素出现次数。2. 根据统计的结果将序列写回到原来的序列中,需要开一个助数组count,即利用了hash映射的思想。
直接映射
当数据和数组下标直接映射会产生空间的浪费时候,我们就采用相对映射。
代码实现:
cpp
void CountSort(int* a, int n)
{
//找出最小和最大的数
int min = a[0], max = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] < min)
min = a[i];
if (a[i] > max)
max = a[i];
}
int range = max - min + 1;
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
perror("calloc fail");
return;
}
//计数
for (int i=0;i<n;i++)
{
count[a[i] - min]++;
}
//排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[j++] = i + min;
}
}
free(count);
}
计数排序的特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限(一般适用于整数排序)。
- 时间复杂度:O(MAX(N,范围))。
- 空间复杂度:O(范围)。
- 稳定性:稳定。
11.其他排序(了解)
在《算法导论》中有详细的关于排序的讲解,有需要pdf可私信联系我。(中文版)
三.排序算法复杂度及稳定性分析
