排序算法
排序算法
序
排序就是一串记录,按照其中某个或者某些关键字的大小,使之整体递增或者递减的操作。排序虽然基础但却十分有用,例如,在对各项数据进行操作的时候,往往第一步就是进行排序,这会使得后续操作简化。
排序算法有多种,不同的情况下可以使用不同的排序算法。如何进行选择呢?首先需要知道以下几个衡量排序算法的标准:
一、复杂度
复杂度实际上说的是程序运行所需的资源,其中又包含了时间资源和内存上的资源。这样复杂度可以分为空间复杂度和时间复杂度。
1、空间复杂度指的是算法在运行过程中所占据的存储空间的量。
2、时间复杂度也可以简单理解为运行程序时间,它实际上是一个函数,用 \(T(n)\)表示。评估一个算法的时间复杂度往往看到的是\(O(n)\),这是使用的大\(O\)表示法,实际上指的是渐进时间复杂度,是一种简化,只保留 \(T(n)\)中的阶数最高的项,并且去掉系数,如果是常数量级则用\(O(1)\)表示。在实际估算算法的时间复杂度的时候,只需要找出算法中重复次数最多的语句的频次,保留最高次幂。
例如单层循环的时间复杂度为\(O(n)\),双层嵌套循环的时间复杂度为\(O(n^2)\)。但是需要了解的是,在一定情况下复杂度为\(O(n)\)的算法效率未必一定高于\(O(n^2)\),例如
明显可以看出,当数据少于\(200\)时,\(O(n^2)\)的算法效率优于\(O(n)\)。因此数据少量的情况下,也许复杂度高的算法效率更优。
二、稳定性
在待排序的序列中,存在具有多条相同关键字的记录,若是经过排序,这些记录的相对次序不变,即原序列中\(A[i] = A[j]\)并且\(A[i]\)在\(A[j]\)之前,在排序之后的序列中,\(A[i]\)仍在\(A[j]\)之前 ,则该排序算法是稳定的,否则该算法是不稳定的。
讨论稳定性的意义在于待排序的对象是一个具有多属性的复杂对象,且初始序列具有意义。
例如:我们在商场中购买某样物品,物品的价格本身是从低到高进行排序的,现在需要考虑的是,优先买销量高的物品,同时销量相同的情况下购买低价的物品。在这种情境下,我们就可以使用具有稳定性的排序算法。
按照排序的过程,又可以分为比较类排序和非比较类排序
注:
以下例题为后续算法中代码所解决的问题:
给定\(n\)个元素,输出这\(n\)个元素从小到大的顺序(第一行读入一个\(n\),接下来读入\(n\)个元素,每个元素不超过\(100\) \((n < 100)\)
样例输入:
6 21 25 49 28 16 8样例输出:
8 16 21 25 28 49
一、比较类排序
- 通过比较元素大小来决定顺序
交换排序
1 冒泡排序
| 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| \(O (n^2)\) | \(O (n)\) | \(O (n^2)\) | \(O (1)\) | 稳定 |
原理
在水中,大的气泡往往会先浮动上去。而该算法的运行也类似于的气泡的浮动。
算法会从头开始进行两两相邻元素比较,保证大的在后面,小的在前面,如果顺序不对,则交换。这样比较一轮之后,所有元素中最大的会被交换到最后面。然后继续从头开始比较,第二大的元素被浮动到后面,不断重复这个过程,直到元素有序。
在整个序列中,保证所有的两两相邻元素比较,顺序不对则交换,这样最大的元素一定能沉到最后。
步骤
-
从头开始比较相邻两个元素,顺序不对则交换。
-
重复上一步,每重复一次,可少比较一个元素(上一轮比较已经将最大的元素沉在最后,可以不用比较)
-
直到所有元素有序
如果能够想清楚这个过程,那么应该知道如果有\(n\)个元素进行冒泡排序,以从头到尾两两比较作为一轮,至多要比较\(n-1\)轮,即可完成排序。
图解
算法改进
实际上,冒泡排序仍然拥有改进的余地,即当我们发现某一轮的两两比较中,如果从头到尾都没有发生交换行为,那么整个序列已然有序,无需重复比较。举出一个极端的例子
2 8 3 4 5
如果考虑对算法进行改进,那么效率能够有所上升
代码
// C++:普通版
#include <cstdio>
#define M 105
int main () {
int n;
int a[M];
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
for (int i = 1; i < n; i++) {
for (int j = 2; j <= n - i + 1; j++) {
if (a[j] < a[j-1]) {
int t = a[j];
a[j] = a[j - 1];
a[j - 1] = t;
}
}
}
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
printf ("\n");
return 0;
}
// C++:改进版
#include <cstdio>
#define M 105
int main () {
int n;
bool flag;
int a[M];
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
for (int i = 1; i < n; i++) {
flag = false; // 预设不会发生交换
for (int j = 2; j <= n - i + 1; j++) {
if (a[j] < a[j-1]) {
int t = a[j];
a[j] = a[j - 1];
a[j - 1] = t;
flag = true; // 发生交换的情况
}
}
if (!flag) //如果没有发生交换,说明已经有序,跳出循环即可
break;
}
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
printf ("\n");
return 0;
}
2 快速排序
| 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(n^2)\) | \(O(nlog_2n)\) | 不稳定 |
原理
快速排序也是基于两两交换的排序,不同的是,快速排序不满足于在相邻的两个元素之间进行交换,相当于一种改进的冒泡排序。
首先需要明白一点,在一个序列中,该序列经过排序后,每一个元素一定有一个唯一的属于自己的位置(索引)。例如序列\(3\,\, 5\,\, 1\,\, 6\,\, 4\),排序后\(1\,\,3\,\,4\,\,5\,\,6\),显然正确的顺序是元素\(3\)应该放在第\(2\)个位置上。而这个正确的位置有一大特点,就是该位置的左边元素都至少比它小,右边元素都至少比它大。换言之,如果一个序列中,每个元素都能满足这个特点,那么这个序列一定是顺序递增的序列。快速排序就是依照这个原理来进行的。
实际上快速排序就是在不断地确定每一个元素正确的位置。如何能够快速的确定每个元素的位置呢?我们把需要找到正确位置的元素叫做基准值(\(Pivot\))。将所有比基准值小的放在左边,所有比它打的放在右边。接下来利用递归的特性分别处理左右两边,在左右两个继续找基准值,然后继续左右分开,直到拆分到最小单位为止。这样整个序列在不断地分开并且交换的过程中,就变得有序了。
步骤
- 首先确定基准值,用索引\(i\)从左边寻找第一个比基准值大的元素、用索引\(j\)从右边开始寻找第一个比基准值小的元素,交换\(i,j\)索引处的值。如果\(i,j\)相遇,说明找到了基准值的位置,将基准值交换到该位置。以此位置分成两段进行递归
- 重复上述步骤,直到序列被分成单独的一个元素,这样位置一定是正确的
图解
代码
// 以左边第一个值作为基准元素
#include <cstdio>
#define M 105
void swap (int &x, int &y) {
int t = x;
x = y;
y = t;
}
void quick_sort (int left, int right, int a[]) {
if (left >= right)
return;
int i, j, pivot;
i = left, j = right;
pivot = a[left];
while (i < j) {
while (i < j && a[j] >= pivot) j --; //一定要先从右边开始比较(详细见注解1)
while (i < j && a[i] <= pivot) i ++;
if (i < j) swap (a[i], a[j]);
}
swap (a[left], a[i]);
quick_sort(left, i - 1, a);
quick_sort(i + 1, right, a);
}
int main () {
int n;
int a[M] = {0};
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
quick_sort(1, n, a);
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
puts("");
return 0;
}
// ---------------------------------------------
// 以中间元素的位置作为基准值,排序后保证中间那个位置放的正确的数值
#include <cstdio>
#define M 105
void swap (int &x, int &y) {
int t = x;
x = y;
y = t;
}
void quick_sort (int left, int right, int a[]) {
if (left >= right)
return;
int i, j, pivot, mid = left + right >> 1;
i = left, j = right;
pivot = a[mid];
while (i < j) {
while (a[j] > pivot) j --;
while (a[i] < pivot) i ++;
if (i <= j) {
swap (a[i], a[j]);
i ++, j --; // 这一步很重要(详细见注释2)
}
}
quick_sort(left, j, a);
quick_sort(i, right, a);
}
int main () {
int n;
int a[M] = {0};
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
quick_sort(1, n, a);
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
puts("");
return 0;
}
注解1:
举例说明:元素\(1\,\,5\,\,3\,\,7\,\,9\)进行排序。依照左边第一个元素作为\(pivot\)也就是\(1\),在确定\(pivot\)的位置时,指针\(i,j\)分别指向\(1,9\)处的索引。如果先执行while (i < j && a[i] <= pivot) i ++语句,那么情况为先找左边第一个比\(1\)小的数,因为\(1\)已经是最小,所以\(i\)此时会不断增加直到\(j\)的位置。而当\(i,j\)两者相遇后,我们说这个位置应该是\(pivot\)的正确位置,但此时是第\(5\)个位置,这显然不是\(1\)的正确位置。因此在以左边元素作为基准值时,比较时要从右边开始
注解2:
以中值作为基准值需要考虑的问题稍多,上述步骤是为了防止出现无限递归的情况;例如排序\(2\,\,1\,\,2\)
实际上对于基准元素的选择会影响快速排序的效率,在序列较为有序的时候,容易退化成冒泡。那么对于基准元素的选择可以随机实现,随机选择出基准元素后,与当前序列第一个元素交换,然后按照以左边第一个值为基准元素的方法进行即可。
#include <cstdio>
#include <iostream>
#include <ctime>
#include <cstdlib>
#define M 100005
void swap (int &x, int &y) {
int t = x;
x = y;
y = t;
}
void quick_sort (int left, int right, int a[]) {
if (left >= right)
return;
int i, j, pivot, R;
R = rand() % (right - left + 1) + left; // 随机值的下标保证出现在[Left,right]之间
i = left, j = right;
swap(a[left], a[R]); //交换
pivot = a[left];
while (i < j) {
while (i < j && a[j] >= pivot) j --;
while (i < j && a[i] <= pivot) i ++;
if (i < j)
swap (a[i], a[j]);
}
swap(a[left], a[i]);
quick_sort(left, i - 1, a);
quick_sort(i + 1, right, a);
}
int main () {
srand((int)time(0)); //初始化随机种子
int n;
int a[M] = {0};
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
quick_sort(1, n, a);
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
puts("");
return 0;
}
选择排序
3 选择排序
| 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| \(O(n^2)\) | \(O(n^2)\) | \(O(n^2)\) | \(O(1)\) | 不稳定 |
原理
选择排序更像是人们对于完成排序的任务所作出的最为直接的反应,如果有\(2\)个元素比大小,互相比较即可,如果是\(3\)个元素\(a,b,c\)比较大小,那么很直接的做法是首先比较\(ab\)、\(ac\),可以找出最小或最大的元素,接着比较\(bc\),确定第二大或者第二小的元素,那么依次类推,\(n\)个元素的比较首先那第\(1\)个元素和后面的全部比较,找出最小或最大,然后第\(2\)个元素向后依次比较。直到比较到\(n-1\)个元素。选择排序是很直接的一种方式因此复杂度一直都是\(O(n^2)\)
步骤
以找最小的元素为例
- 在未排序的序列中从头到尾找出最小的元素,与首位元素交换,并将第一个元素表示成已排好序
- 重复第一步
图解
代码
#include <cstdio>
#define M 105
int main () {
int n, min_index; //min_index记录每轮比较中最小值的下标
int a[M];
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
for (int i = 1; i <= n; i++) {
min_index = i;
for (int j = i + 1; j <= n; j++) {
if (a[j] < a[min_index])
min_index = j;
}
int t = a[i];
a[i] = a[min_index];
a[min_index] = t;
}
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
printf ("\n");
return 0;
}
4 堆排序
| 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| \(O(nlog_2 n)\) | \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(1)\) | 不稳定 |
原理
利用堆这种数据结构来进行辅助排序。
堆实际上是一棵完全二叉树。其中每个结点的值都大于等于左右孩子结点的值的堆叫做大顶堆,每个结点的值都小于等于左右孩子结点的值的堆叫做小顶堆。
例如
上图表述的就是一个大顶堆。如果按照从上到下从左到右对堆中的结点从\(0\)开始进行编号,再映射到数组。那么这个数组实际上就是一个堆结构。而这样的大顶堆一定满足条件:arr[i] >= arr[2*i+1] and arr[i] >= arr[2*i+2]
了解了堆的结构后,那么堆排序实际上就是构建一个堆,假定是大顶堆,堆构造好后,这样数组首位元素一定是最大的,那么和末尾元素交换,然后重新构造大顶堆,再用首位元素和倒数第二位元素交换,不断重复直到有序。从这点来看,堆排序实际上就是选择排序,不过选择的过程利用了堆的特点帮助我们更快的找出最大或者最小的元素。
接下来要解决的问题就是如何去构造一个大(小)顶堆。我们可以从大顶堆的特点来入手,只要堆上的每一棵子树都满足当前结点大于等于左右孩子即可,如果不满足可以进行交换,从左右孩子中找出大的那个和父结点交换。
然而需要考虑一个问题,在当前子树中可能符合大顶堆的特点,但是从整体看未必满足。原因在于当前孩子结点可能也有孩子结点,那么当父结点和子结点交换后未必,其子树未必能满足大顶堆特点,因此被交换的结点应该从当前结点继续深入调整结构到叶子结点为止。
例如:下图,只需调整\(1\)和\(5\)的位置即可
假设\(5\)下也有子树,在调整位置后需要继续深入子树调整。情况如下
但是实际上,从上往下的调整是十分消耗时间的情况。例如下面这种情况
原因在于我们希望尽可能少的交换,而从上往下调整,子树的大小顺序并不确定。上图的情况,当把\(5\,1\,4\)调整好后,按照之前的说法,\(5\)和\(1\)做了交换,那么需要调整\(1\)这棵子树的结构,但是调整完发现之前的结构被破坏了,\(5\,1\,7\)又需要重新调整。看到这里,一定能想到,如果子树也是满足大顶堆的顺序,那么肯定就不会有问题了。因此,结构调整需要从下往上进行。首先要找到第一个非叶子结点,在完全二叉树中它的应该是\(length/2 - 1\),其中\(length\)表示结点个数,也就是数组长度。然后从这个结点开始,向上不断调整。看到这里代码就很简单了。
步骤
- 构造堆,根据升降序构造大或者小顶堆
- 将堆顶元素和末尾元素交换
- 除开当前末尾元素,剩余元素继续构造堆。反复执行调整堆以及交换的操作。直到序列有序
图解
代码
#include <cstdio>
#define M 100005
void swap (int &x, int &y) {
int t = x;
x = y;
y = t;
}
// 用于调整结构
void adjust_heap (int a[], int k, int n) {
int mson, lson = 2*k+1;
mson = lson;
if (lson >= n) return; // 没有孩子结点
if (lson + 1 < n && a[lson] < a[lson+1]) // 如果有右孩子并且大于左孩子,mson记录为右孩子下标
mson ++;
if (a[k] < a[mson]) { //调整该子树的结构
swap (a[k], a[mson]);
adjust_heap(a, mson, n);
}
}
// 建立大顶堆
void build_max_heap (int a[], int n) {
/* 对每一个非叶子结点进行大顶堆结构的调整
该调整一定要是递归形式的调整,换句话说
如果该结点的孩子结点发生了变化,那么该孩子结点要继续往下调整
*/
for (int i = n / 2 - 1; i >= 0; i --)
adjust_heap (a, i, n);
}
void heap_sort (int a[], int n) {
for (int i = 0; i < n; i++) {
build_max_heap(a, n - i);
//建立好大顶堆,交换。重复这个过程
swap (a[0], a[n - 1 - i]);
}
}
int main () {
int a[M] = {0};
int n;
scanf ("%d", &n);
for (int i = 0; i < n; i++)
scanf ("%d", &a[i]);
heap_sort(a, n);
for (int i = 0; i < n; i++)
printf("%d ", a[i]);
return 0;
}
插入排序
5 插入排序
| 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| \(O(n^2)\) | \(O(n)\) | \(O(n^2)\) | \(O(1)\) | 稳定 |
原理
插入排序,实际上相当于对把一个有序序列扩大的过程,单个元素的序列,一定是有序的,在这个基础上,新插入一个元素,使之有序,继续插入下一个元素。直到所有元素全部有序。
步骤
- 从无序的序列中选出一位,和前面的有序序列进行比较,比较过程中整体后移
- 找到该元素的位置插入,重复上述过程
图解
代码
#include <cstdio>
#define M 1005
int main () {
int a[M] = {0};
int n;
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
for (int i = 1; i <= n; i++) {
int pre, cur; //cur表示当前插入的值
pre = i - 1;
cur = a[i];
while (pre >= 1 && a[pre] >= cur) {
a[pre+1] = a[pre];
pre--;
}
a[pre+1] = cur;
}
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
return 0;
}
/*------------------------------------
以下这种写法类似于冒泡,反向的过程
在一个有序的序列中,把一个数插入进去,可以从后往前进行冒泡的过程
这样一定能找到正确位置
*/
#include <cstdio>
#define M 1005
int main () {
int a[M] = {0};
int n, temp;
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
for (int i = 2; i <= n; i++) {
temp = a[i]; //要插入的元素
for (int j = i; j >= 1 && a[j] < a[j-1]; j--) { //类似冒泡,小的不断往前
a[j] = a[j-1];
a[j-1] = temp;
}
}
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
printf("\n");
return 0;
}
6 希尔排序
| 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| \(O(n^1.3)\) | \(O(n)\) | \(O(n^2)\) | \(O(1)\) | 不稳定 |
原理
希尔排序是对插入排序的改进版。考虑这样一个问题,插入排序做的是,在有序序列的基础上插入一个元素,而这个插入的过程本质上是不断地进行比较。比较的过程实际上可以进行优化。举一个极端的例子,假设序列为\(2\,3\,4\,5\,6\,1\,\)前面的已经有序,把\(1\)插进去,按照插入排序的思想,\(1\)会和前面每个元素进行比较,才能找到位置。
而希尔排序可以让这种比较的此时缩短。具体做法就是,将原本序列按照一定距离划分为若干个子序列,对每一个子序列进行插入排序,然后将这个距离缩短,再次划分区域,同样继续进行插入排序,直到这个距离为1,出现整个序列,最后一次插入排序即可。这样做的好处是可以使得同区域内的数有序,那么上述例子中的\(1\)可能在第一个划分区域时,就会被交换到前面去。
这个距离就是增量,随着增量的减少,整个序列会越发趋于有序。
增量设定为数组的一半,每轮下去依次减半,对序列\(9\,1\,2\,5\,7\,4\,8\,6\,3\,5\)执行排序,情况如下
步骤
- 确定增量,划分子序列
- 对子序列执行插入排序
- 缩小增量。重复上述步骤
图解
假定初始增量设置为数组长度的一半,每次变化为原来的一半。
代码
#include <cstdio>
#define M 100005
int main () {
int n, temp, a[M] = {0};
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
for (int gap = n / 2; gap >= 1; gap /= 2) { //增量的设定
for (int i = gap; i <= n; i ++) { //以此增量划分子序列,并对子序列进行插入排序
temp = a[i];
for (int j = i; j - gap >= 1 && a[j] < a[j-gap]; j-=gap) {
a[j] = a[j-gap];
a[j-gap] = temp;
}
}
}
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
return 0;
}
7 归并排序
| 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(n)\) | 稳定 |
原理
归并排序,主要是通过一种归并的操作来进行的。是一种分治的思想,将这整个序列不断一分为二,直到每个子序列只剩一个元素,然后再进行合并,合并结束后保证有序,这样最后会合并成一个完整的有序序列。
归并的操作过程中,只有一点需要考虑清楚,那就是将两个有序序列合并是如何进行的。
显然,需要从头开始逐一比对这两个有序序列的元素大小。假定这两个有序序列为\(X,Y\),并且下标分别从\(i,j\)开始。如果\(X[i] >= Y[j]\),那么合并中\(X[i]\)显然应该是首位。接下来应该比较\(X[i+1]\)和\(Y[j]\)的关系了,多次推导,不难发现,这是一个循环的过程。还有一点需要注意,极端情况下,可能序列\(X\)中的最大值都小与\(Y\)序列中的最小值,因此合并的过程需要判断是否这两个子序列的所有元素都完全合并进去了。
另外合并的操作,不能在原数组中进行,这会打乱顺序,需要建立一个辅助数组,辅助数组的左右就是暂时存储\([l,r]\)区间内的有序数组,这个数组是通过两段子序列合并而来,然后将这个区域的内的数组赋值给原序列即可。
步骤
- 将序列一分为二,对每一部分执行这个操作
- 重复上述过程,直到无可再分,然后开始合并,直到成为一个完整有序序列
图解
代码
#include <cstdio>
#define M 100005
int a[M], b[M]; //数组b用作辅助
void merge_sort (int l, int r) {
if (l >= r) return ;
int mid = l + r >> 1;
merge_sort (l, mid); //平分为两份
merge_sort (mid+1, r);
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) { // 合并
if (a[i] <= a[j])
b[k++] = a[i++];
else
b[k++] = a[j++];
}
while (i <= mid) b[k++] = a[i++];
while (j <= r) b[k++] = a[j++];
for (int t = l; t <= r; t++)
a[t] = b[t];
}
int main () {
int n;
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
merge_sort(1, n);
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
return 0;
}
二、非比较类排序
8 计数排序
| 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| \(O(n+k)\) | \(O(n+k)\) | \(O(n+k)\) | \(O(n+k)\) | 稳定 |
原理
计数排序,利用了数组的下标这一特点。使用在给定的序列中,最大值不是很大的情况下。
序列中元素的值为多少就放人下标为多少的数组中,而这个辅助数组初始化为\(0\),那么对于数组某一项的具体数值就是原序列中,该数值的个数。
步骤
- 找出待排序序列中最大值,开辟相应空间大小的数组
- 统计值为\(i\)的元素在序列中的个数,并且存放在数组中
- 正向遍历数组,该项的值为多少就输出多少次\(i\)
图解
可以看出计数排序是典型的以空间换时间,往往无序的序列中最大值是多少,就要开这么大的空间。
代码
#include <cstdio>
#define M 105
#define MAX 100000001
int main () {
int n, k = -MAX, a[M];
scanf ("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
if (a[i] > k)
k = a[i]; //最大值
}
int bucket[k+1] = {0};
for (int i = 1; i <= n; i++)
bucket[a[i]] ++;
for (int i = 0; i <= k; i++)
for (int j = i; bucket[j]; bucket[j]--)
printf("%d ", i);
return 0;
}
计数排序可以升级成桶排序,改进的地方在于,我们可以将每个\(bucket\)不仅仅表示一个值,可以表示一个范围,范围内的数都装进来,然后每个桶都需排序。难点在于可以装范围数据的桶应该用什么数据结构表示。
9 基数排序
| 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| \(O(n*k)\) | \(O(n*k)\) | \(O(n*k)\) | \(O(n+k)\) | 稳定 |
原理
基数排序是按照,数的位数来进行的,从低位到高位,每次排位数的时候,都可以使用计数排序,因为\(10\)进制的数,最大为\(9\)。换句话说,就是先按个位数大小排序,然后是十位、百位,依次类推。每一次当前位数排序完后,都需要按照这个顺序重组数组,直到最大的一个数的所有位数排序。
可行的原因在于,位数小的数排到高位时,已经是\(0\)了,使用计数排序自然会被放在前面。
步骤
- 找到数组中的最大值,计算位数
- 根据上述步骤的位数进行计数排序
图解
关键在于,能够存储这样的带有下标以及原数组元素的数据结构应该选用什么。可以使用链表,可以使用\(Vector\),为了简便,以下代码使用二维数组。二维数组共\(10\)行,每行的长度最大为待排序的数组长度。每行的行首元素代表这行的元素个数。这样维护的过程中,维护好行首,即可方便的取出存进。
代码
#include <cstdio>
#define M 100005
int get_max_digit (int a[], int n) { //获取数组中最大值
int result = a[1];
for (int i = 2; i <= n; i++)
if (a[i] > result)
result = a[i];
return result;
}
void radix_sort (int a[], int len) {
int b[10][len+1]; // 二维数组第一列的值表示这一行的个数
int m = get_max_digit(a, len);
for (int e = 1; m/e; e *= 10) { //按照位数进行计数排序
for (int i = 0; i < 10; i++)
b[i][0] = 0;
for (int i = 1; i <= len; i++) { //按位存储,同时记录个数
int d = a[i] / e % 10;
int k = ++b[d][0];
b[d][k] = a[i];
}
int cnt = 1;
for (int i = 0; i < 10; i++) { //取出放回原数组
for (int j = 1; j <= b[i][0]; j++, cnt++)
a[cnt] = b[i][j];
}
}
}
int main () {
int n, a[M] = {0};
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
radix_sort (a, n);
for (int i = 1; i <= n; i++)
printf ("%d ", a[i]);
return 0;
}

浙公网安备 33010602011771号