排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
稳定的意义:
稳定的排序算法不会改变相同的值的相对顺序,而相同的值可能带有不同的含义。
比如将学生按总分排名,如总分相同,按数学排名。那么先将他们按数学排名,然后使用稳定的算法对总分排名,这样就能保证总分相同时数学排名的相对位置不变。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
我们之前已经学过冒泡排序和堆排序,下面简单复习一下。
注:本文的所有排序默认排升序
冒泡排序一般是第一个学习的排序算法。我们也用它模拟过qsort的传参实现了一个通用的冒泡排序qsort函数用法 | 模拟qsort实现一个通用的冒泡排序_世真的博客-CSDN博客
下面简单复习一下。
代码如下:
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
int flag = 0;
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
flag = 1;
Swap(&a[j], &a[j + 1]);
}
}
if (flag == 0)
break;
}
}
这里定义的flag
是对冒泡排序的优化。
我们知道,冒泡排序最坏的情况,时间复杂度为 O ( n 2 ) O(n^2) O(n2),最好的情况呢?
最好情况就是数组已经有序,但是两层循环依然会全部走一遍,进行两两比较,时间复杂度依然是 O ( n 2 ) O(n^2) O(n2)
所以我们定义一个flag
,当某一趟排序没有发生交换时,就说明数组已经有序,然后跳出循环。
这样对于接近有序的序列就提高了排序效率。
在学习堆的时候已经是实现了。[数据结构](8)二叉树顺序结构之堆|堆实现|堆排序|TOPK_世真的博客-CSDN博客
这里贴出代码:
void AdjustDown(int* a, size_t size, size_t root)
{
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && 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)
{
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
size_t end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
一般也被称为直接插入排序,插入排序是一种最简单的排序方法。
基本思想就像我们玩斗地主抓牌,开始时我们的左手为空,每次摸到牌之后,我们就会选择一个合适的位置将这张牌插入进去,为了找到合适的位置,我们将它与已在手中的每张牌进行比较。这样手中的牌就总是有序的。
插入排序一般都是最后一个元素插到前面的元素中,但是前面的元素必须有序。那么可以把第一个元素看成有序,第二个元素往里面插,这样前两个元素有序,然后第三个元素往里面插,前三个就有序,第四个往里面插 ⋯ ⋯ \dotsi\space\dotsi ⋯ ⋯
单趟插入排序:假设下标[0,end]
的元素有序,下标end+1
的元素和前面的元素依次从后往前比较,比它大的都往后移动,直到遇到比它小的,就把它插入到小的元素的后面。或者与[0,end]
个元素都比较完毕,就把它插入到最前面。
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
int end = i;
// 单趟排序:[0, end]有序 end+1位置的值,插入进入,保持他依旧有序
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 ) O(n) O(n)
最坏情况:逆序,每个元素都要与前面所有元素都比较一次,等差数列求和。时间复杂度为 O ( n 2 ) O(n^2) O(n2)
对于插入排序来说,接近有序的序列排序是比较快的。
空间复杂度 O ( 1 ) O(1) O(1)
与冒泡排序比较
对于顺序有序的数组,如 1, 2, 3, 4, 5, 6, 8, 7
插入排序只需要走一趟,比较8次。
冒泡排序走第一趟可以排到有序,但是发生了交换,flag不为0,所以还要再走一趟,比较7 + 6 = 13次。不优化要比较28次。
对于局部有序的数组,如 3, 4, 2, 8, 7, 9, 5, 9
插入排序只要比较12次。
冒泡排序要比较22次,不优化也要比较28次,优化的效果不明显。
所以,顺序有序,插入排序和冒泡排序是差不多的。
但是如果局部有序,或者接近有序,那么插入排序的适应性更高。
希尔排序是插入排序的一种,是直接插入排序的一种更高效的改进版本。
我们知道插入排序在数组接近有序时更好,但是大多数情况都是无序的,这个要求太高了。
希尔排序则是将排序分为两部分。
预排序就是将元素进行分组,如gap = 3
,就是每间隔为3的元素为一组,总共分为3组。
然后分别对这gap组元素进行插入排序。
这样排出来的结果为1 3 2 4 6 2 7 9 5 8
明显比原数组更接近有序了。
并且不难发现,gap越小越接近有序。gap越大,小的数可以更快到前面,大的数可以更快到后面。
先来写这部分代码:
第一层循环控制每一组的第一个元素,第二次循环对这一组进行插入排序。
int gap = 3;
for (int i = 0; i < gap; ++i) //控制组
{
for (int j = i; j < n - gap; j += gap) //控制每一组中的插入排序
{
int end = j;
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;
}
}
也可以写成这样,只要两层循环:
第一组先排前两个,然后去排第二组的前两个,接着排第三组的前两个,然后排第一组的前三个 ⋯ ⋯ \dotsi\space\dotsi ⋯ ⋯
int gap = 3;
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初始给多少?不同的数据量肯定要设定不同的gap;而且为了使数组越来越接近有序,gap肯定是要缩小的,怎么缩小?
其实gap的给值和缩小官方并没有给固定的方式。这边一种建议的写法就是初始为n / 3 + 1
,以后每一次都有gap = gap / 3 + 1
直到gap为1。
gap > 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;
}
}
}
希尔排序快不快?就这样看没什么感觉,我们来测试一下。
用以下测试代码生成一些随机数,然后分别对不同的排序算法计时
void TestOP()
{
srand(time(0));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
BubbleSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("BubbleSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
free(a1);
free(a2);
free(a3);
free(a4);
}
这边拿10万个数测试了一下。结果非常的amazing啊!希尔排序不仅远比插入排序快,甚至赶上了堆排序。
所以希尔排序还是非常快的,下面来讨论一下希尔排序的时间复杂度。
希尔排序的时间复杂度很难计算,这里给出一个粗略的估计:
gap很大时,数据跳得很快,里面的循环几乎可以忽略不计,差不多是 O ( n ) O(n) O(n)。当gap很小时,数组已经很接近有序,差不多也是 O ( n ) O(n) O(n)。最外层可控制gap的循环,每次令gap/3,知道最后为1,差不多是 O ( log 3 n ) O(\log_3n) O(log3n)。所以总的时间复杂度大概就是 O ( n log 3 n ) O(n\log_3n) O(nlog3n)
具体的计算涉及一些数学上的难题。因为gap的变化不固定,很多书中给出的希尔排序的时间复杂度也不固定。比如有些地方也给出了 O ( n 1.3 ) O(n^{1.3}) O(n1.3)的时间复杂度。
选择排序也是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,然后与序列第一个元素交换,然后从剩余未排序元素中选出一个最小的,与已排序序列后一个元素交换,以此类推,直到待排序的数据元素的个数为零。
与堆排序的不同就在于,它是通过遍历选出最大或最小的元素,这肯定不如堆的调整快。
代码很简单,我们直接写一个优化版本,一次遍历同时选出最大和最小两个数。
void SelectSort(int* a, int n)
{
int left = 0, right = n - 1;
while (left < right)
{
int mini = left, maxi = left;
for (int i = left + 1; i <= right; ++i)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[left], &a[mini]);
// 如果left和maxi重叠,修正一下maxi即可
if (left == maxi)
maxi = mini;
Swap(&a[right], &a[maxi]);
left++;
right--;
}
}
注意修正left和maxi重叠的情况,因为最小的元素和left位置的元素交换,如果原来left位置的元素就是最大的,那么它会被换到mini的位置。
时间复杂度: O ( n 2 ) O(n^2) O(n2),最好和最坏情况都是 O ( n 2 ) O(n^2) O(n2)
到这里 O ( n 2 ) O(n^2) O(n2)的排序我们学了三个:冒泡排序、插入排序、选择排序。
对于接近有序的数组,插入排序和优化后的冒泡排序都有一定的适应性,而选择排序识别不了有序的情况。对于完全无序的数组,选择排序一次选两个比冒泡好一些。
总体而言,插入排序更优一些,顺序有序,局部有序都有一定的适应性。
快速排序内容较多,下面都是“硬菜“,也是重点。
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
将序列分成两子序列的方式有三种。
这是Hoare最初写的版本,使用的是相向双指针。
思考:
双指针一定会相遇吗?
如果选第一个数为key,那么相遇位置的值一定要比key小才能交换,相遇位置的值比key小怎么保证?
答:
双指针一定会相遇,两指针相向而行,并且每次只走一步,相遇就停止循环。
右指针先走可以保证相遇位置的值比key小,因为右指针找到最后一个小于key的值后,左指针走,左指针会因为与右指针相遇而跳出循环,此时这个位置的值必然比key值小。
如果选最后一个数为key,相遇位置的值应该比key大,那么应该让左指针先走。
上图是右指针去遇到左指针,之后,左指针判断与右指针相遇就没有继续走。实际也会有左指针遇右指针的情况,一样是左指针走,判断到与右指针相遇便跳出循环。
我们先写这部分代码:
因为除了第一次划分,后面要划分的序列都是一段一段的,所以参数传入的是一段区间[left, right]
,最后不要忘了返回key
int PartSort(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
// 找小
while (left < right && a[right] >= a[keyi])
--right;
// 找大
while (left < right && a[left] <= a[keyi])
++left;
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
这是一种更好理解的方法
这样坑位其实一直在左右指针之间传递,其中一个挖好最后一个坑之后,另一个指针会因为与它相遇而停止,所以相遇位置必然是个坑位。
与Hoare的版本比起来,挖坑法不用理解为什么最终相遇位置的值比key值小,也不用理解为什么左边作key右指针先走,右边作key左指针先走。一个挖坑一个填,是非常自然的,所以说比较好理解。
代码实现:
int PartSort2(int* a, int left, int right)
{
int key = a[left];
// 坑位
int pit = left;
while (left < right)
{
// 右边先走,找小
while (left < right && a[right] >= key)
{
--right;
}
a[pit] = a[right];
pit = right;
// 左边走,找大
while (left < right && a[left] <= key)
{
++left;
}
a[pit] = a[left];
pit = left;
}
a[pit] = key;
return pit;
}
perv
指向第一个数,cur
指向第二个数,选第一个数作为keycur
往右找比key
小的数,找到后prev++
,然后和prev
位置的数交换。cur
走到末尾,然后将key
与prev
交换遇到小的就给prev
,遇到大的cur
走prev
不走,这样就能保证cur
和prev
之间的数是比key大的。
代码实现:
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && a[++prev] != a[cur])
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
这个方法的好处就是代码容易写,不容易错。
这三种划分的时间复杂度都是 O ( n ) O(n) O(n),性能差异不大。
每划分一次,就有一个key已经放到了正确的位置。这个位置就不需要动了。接下来就是分别划分左子序列和右子序列。像这样层层划分,问题规模越来越小,显然可以用递归分治的方法。
递归三要素:
[begin, end]
区间的元素排序,所以参数int* a, int begin, int end
,返回类型void
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
时间复杂度计算:
最好情况: O ( n log n ) O(n\log n) O(nlogn)
每次划分选择的key都正好是这个区间的中位数,这样每一次划分都正好把区间分为两半。
划分的时间复杂度是 O ( n ) O(n) O(n),而每一层有n个数,每一层的时间复杂度就是 O ( n ) O(n) O(n),层数为 log 2 n + 1 \log_2n+1 log2n+1,所以总的时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)
最坏情况: O ( n 2 ) O(n^2) O(n2)
每次划分选择的key都正好是这个区间的最值,这样就和选择排序差不多了,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
对于无序数组,每次选到最大值或最小值那运气就太背了,可是对于接近有序的数组呢?而且递归层数太深,还很容易出现栈溢出。这么一看,这快排也不行啊,那为什么c、c++库里的排序都是快速排序?
如果不优化,你的快排可能过不了这题912. 排序数组 - 力扣(LeetCode) (leetcode-cn.com)
三数取中,就是从区间第一个数,中间一个数和末尾的一个数中选择中位数作key。
写一个函数传入这段区间,获得三数取中后的值的下标,逻辑和代码如下:
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right]) return mid;
else if (a[left] > a[right]) return left;
else return right;
}
else // a[left] >= a[mid]
{
if (a[mid] > a[right]) return mid;
else if (a[left] < a[right]) return left;
else return right;
}
}
接下来只要在划分前把选出的值换到第一个数的位置,然后取第一个数为key,剩下的不变。
int PartSort(int* a, int left, int right)
{
int midi = GetMidIndex(a, left, right);
Swap(&a[midi], &a[left]);
int keyi = left;
//...
}
这样快排就没有什么大的缺陷了。
通过分析时间复杂度的最好情况可以看出,快排的递归很像一棵满二叉数,层数越深,区间越小,需要递归的次数越多。对于小区间,继续递归调用函数显得不太划算。不如直接用插入排序。
void QuickSort2(int* a, int begin, int end)
{
if (begin >= end)
return;
// 小区间直接插入排序控制有序
if (end - begin + 1 <= 10)
{
InsertSort(a + begin, end - begin + 1);
}
else
{
int keyi = PartSort3(a, begin, end);
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
}
递归虽然代码简单,但是有栈溢出的风险。有些时候可能要改写迭代版本。
我们知道,递归的原理和栈相似。递归版本每次都要传入一个区间的左右两端,我们的迭代版本也可以通过栈来保存区间左右两端。
栈在这里[数据结构](6)栈和队列_世真的博客-CSDN博客
有详细注释的代码如下:
void QuickSort3(int* a, int begin, int end)
{
//创建并初始化栈,将区间左右端入栈
ST st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
//出栈,先出栈的是右端,后出栈的是左端
int right = StackTop(&st);
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
int keyi = PartSort3(a, left, right);//划分
// 接下来对左右子区间进行划分,划分前要先判断左右区间的元素个数是否大于1,如果小于等于1则不需要划分,也就不需要入栈。
if (left < keyi - 1)
{
StackPush(&st, left);
StackPush(&st, keyi - 1);
}
if (keyi + 1 < right)
{
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
}
StackDestory(&st);
}
其实用队列也是可以的。都是成对的入,成对的出,最终只要能把所以最小子区间都划分到就可以了。
归并的思想很简单,也很容易想到。你可能在这道题中已经用过归并21. 合并两个有序链表 - 力扣(LeetCode) (leetcode-cn.com)
归并排序就是把已经有序的子序列合并,得到完全有序的序列。要使子序列有序,则需要继续分解,直到不可分解再开始合并。
这显然也是可以分治递归实现。
重新开辟一个数组tmp用来存放合并后的序列,由于开辟tmp数组不能放到递归函数里,所以我们再写一个子函数递归。
递归三要素:
[begin, end]
的元素排序。传入参数int* a, int begin, int end, int* tmp
,返回类型void
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;// 这里是向下取整,如果分割为[begin, mid-1][mid, end]就不均匀,容易死循环。
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
// 归并[begin, mid][mid+1, end]
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int index = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
assert(tmp);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
如图所示,我们只要控制好子区间的大小和子区间的位置也能实现归并。
但是这不容易控制,不是所有序列的元素个数都是 2 n 2^n 2n,比如6个数的序列,按1个元素大小的区间归并没问题,而2个元素大小的区间只能分成3组,最后一组没有区间和它归并,如果不处理,就会出现越界。
有详细注释的代码如下:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)// 控制子区间的大小gap
{
for (int i = 0; i < n; i += 2 * gap)// 控制要归并的两个区间的位置
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
// end1 越界,修正
if (end1 >= n)
end1 = n - 1;
// begin2 越界,说明第二个区间不存在,那么直接给个不存在的区间
if (begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
// begin2 合法, end2越界,修正end2即可
if (begin2 < n && end2 >= n)
end2 = n - 1;
//归并
int index = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
}
memcpy(a, tmp, n * sizeof(int));
gap *= 2;
}
free(tmp);
}
时间复杂度: O ( n log n ) O(n\log n) O(nlogn)
每一层有n个数,排序的时间复杂度为 O ( n ) O(n) O(n),一共有 log 2 n + 1 \log_2n+1 log2n+1层,所以时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)
和快速排序不同的是,它是绝对的二分,不受选key的影响。
空间复杂度: O ( n ) O(n) O(n)
创建了大小为n的tmp
数组,空间复杂度 O ( n ) O(n) O(n);递归深度为 log 2 n + 1 \log_2n+1 log2n+1,每一层递归开辟常数个空间,所以递归的空间复杂度为 O ( log n ) O(\log n) O(logn),总的空间复杂度为 O ( n ) O(n) O(n)
迭代也要开辟tmp
数组,所以空间复杂度也是 O ( n ) O(n) O(n)
最终对决:堆排序,希尔排序,快速排序,归并排序四个最强排序放到一起比一比
内排序是指数据在内存上进行排序;外排序指数据在磁盘上,数据量通常很大。
对于数据量小的内排序,归并排序在空间复杂度上占不到优势。但是对于外排序,只有归并排序能做。
这就涉及到文件操作了,这里就不细讲了。
以上都是比较排序,计数排序是非比较排序。计数排序的思想也比较简单,我们在oj题中也可能用到过。
计数排序是通过哈希数组统计数据个数,最后遍历哈希数组输出有序序列。
如要排序的序列为{ 10, 3, 5, 8, 4, 1, 3, 1 }
开辟一个大小为11的哈希数组并初始化为0
遍历序列,以待排元素的值为下标使哈希数组相应位置的值加1。
遍历哈希数组,数组中的值代表该下标输出次数。
哈希数组开的大小取决于待排元素值大小的跨度,对于跨度在500~1000内的序列,只需要开大小为501的数组,然后对每个值加偏移量即可。待排序列中有负数也同理。
缺点:可排序的类型有限,比如浮点型,字符串。对于值特别分散的序列,时间和空间复杂度都较高。
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* countA = (int*)calloc(range, sizeof(int));
assert(countA);
// 计数
for (int i = 0; i < n; ++i)
{
countA[a[i] - min]++;
}
// 排序
int j = 0;
for (int i = 0; i < range; ++i)
{
while (countA[i]--)
{
a[j++] = i + min;
}
}
}
时间复杂度: O ( r a n g e + n ) O(range+n) O(range+n)
空间复杂度: O ( r a n g e ) O(range) O(range)
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
希尔排序 | O ( n log n ) ∼ O ( n 2 ) O(n\log n)\sim O(n^2) O(nlogn)∼O(n2) | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
堆排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( 1 ) O(1) O(1) | 不稳定 |
归并排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n ) O(n) O(n) | 稳定 |
快速排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( log n ) ∼ O ( n ) O(\log n)\sim O(n) O(logn)∼O(n) | 不稳定 |
快排,归并,希尔是我们学习排序的重中之重,难度也相对比较大。