一.基础排序算法
1.冒泡排序
(1)算法思想
重复循环遍历,每次比较相邻的两个元素,如果前大于后就交换,这样每次最大的元素都会交换到最后,每次循环遍历时去掉最后的元素。
(2)代码实现
cpp
void BubbleSort(int arr[], int size)
{
for (size_t i = 0; i < size-1; i++)
{
bool flag=false;
for (size_t j = 0; j < size - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp=arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag=true;
}
}
if(!flag)//优化:如果这一趟没有交换元素,说明已经有序,无需再排了
return;
}
}
(3)性能分析与优化
最好时间复杂度:O(n),(在加了优化操作后,如果元素原本就已经有序,则只需进行一次遍历)
平均时间复杂度:O(n^2);(双重循环)
最坏时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
**优化:**定义一个flag记录本次循环是否交换元素,如果没有则说明元素已经有序,直接结束排序。
2.选择排序
(1)算法思想
遍历数组,每次假定第i个为最小,与后面的比较,找到最小的,交换,这样每次都能将最小的元素交换到i的位置(即每次将最小的元素排到正确位置)
(2)代码实现
cpp
void ChoiceSort(int arr[], int size)
{
for (size_t i = 0; i < size-1; i++)
{
int min = arr[i];
int k = i;
for (size_t j = i+1; j < size ; j++)
{
if (min > arr[j])
{
min = arr[j];
k = j;
}
}
if (k != i)
{
int temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
}
}
}
(3)性能分析与优化
最好时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
最坏时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
注意:虽然冒泡排序与选择排序的平均时间复杂度都是O(n^2),但是选择排序一般更快,原因在于交换次数差异,冒泡排序的交换频繁(交换n^2次),而选择排序交换较少(n次)。
3.插入排序
(1)算法思想
假设第一个元素已经有序,每次遍历将遍历到的元素按顺序插入到前面已经有序的数组中。
(2)代码实现
cpp
void InsertSort(int arr[], int size)
{
for (size_t i = 1; i < size; i++)
{
int val = arr[i];
int j = i - 1;
for (; j >= 0; j--)
{
if (arr[j] <val)
{
break;
}
arr[j+1] = arr[j];
}
arr[j + 1] = val;
}
}
(3)性能分析与优化
最好时间复杂度:O(n)(原数组已经有序,只需要进行一次遍历)
平均时间复杂度:O(n^2)
最坏时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
适用场景:数据规模小,相对有序的数据,常用于收尾工作(对于相对有序的数据排序速度较快)
4.希尔排序
(1)算法思想
希尔排序实际上是将插入排序进行分组优化。
选择合适的希尔增量,基础的为每次按半分组(即如果数组中存在10个元素,那么分成5组,每组2个元素,比较这两个元素,交换)。
(2)代码实现
cpp
void ShellSort(int arr[], int size)
{
for (size_t gap = size/2; gap >0 ; gap/=2)
{
for (size_t i = gap; i < size; i++)
{
int val = arr[i];
int j = i - gap;
for (; j >= 0; j-=gap)
{
if (arr[j] < val)
{
break;
}
arr[j + gap] = arr[j];
}
arr[j + gap] = val;
}
}
}
(3)性能分析与优化
最好时间复杂度:O(nlogn)或O(n);
平均时间复杂度:O(n^1.3),(依赖于希尔增量,如果二分的话,就为n^2)
最坏时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
四种基础排序算法比较

