目录
一、引言
在我们的生活中处处可见排序,期末考试成绩排名、商品价格榜单、福布斯富豪榜等等。这些排名都需要排序算法来帮忙,由此可见排序算法的应用价值是巨大的。
二、常见的排序算法
常见的排序算法可以按不同的思想大概分为4类:
三、深度剖析各种排序
由于以下排序许多都要用到交换函数,这里就先写上。注意:因为我们想着是形参的改变影响实参,所以这里要传址。用指针对两个数进行交换。
//交换函数
void Swap(int* p, int* q)
{
int tmp = *p;
*p = *q;
*q = tmp;
}
冒泡排序
冒泡排序简介
冒泡排序作为一种容易理解,代码量少的排序深得编程小白们的喜爱。许多小白第一个接触到的排序就是冒泡排序。
冒泡排序的原理:从左到右,相邻元素进行比较。每次比较一轮,就会找到序列中最大的一个或最小的一个。这个数就会从序列的最右边冒出来。
以从小到大排序为例,第一轮比较后,所有数中最大的那个数就会浮到最右边;第二轮比较后,所有数中第二大的那个数就会浮到倒数第二个位置……就这样一轮一轮地比较,最后实现从小到大排序。
核心思想:跟隔壁的元素比较,比我大(小)就交换。
以下动图是以排升序为例,大的放后面。
代码实现:
//冒泡排序
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (a[j] > a[j + 1]) {
Swap(&a[j], &a[j + 1]);
}
}
}
}
时间复杂度:
-
平均时间复杂度:O(n^2)
-
最好情况时间复杂度:O(n)
-
最坏情况时间复杂度:O(n^2)
稳定性:稳定
优点:冒泡排序简单,理解和上手都较为容易。
缺点:冒泡排序效率低下,在实际生活当中(上万上亿的数据)被运用的机会很少
选择排序
选择排序简介
基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
代码实现
代码讲解:先定义头是begin,尾是最后一个元素的下标。 只要begin小于end就继续循环,先定义mini并把begin的值给mini,然后for循环里面的就是找出一轮当中的最小值,然后交换begin位置的值和mini位置的值,相当于把最小的放到前面。走完一轮后,begin++。然后直到begin=end时退出循环,那么这时就排好序了。
//选择排序
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin;
for (int i = begin + 1; i <= end; i++)
{
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[mini], &a[begin]);
begin++;
}
}
优化版:同时找到一个最小的和最大的放在头和尾,然后同时找次小和次大的放在第二个位跟倒数第二个位。以此类推......
//选择排序
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[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[mini], &a[begin]);
if (begin == maxi)
maxi = mini;
Swap(&a[maxi], &a[end]);
begin++;
end--;
}
}
注意: 当begin的位置的最大的数时,mini和begin位置的数交换完后,最大的数现在的位置在mini处,因此要把mini的值给maxi,这样之后maxi的值再与end位置的值交换,才算把最大的放到了后面。不然就会出现刚把最小的数交换到begin的位置,又把这个小的数换到后面去了的情况。
时间复杂度:
-
平均时间复杂度:O(n^2)
-
最好情况时间复杂度:O(n^2)
-
最坏情况时间复杂度:O(n^2)
稳定性:不稳定
优点:也比较简单,容易理解。
缺点:效率仍旧不高。
直接插入排序
插入排序简介:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为 止,得到一个新的有序序列 。 实际中我们玩扑克牌时,就用了插入排序的思想。
代码实现:
先定义end=i,tmp=a[end+i],然后进入循环进行单次调整,如果tmp比a[end]小,那么就让end后面的数等于end坐标的数,同时让end--,相当于把原来end+1坐标的值存在tmp中往前挪,而end坐标的值向后挪。当tmp大于a[end]时即没法再插到前面时,退出循环,让这时的a[end+1]的值=tmp。上面的是一轮的操作,然后for循环控制多轮操作,直至排好序为止。
//插入排序
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++) {
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(n)
-
最坏情况时间复杂度:O(n^2)
稳定性:稳定
优点:比冒泡排序快了
缺点:效率仍不高
希尔排序
希尔排序简介
自希尔排序开始,排序算法就开始上难度了。希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成gap个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取gap = gap / 2(gap越大则大的数越快到后面,但是gap越大总体上看整个数组越接近无序,所以先让gap为大的值让大数快点跳到后面,再慢慢让gap变小让原数组越来越接近有序),重复上述分组和排序的工 作。当到达gap=1时进行的就是插入排序,这样以后所有数就都排好了。
其实希尔排序分组的思想就是预排序的过程,先把分好的具有相同距离的一组排好序,然后再把几组放在一起排序,这样比直接插入排序会有效率提升。
下面的图以初始的gap = 5,并且每一轮后gap = gap / 2为例,在同一条直线上的为同一组数
我们可以看到,一开始gap=5时我们把10个数分成了5组,每组为一条横线连接的两个数(每个数都在同组的数的后gap个)。进行第一趟排序,第一组为9和4,因为4小于9,所以让tmp=4,原来放4的位置放9,4插到原来9的位置;然后到第二组为1和8,因为8大于1,所以不用变,以此类推一共走5组。第二趟排序gap = 5 / 2(gap=2);这时第一组有4、2、5、8、5,然后把2插到4的前面,然后再把后面的5插到8的前面。同样的第二组也进行一样的操作。最后当第三趟排序的时候gap=1就直接插入排序了。
以上为针对图片的讲解,下面看看动图可能会更加容易理解。但是下面的动图原始数据与上图不一样,所以看排序过程是怎么进行即可。
代码实现:
//希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 2;
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;
}
}
}
但是由于gap = gap / 2分的组比较多,要走比较多趟。经研究gap = gap / 3 +1(这里加一是为了保证最后一次gap=1,最后一次为插入排序)。
下面为优化版的代码:
//希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
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;
}
}
}
时间复杂度:
-
平均时间复杂度:O(nlogn ~ n^2)
-
最好情况时间复杂度:O(n^1.3)
-
最坏情况时间复杂度:O(n^2)
稳定性:不稳定
优点:运用了预排序,比直接插入更快
快速排序
快速排序简介
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
单趟动图展示
解释:先令key等于6,即将6作为基准值。定义一个end,让end=最后一个数的坐标,然后先从右边开始比较,找到一个比6小的数后停下来(没找到就一直让end往左挪一个)。定义一个begin,让begin=最前面的数的坐标,然后从最左端开始比较,找到一个比6大的数就停下来(没找到就一直让begin往右挪),此时再交换begin和end坐标的值,交换是为了把比key小的数放在左边,比key大的放在右边。交换完之后继续在右边找比key小的,在左边找比key大的交换,直到behin>=end为止。这个时候再把key的值和相遇位置的值交换,交换完后key的左边就都是比key小的数,key的右边就都是比key大的数。
然后将需要排序的数分为key的左边的数和key右边的数,再递归下去排好左边和右边。
代码实现
//快速排序
void QuickSort(int* a, int left, int right)
{
int keyi = left;
int begin = left;
int end = right;
if (left >= right)
return;
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;
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
这个是最原始的霍尔版本的快速排序,但是这样设置的keyi有可能不是中间的数,这样就很可能造成左边或者右边要排序的范围很大,从而造成时间的浪费。
为了解决这个办法,我们引入一个叫三数取中法的办法。目的是让keyi坐标下的参考值尽量处于中间。
//三数取中法
int Getmid(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else //a[left] > a[mid]
{
if (a[right] < a[mid])
{
return mid;
}
else if (a[right] < a[left])
{
return right;
}
else
{
return left;
}
}
}
void QuickSort(int* a, int left, int right)
{
int keyi = Getmid(a, left, right);
int begin = left;
int end = right;
if (left >= right)
return;
while (begin < end)
{
//右边找小
if (begin < end && a[end] >= a[keyi])
{
end--;
}
//左边找大
if (begin < end && a[begin] <= a[keyi])
{
begin++;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[keyi], &a[begin]);
keyi = begin;
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
但由于递归的原因,当需要排序的数很少时进行递归反而会浪费大量的时间,为此我们再作出优化的方案。当需要排序的数少于10个时,我们就直接插入排序降低时间成本。
void QuickSort2(int* a, int left, int right)
{
if (left >= right)
return;
//少于10个数用插入排序
if (right - left + 1 < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
int midi = Getmid(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
int begin = left;
int end = right;
while (begin < end)
{
//右边找小
if (begin < end && a[end] >= a[keyi])
{
end--;
}
//左边找大
if (begin < end && a[begin] <= a[keyi])
{
begin++;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[keyi], &a[begin]);
keyi = begin;
QuickSort2(a, left, keyi - 1);
QuickSort2(a, keyi + 1, right);
}
}
另外介绍两种快速排序的优化方案。
1、挖坑法
如图所示
第一步:同样的确定一个数作为基准值key,把begin(left)的值放到key,将原来begin位置设置为坑位,然后从右边开始移动(end--) ,右边找比key小的数。
第二步:将右边找到的这个比key小的值放到原来begin(left)的位置,然后把这个比key小的值的位置设置为坑位。
第三步:从左边开始移动,找比key大的数。把这个比key大的数放到新的坑位上。把现在这个位置更新为新的坑位。
重复第二、第三步,直到left和right(begin和end)相遇。相遇后把key的值放到坑位中。
递归下去进行多趟排序。(这里调用刚刚的QuickSort2进行递归)
代码实现
//快速排序挖坑法
void QuickSort3(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left;
int end = right;
int key = a[left];
int keng = left;
while (begin < end)
{
while (begin < end && a[end] >= key)
{
end--;
}
a[keng] = a[end];
keng = end;
while (begin < end && a[begin] <= key)
{
begin++;
}
a[keng] = a[begin];
keng = begin;
}
a[keng] = key;
QuickSort2(a, left, keng - 1);
QuickSort2(a, keng + 1, right);
}
2、快慢指针法
如图所示
第一步:开始时,令prev指向初始key的位置,让cur指向prev后面的一个。
第二步:判断cur指向的数据是否小于keyi的数据,如果cur指向的数据大于keyi的数据,则cur往后移一位,prev指向的数据保持不动。当cur指向的数据小于keyi的数据时,将prev后移一位,然后将cur指向的值和prev指向的值交换。
第三步: cur再往后移,重复第一第二步,直到cur到数据最后一个的后面一个(越界)。
//快排快慢指针法
void QuickSort4(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] <= a[keyi] && ++prev != cur)
Swap(&a[cur], &a[prev]);
cur++;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
QuickSort2(a, left, keyi);
QuickSort2(a, keyi + 1, right);
}
解决了上面的问题后,我们再想想能不能再提高些效率呢。我们上面的快速排序都要用到递归的办法,递归会调用栈帧,调用的次数太多,递归深度太深就有可能造成栈溢出。为了解决这个问题,我们想想能不能有不用调用递归的方法。
我们可以用栈来模拟递归的过程,其实每次递归我们都只需要知道数据最左和最右的下标是什么。那么我们只需要把最左和最右的下标放到栈中。利用栈后进先出的特点,就可以模拟递归的过程。
以下是栈的实现代码和非递归的代码,栈的实现代码解说在我的另一篇栈和队列的推文中有提及,可点开博主主页查看。
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
typedef int STDataType;
typedef struct Stack {
STDataType* arr;
int top;
int capacity;
}ST;
//栈的初始化
void STInit(ST* pst);
//栈的销毁
void STDestroy(ST* pst);
//打印栈
void STPrint(ST* pst);
//入栈
void STPush(ST* pst, STDataType x);
//出栈
void STPop(ST* pst);
//查找栈顶元素
STDataType STFind(ST* pst);
//栈的判空
bool STEmpty(ST* pst);
//获取栈中数据数量
int STSize(ST* pst);
#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"
//栈的初始化
void STInit(ST* pst)
{
assert(pst);
pst->arr = NULL;
pst->top = 0;
pst->capacity = 0;
}
//栈的销毁
void STDestroy(ST* pst)
{
assert(pst);
free(pst->arr);
pst->arr = NULL;
pst->top = pst->capacity = 0;
}
//打印栈
void STPrint(ST* pst)
{
int i;
for (i = 0; i < pst->top; i++) {
printf("%d ", pst->arr[i]);
}
printf("\n");
}
//入栈
void STPush(ST* pst, STDataType x)
{
assert(pst);
//扩容
STDataType* tmp;
if (pst->top == pst->capacity) {
int newcapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity;
tmp = (STDataType*)realloc(pst->arr, newcapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("realloc fail\n");
return;
}
pst->arr = tmp;
pst->capacity = newcapacity;
}
pst->arr[pst->top] = x;
pst->top++;
}
//出栈
void STPop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
//查找栈顶元素
STDataType STFind(ST* pst)
{
assert(pst);
assert(pst->top > 0);
return pst->arr[pst->top - 1];
}
//栈的判空
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0;
}
//获取栈中数据数量
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
我们每次只需把数据中最开头跟最后的数据下标放入栈中,需要时取出即可。
//快排非递归法
void QuickSortNonR(int* a, int left, int right)
{
ST st;
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st))
{
int begin = STFind(&st);
STPop(&st);
int end = STFind(&st);
STPop(&st);
int keyi = PartSort4(a, begin, end);
//[begin, keyi - 1] [keyi, 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(nlogn)
-
最好情况时间复杂度:O(nlogn)
-
最坏情况时间复杂度:O(n^2)
辅助空间:O(logN)~O(N)
稳定性:不稳定
优点:快就完事了,实际上经过实践,霍尔版的优化比快慢指针法要快。
堆排序
堆排序简介
秉持升序建大堆,降序建小堆的原则先采用向下建堆法建堆(这里排升序要建大堆)。从最后一个非叶子节点开始调,调整以该节点为根的子树,让该子树成为大堆。
代码实现:
向下调整具体实现:先定义孩子节点为父节点乘2加1,只要孩子节点没有超过最后的元素(没有越界)就继续循环。如果右孩子大于左孩子,就让孩子节点++。如果孩子节点大于父节点,则交换父子节点的值,child给给parent,再更新child节点。如果不满足就退出。
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
{
child++;
}
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
堆排序:
先向下调整建好大堆,再开始排序。把现在大堆的堆顶与堆的最后一个元素交换,这样就确定了最大的数放在了最后。然后再把现在堆顶的元素向下调整,但这时最后的元素就是倒数第二个数(因为最大的数已经放到最后了,不用排他)。一直这样操作下去直到所有数都排好序。
//堆排序
void HeapSort(int* a, int n)
{
for (int i = (n - 2) / 2; i >= 0; i--) {
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[end], &a[0]);
AdjustDown(a, end, 0);
end--;
}
}
时间复杂度
-
平均时间复杂度:O(nlogn)
-
最好情况时间复杂度:O(nlogn)
-
最坏情况时间复杂度:O(nlogn)
稳定性:不稳定
优点:也挺快的
缺点:当数据量比较小的时候,反而用堆排序有点浪费时间。
归并排序
归并排序简介:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
简单点来说就是先把要排序的数字分成几组,然后先把各组内的数字排好,然后再各组之间排序。
动图演示:
我们拿一组数为例。这组数一共有8个,我们先通过递归分解数组,把数组每次递归都分为左右两部分,递归到最后一层的时候就组内两两比较,比较后开始合并。
代码实现:
我们要令开一个空的数组来放排好序的数,然后调用_MergeSort函数进一步实现排序。假如left>=right就返回(作为退出递归的条件)。定义mid为中间的数。然后进行左递归和右递归,当递归到最后一层后,定义begin1,end1,begin2,end2。只要begin1和begin2没有越界就进入循环,哪个小就放哪个进tmp数组。当退出循环时,有可能begin1或者begin2没到头,那么就分别循环把剩下的数都放在tmp中。最后把tmp中的数复制回a数组中。
void _MergeSort(int* a, int* tmp, int left, int right)
{
if (left >= right)
return;
int mid = (left + right) / 2;
//[left, mid] [mid + 1, right]
_MergeSort(a, tmp, left, mid);
_MergeSort(a, tmp, mid + 1, right);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + left, tmp + left, (right - left + 1) * sizeof(int));
}
//归并排序
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
tmp = NULL;
}
这样就能完成归并排序,但是有没有优化版呢。因为这个版本的归并排序运用了递归的算法,递归会调用栈帧,有可能导致栈溢出。所以我们就想有没有非递归的办法排序。
我们可以像动图演示那样,先两两归并排序,再四四归并排序......
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
int gap = 1;
while (gap < n)
{
for (int j = 0; j < n; j += 2 * gap)
{
int begin1 = j, end1 = j + gap - 1;
int begin2 = j + gap, end2 = j + 2 * gap - 1;
if (begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
int i = j;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + j, tmp + j, (end2 - j + 1) * sizeof(int));
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
时间复杂度
-
平均时间复杂度:O(nlogn)
-
最好情况时间复杂度:O(nlogn)
-
最坏情况时间复杂度:O(nlogn)
空间复杂度:O(n)
优点:也挺快的
缺点:当数据量比较小的时候,反而用归并排序有点浪费时间。
计数排序
计数排序是除七大排序外另一个有趣的排序。他的核心思想就是新开一个数组存放数组最小到数组最大值,遍历一遍数组,数组中出现了什么数字,就让新数组的对应数字++。例如:要排序的数组中出现了3次10,我们就让新数组10的次数++三次。
//计数排序
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
int i = 0;
for (i = 0; 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));
for (i = 0; i < n; i++)
{
count[a[i] - min]++;
}
int j = 0;
for (i = 0; i < range; i++) {
while (count[i]--)
{
a[j++] = i + min;
}
}
free(count);
count = NULL;
}
时间复杂度:O(MAX(N,范围))
空间复杂度:O(范围)
稳定性:好
四、排序总结
排序情况 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
简单选择排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
直接插入排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
希尔排序 | O(NlogN)~O(N^2) | O(N^1.3) | O(N^2) | O(1) | 不稳定 |
快速排序 | O(NlogN) | O(NlogN) | O(N^2) | O(logN)~O(N) | 不稳定 |
堆排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(1) | 不稳定 |
归并排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(N) | 稳定 |