一般情况下:算法效率:希尔>插入>选择>冒泡
对于趋向于有序的数组,我们使用插入排序的效率更高。
一般中等数据量的排序都用希尔排序,选择合适的增量序列,效率就已经不错了,如果数据量比较大,可以选择高级的排序算法,如快速排序。
二.高级排序算法
1.快速排序
(1)算法思想
思想:选取一个基准数,把小于基准数的元素都调整到基准数的左边,把大于基准数的元素都调整到基准数的右边,然后对基准数左边和右边的序列继续进行如下的操作,直到整个序列变成有序的。
操作流程:1:选取基准数val(默认选择第一个元素),定义L与R两个下标值,指向数组的首和尾。
2、从R开始往前找第一个<val的数字,放到L的地方,L++
3、从L开始往后找第一个>val的数字,放到R的地方,R --
4、重复上面的过程
(2)代码实现
cpp
//快排分割函数
int Partation(int arr[], int L, int R)
{
int val = arr[L];
while (L < R)
{
while (arr[R]>val&&L<R)
{
R--;
}
if (L < R)
{
arr[L] = arr[R];
L++;
}
while (arr[L] < val && L < R)
{
L++;
}
if (L < R)
{
arr[R] = arr[L];
R--;
}
}
arr[L] = val;
return L;
}
//快排函数接口
void QuickSort(int arr[], int L, int R)
{
if (L >= R)
return;
int pos = Partation(arr, L, R);
//对分割后的两部分分别进行快排
QuickSort(arr, L, pos - 1);
QuickSort(arr, pos+1, R);
}
//快排函数
void QuickSort(int arr[],int size)
{
return QuickSort(arr,0,size-1);
}
(3)性能分析与优化
最好时间复杂度:O(nlogn),递归深度最好为log(n)(完全二叉树),比较过程为O(n);
平均时间复杂度:O(nlogn);
最坏时间复杂度:O(n^2)(数据完全有序且恒选择第一个元素作为基准数,会退化为冒泡排序);
空间复杂度:最好O(nlogn),最坏O(n^2)(数据完全有序且恒选择第一个元素作为基准数)
稳定性:不稳定
快排算法优化
1、随着快排算法执行,数据越来越趋于有序,在一定的范围内,可以采用插入排序代替快速排序
2.选择合适的基准数,三数取中法(L,R,mid三数值在中间的),随机数法。
适用场景:绝大多数通用场景,尤其是处理大量随机元素,标准库的默认排序。
2.归并排序
(1)算法思想
归并排序的核心是分治思想,先递,递到只有一个元素,再在归的过程中,对数据进行合并排序
(2)代码实现
cpp
//归并过程函数
void Merge(int arr[], int l, int m, int r)
{
int* p = new int[r - l + 1];
int idx = 0;
int i = l;
int j = m + 1;
while (i <= m && j <= r)
{
if (arr[i] <= arr[j])
{
p[idx++] = arr[i++];
}
else
{
p[idx++] = arr[j++];
}
}
while (i <= m)
{
p[idx++] = arr[i++];
}
while (j <= r)
{
p[idx++] = arr[j++];
}
//在不合并好的数据拷贝到原始数组中
for (size_t i = l,j=0; i <=r ; i++,j++)
{
arr[i] = p[j];
}
delete[]p;
p = nullptr;
}
//归并排序
void MergeSort(int arr[], int begin, int end)
{
if (begin >= end)
{
return;
}
int mid = (begin + end) / 2;
//先递归
MergeSort(arr, begin, mid);
MergeSort(arr, mid + 1, end);
//再归并
Merge(arr, begin, mid, end);
}
(3)性能分析与优化
最好时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn);
最坏时间复杂度:O(nlogn)(无论原先元素是否有序,递归的深度恒是nlogn)
空间复杂度:O(n)
稳定性:稳定
适用场景:对稳定性有要求,或者处理链表排序、海量数据的外部排序时,归并排序是首选。
3.堆排序
(1)算法思想
找到每一个非叶子节点,将二叉堆调整为一个大根堆,之后将堆顶元素与队尾元素交换,由于大根堆堆顶是恒是最大值,所以每一次调整都将一个最大值交换到了一个正确位置。
(2)代码实现
cpp
//堆排下沉操作
void siftDown(int arr[], int i, int size)
{
int val = arr[i];
while (i < size / 2)
{
int child = 2 * i + 1;
if (child + 1 < size && arr[child + 1] > arr[child])
{
child = child + 1;
}
if (val < arr[child])
{
arr[i] = arr[child];
i = child;
}
else
break;
}
arr[i] = val;
}
//堆排序
void HeapSort(int arr[], int size)
{
int n = size - 1;
//找到每一个非叶子节点
for (int i = (n - 1) / 2; i >= 0; i--)
{
siftDown(arr, i, size);
}
for (size_t i = n; i > 0; i--)
{
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
siftDown(arr, 0,i);
}
}
(3)性能分析与优化
最好时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn);
最坏时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性:不稳定
适用场景:内存极其受限的嵌入式系统,或者经典的 Top K 问题
4.基数排序(桶排序)
(1)算法思想
先构建桶(没有负数构建0~9号桶,有负数构建-9~9号桶),从右向左依次看元素的每一位,如果那一位与桶的序号相同,就将其放在对应的桶中,放完一位后,再将所有的元素按桶的顺序取出,如此循环,直到排完所有的位后。
(2)代码实现
cpp
//基数排序
void RadixSort(int arr[], int size)
{
int maxData = arr[0];
for (size_t i = 1; i < size; i++)
{
if (maxData<abs(arr[i]))
{
maxData = abs(arr[i]);
}
}
int len = to_string(maxData).size();
vector<vector<int>>vecs;
int mod = 10;
int dev = 1;
for (size_t i = 0; i < len; mod*=10,dev*=10,i++)
{
vecs.resize(20);
for (size_t j = 0; j < size; j++)
{
int index = arr[j] % mod / dev+10;
vecs[index].push_back(arr[j]);
}
int idx = 0;
for (auto vec : vecs)
{
for (int v:vec)
{
arr[idx++] = v;
}
}
vecs.clear();
}
(3)性能分析与优化
最好时间复杂度:O(nd)(d为最大数据的长度)
平均时间复杂度:O(nd);
最坏时间复杂度:O(nd)
空间复杂度:O(n)(O(n+k))(k为桶/基数的个数)
稳定性:稳定
适用场景:整数、固定长度的字符串排序(适用范围较窄,在处理海量整数时,速度可以碾压前面三种 O(n log n) 的算法)
四种高级排序算法的比较

对于一般无序的大规模数据一般:快速排序 > 归并排序 ≈ 堆排序 > 基数排序