排序算法
排序算法详解
排序的基本概念
排序,就是重新排列表中的元素,使表中的元素满足按关键字有序的过程。为了查找方便,通常希望计算机中的表是按关键字有序的。排序的确切定义如下:
- 输入:n 个记录,对应的关键字为 k_1, k_2,..., k_n)。
- 输出:输入序列的一个重排(k'_1, k'_2,..., k'_n),使得(k'_1 ≤ k'_2 ≤...≤ k'_n)(其中“≤”可以换成其他的比较大小的符号)。
算法的稳定性
若待排序表中有两个元素 R_i 和 R_j,其对应的关键字相同即 k_i = k_j,且在排序前 R_i 在 R_j 的前面,若使用某一排序算法排序后,R_i 仍然在 R_j 的前面,则称这个排序算法是稳定的,否则称这个排序算法是不稳定的。
需要注意的是,算法是否具有稳定性并不能衡量一个算法的优劣,它主要是对算法的性质进行描述。若待排序表中的关键字不允许重复,排序结果是唯一的,则对于排序算法的选择,稳定与否无关紧要。
内部排序与外部排序
在排序过程中,根据数据元素是否完全存放在内存中,可将排序算法分为两类:
- 内部排序:指在排序期间元素全部存放在内存中的排序。
- 外部排序:指在排序期间元素无法全部同时存放在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动的排序。
排序的核心操作与分类
一般情况下,内部排序算法在执行过程中都要进行两种操作:比较和移动。通过比较两个关键字的大小,确定对应元素的前后关系,然后通过移动元素以达到有序。当然,并非所有的内部排序算法都要基于比较操作,事实上,基数排序就不基于比较操作。
每种排序算法都有各自的优缺点,适合在不同的环境下使用,就其全面性能而言,很难提出一种被认为是最好的算法。通常可以将排序算法分为插入排序、交换排序、选择排序、归并排序和基数排序五大类。内部排序算法的性能取决于算法的时间复杂度和空间复杂度,而时间复杂度一般是由比较和移动的次数决定的。
插入排序
直接插入排序
直接插入排序的核心思想是:将待排序元素逐个插入到已排好序的子序列中,直到所有元素插入完成。
假设在排序过程中,待排序表 L[1...n] 在某次排序过程中的某一时刻状态如下
- 有序序列:L[1 ... i-1]
- 待插入元素:L(i)
- 无序序列:L[i+1 ... n]
要将元素 L(i) 插入已有序的子序列 L[1...i - 1],需要执行以下操作(为避免混淆,下面用 L[] 表示一个表,而用工()表示一个元素):
- 查找出 L(i) 在 L[1...i - 1] 中的插入位置 k;
- 将 L[k...i-1] 中的所有元素依次后移一个位置;
- 将 L(i) 复制到 L(k)。
为了实现对 L[1..n] 的排序,可以将 L(2) ~ L(n) 依次插入前面已排好序的子序列,初始 L[1] 可以视为一个已推好序的子序列。上述操作执行 n - 1 次就能得到一个有序的表。插入排序在实现上通常采用原地排序(空间复杂度为 O(1)),因而在从后往前的比较过程中,需要反复把已排序元素逐步向后挪位,为新元素提供插入空间。
代码实现
#include <iostream>
using namespace std;
#define MAX 11
int a[MAX];
int length;
void InsertSort() {
int i, j;
for(i = 2; i <= length; i++) { // a[1]~a[n]
if(a[i] < a[i - 1]) { // 待插入元素小于前一个元素
a[0] = a[i]; // 待插入元素暂存
for(j = i - 1; a[j] > a[0]; j--) a[j + 1] = a[j]; // 后移
a[j + 1] = a[0];// 插入待插入元素
}
}
}
// 辅助函数:打印数组
void printArray(int arr[], int len) {
for (int i = 1; i <= len; i++) cout << arr[i] << " ";
cout << endl;
}
// 辅助函数:初始化数组
void initArray(int arr[], int len, const int source[]) {
for (int i = 1; i <= len; i++) arr[i] = source[i - 1];
length = len;
}
// 测试函数
void testInsertSort() {
cout << "=== 直接插入排序测试 ===" << endl;
// 测试用例1:随机无序数组
cout << "\n[测试1] 随机无序数组: " << endl;
int test1[] = {38, 49, 65, 97, 76, 13, 27, 49};
initArray(a, 8, test1);
cout << "排序前: ";
printArray(a, length);
InsertSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例2:已排序数组
cout << "\n[测试2] 已排序数组: " << endl;
int test2[] = {1, 2, 3, 4, 5, 6, 7, 8};
initArray(a, 8, test2);
cout << "排序前: ";
printArray(a, length);
InsertSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例3:逆序数组
cout << "\n[测试3] 逆序数组: " << endl;
int test3[] = {8, 7, 6, 5, 4, 3, 2, 1};
initArray(a, 8, test3);
cout << "排序前: ";
printArray(a, length);
InsertSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例4:含重复元素数组
cout << "\n[测试4] 含重复元素数组;
int: " << endl test4[] = {5, 3, 8, 3, 9, 1, 5, 2};
initArray(a, 8, test4);
cout << "排序前: ";
printArray(a, length);
InsertSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例5:单元素数组
cout << "\n[测试5] 单元素数组: " << endl;
int test5[] = {10};
initArray(a, 1, test5);
cout << "排序前: ";
printArray(a, length);
InsertSort();
cout << "排序后: ";
printArray(a, length);
}
int main() {
testInsertSort();
return 0;
}
性能
- 空间效率:仅使用了常数个辅助单元,因而空间复杂度为 O(1)。
- 时间效率:在排序过程中,向有序子表中逐个地插入元素的操作进行了 n - 1 趟,每趋操作都分为比较关键字和移动元素,而比较次数和移动次数取决于待排序表的初始状态。在最好情况下,表中元素已经有序,此时每插入一个元素,都只需比较一次而不用移动元素,因而时间复杂度为 O(n)。在最坏情况下,表中元素顺序刚好与排序结果中的元素顺序相反(逆序),总的比较次数达到最大,总的移动次数也达到最大,总的时间复杂度为 O(n ^ 2)。平均情况下,考虑待排序表中元素是随机的,此时可以取上述最好与最坏情况的平均值作为。平均情况下的时间复杂度,总的比较次数与总的移动次数均约为 O((n ^ 2) / 4)。因此,直接插入排序算法的时间复杂度为 O(n ^ 2)。
- 稳定性:因为每次插入元素时总是从后往前先比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序是一个稳定的排序算法。
- 适用性:直接插入排序适用于顺序存储和链式存储的线性表,采用链式存储时无须移动元素。
折半插入排序
从直接插入排序算法中,不难看出每趟插入的过程中都进行了两项工作:从前面的有序子表中查找出待插入元素应该被插入的位置;给插入位置腾出空间,将待插入元素复制到表中的插入位置。注意到在该算法中,总是边比较边移动元素。下面将比较和移动操作分离,即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素。当排序表为顺序表时,可以对直接插入排序算法做如下改进:因为是顺序存储的线性表,所以查找有序子表时可以用折半查找来实现。确定待插入位置后,就可统一地向后移动元素。折半插入排序是对直接插入排序的优化,利用二分查找快速确定待插入元素的位置,减少比较次数。
代码实现
#include <iostream>
using namespace std;
#define MAX 11
int a[MAX];
int length;
void InsertSort() {
int i, j;
for(i = 2; i <= length; i++) { // a[1]~a[n]
int low = 1, high = i - 1, mid;
int temp = a[i];
while(low <= high) {
mid = (low + high) / 2;
if(temp < a[mid]) high = mid - 1;
else low = mid + 1;
}
for(j = i - 1; j >= low; j--) a[j + 1] = a[j];
a[low] = temp;
}
}
// 辅助函数:打印数组
void printArray(int arr[], int len) {
for (int i = 1; i <= len; i++) cout << arr[i] << " ";
cout << endl;
}
// 辅助函数:初始化数组
void initArray(int arr[], int len, const int source[]) {
for (int i = 1; i <= len; i++) arr[i] = source[i - 1];
length = len;
}
// 测试函数
void testInsertSort() {
cout << "=== 折半插入排序测试 ===" << endl;
// 测试用例1:随机无序数组
cout << "\n[测试1] 随机无序数组: " << endl;
int test1[] = {38, 49, 65, 97, 76, 13, 27, 49};
initArray(a, 8, test1);
cout << "排序前: ";
printArray(a, length);
InsertSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例2:已排序数组
cout << "\n[测试2] 已排序数组: " << endl;
int test2[] = {1, 2, 3, 4, 5, 6, 7, 8};
initArray(a, 8, test2);
cout << "排序前: ";
printArray(a, length);
InsertSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例3:逆序数组
cout << "\n[测试3] 逆序数组: " << endl;
int test3[] = {8, 7, 6, 5, 4, 3, 2, 1};
initArray(a, 8, test3);
cout << "排序前: ";
printArray(a, length);
InsertSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例4:含重复元素数组
cout << "\n[测试4] 含重复元素数组: " << endl;
int test4[] = {5, 3, 8, 3, 9, 1, 5, 2};
initArray(a, 8, test4);
cout << "排序前: ";
printArray(a, length);
InsertSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例5:单元素数组
cout << "\n[测试5] 单元素数组: " << endl;
int test5[] = {10};
initArray(a, 1, test5);
cout << "排序前: ";
printArray(a, length);
InsertSort();
cout << "排序后: ";
printArray(a, length);
}
int main() {
testInsertSort();
return 0;
}
性能
从上述算法中,不难看出折半插入排序仅减少了比较元素的次数,时间复杂度约为 O(n * log_2(n)),该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数 n;而元素的移动次数并未改变,它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍为 O(n ^ 2),但对于数据量不算很大的排序表,折半插入排序往往能表现出很好的性能。折半插入排序是一种稳定的排序算法。折半插入排序仅适用于顺序存储的线性表。
希尔排序
希尔排序的基本思想是:先将待排序表分割成若干形如 L[i, i + d, i + 2d,...,i + kd] 的“特殊”子表,即把相隔某个“增量”的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。
希尔排序的过程如下:先取一个小于 n 的增量 d_1,把表中的全部记录分成组,所有距离为 d_1 的倍数的记录放在同一组,在各组内进行直接插入排序;然后取第二个增量 d_1 小于 d_2,重复上述过程,直到所取到的 d_i = 1,即所有记录已放在同一组中,再进行直接插入排序,此时已经具有较好的局部有序性,因此可以很快得到最终结果。到目前为止,尚未求得一个最好的增量序列。
算法示例(以增量序列 ( d = 5, 3, 1 ) 为例,序列为 49,38,65,97,76,13,27,49,55,04)

代码实现
#include <iostream>
using namespace std;
// 希尔排序函数实现
void ShellSort(int a[], int n) {
int dk, i, j;
// 增量序列从n/2开始,逐步减半直至1
for (dk = n / 2; dk >= 1; dk = dk / 2) {
// 对每个子序列进行直接插入排序
for (i = dk + 1; i <= n; i++) { // 若当前元素小于其所在子序列的前一个元素,需要插入排序
if (a[i] < a[i - dk]) {
a[0] = a[i]; // 寻找插入位置
for (j = i - dk; j > 0 && a[0] < a[j]; j -= dk) a[j + dk] = a[j]; // 记录后移
a[j + dk] = a[0]; // 插入到正确位置
}
}
}
}
// 打印数组元素
void printArray(int a[], int n) {
for (int i = 1; i <= n; i++) cout << a[i] << " ";
cout << endl;
}
// 初始化数组(1-based索引)
void initArray(int a[], int arr[], int n) {
for (int i = 0; i < n; i++) a[i + 1] = arr[i];
}
// 测试希尔排序函数
void testShellSort() {
cout << "=== 希尔排序测试 ===" << endl;
// 测试用例1:随机无序数组
int test1[11], arr1[] = {39, 80, 76, 41, 13, 29, 50, 78, 30, 11};
int n1 = sizeof(arr1) / sizeof(arr1[0]);
initArray(test1, arr1, n1);
cout << "测试用例1(随机无序数组):" << endl;
cout << "排序前:";
printArray(test1, n1);
ShellSort(test1, n1);
cout << "排序后:";
printArray(test1, n1);
cout << endl;
// 测试用例2:已经有序的数组
int test2[6], arr2[] = {12, 23, 34, 45, 56};
int n2 = sizeof(arr2) / sizeof(arr2[0]);
initArray(test2, arr2, n2);
cout << "测试用例2(已排序数组):" << endl;
cout << "排序前:";
printArray(test2, n2);
ShellSort(test2, n2);
cout << "排序后:";
printArray(test2, n2);
cout << endl;
// 测试用例3:逆序数组
int test3[6], arr3[] = {98, 76, 54, 32, 10};
int n3 = sizeof(arr3) / sizeof(arr3[0]);
initArray(test3, arr3, n3);
cout << "测试用例3(逆序数组):" << endl;
cout << "排序前:";
printArray(test3, n3);
ShellSort(test3, n3);
cout << "排序后:";
printArray(test3, n3);
cout << endl;
// 测试用例4:包含重复元素的数组
int test4[7], arr4[] = {55, 33, 77, 33, 99, 22};
int n4 = sizeof(arr4) / sizeof(arr4[0]);
initArray(test4, arr4, n4);
cout << "测试用例4(含重复元素):" << endl;
cout << "排序前:";
printArray(test4, n4);
ShellSort(test4, n4);
cout << "排序后:";
printArray(test4, n4);
cout << endl;
// 测试用例5:单元素数组
int test5[2], arr5[] = {42};
int n5 = sizeof(arr5) / sizeof(arr5[0]);
initArray(test5, arr5, n5);
cout << "测试用例5(单元素数组):" << endl;
cout << "排序前:";
printArray(test5, n5);
ShellSort(test5, n5);
cout << "排序后:";
printArray(test5, n5);
}
int main() {
testShellSort();
return 0;
}
性能
- 空间效率:仅使用了常数个辅助单元,因而空间复杂度为 O(1)。
- 时间效率:因为希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。当 n 在某个特定范围时,希尔排序的时间复杂度约为 O(n ^ 1.3),在最坏情况下希尔排序的时间复杂度为 O(n ^ 2)。
- 稳定性:当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序,因此希尔排序是一种不稳定的排序算法。
- 适用性:希尔排序仅适用于顺序存储的线性表。
交换排序
冒泡排序
冒泡排序的基本思想是:从后往前(或从前往后)两两比较相邻元素的值(假设升序),若为逆序(A[i - 1] > A[i]),则交换它们,直到序列比较完。我们称它为第一趟冒泡,结果是将最小的元素交换到待排序列的第一个位置(或将最大的元素交换到待排序列的最后一个位置),关键字最小的元素如气泡一般逐渐往上“漂浮”至“水面”(或关键字最大的元素如石头一般下沉至水底)。下一趟冒泡时,前一趟确定的最小元素不再参与比较,每趟冒泡的结果是把序列中的最小元素(或最大元素) 放到了序列的最终位置···这样最多做 n - 1 趟冒泡就能把所有元素排好序。
代码实现
#include <iostream>
#include <algorithm>
using namespace std;
#define MAX 11
int a[MAX];
int length;
void BubbleSort() {
for(int i = 1; i < length; i++) {
bool flag = false; // 标志位,用于判断是否发生交换
for(int j = length; j > i; j--) {
if(a[j - 1] > a[j]) {
swap(a[j - 1], a[j]);
flag = true; // 发生交换,标志位设为true
}
}
if(!flag) return; // 如果没有发生交换,说明数组已经有序,直接返回
}
}
// 辅助函数:打印数组
void printArray(int arr[], int len) {
for (int i = 1; i <= len; i++) cout << arr[i] << " ";
cout << endl;
}
// 辅助函数:初始化数组
void initArray(int arr[], int len, const int source[]) {
for (int i = 1; i <= len; i++) arr[i] = source[i - 1];
length = len;
}
// 测试函数
void testInsertSort() {
cout << "=== 冒泡排序测试 ===" << endl;
// 测试用例1:随机无序数组
cout << "\n[测试1] 随机无序数组: " << endl;
int test1[] = {38, 49, 65, 97, 76, 13, 27, 49};
initArray(a, 8, test1);
cout << "排序前: ";
printArray(a, length);
BubbleSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例2:已排序数组
cout << "\n[测试2] 已排序数组: " << endl;
int test2[] = {1, 2, 3, 4, 5, 6, 7, 8};
initArray(a, 8, test2);
cout << "排序前: ";
printArray(a, length);
BubbleSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例3:逆序数组
cout << "\n[测试3] 逆序数组: " << endl;
int test3[] = {8, 7, 6, 5, 4, 3, 2, 1};
initArray(a, 8, test3);
cout << "排序前: ";
printArray(a, length);
BubbleSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例4:含重复元素数组
cout << "\n[测试4] 含重复元素数组: " << endl;
int test4[] = {5, 3, 8, 3, 9, 1, 5, 2};
initArray(a, 8, test4);
cout << "排序前: ";
printArray(a, length);
BubbleSort();
cout << "排序后: ";
printArray(a, length);
// 测试用例5:单元素数组
cout << "\n[测试5] 单元素数组: " << endl;
int test5[] = {10};
initArray(a, 1, test5);
cout << "排序前: ";
printArray(a, length);
BubbleSort();
cout << "排序后: ";
printArray(a, length);
}
int main() {
testInsertSort();
return 0;
}
性能
- 空间效率:仅使用了常数个辅助单元,因而空间复杂度为 O(1)。
- 时间效率:当初始序列有序时,显然第一趟冒泡后 flag 依然为 false(本趟没有元素交换)从而直接跳出循环,比较次数为 n - 1,移动次数为 0,从而最好情况下的时间复杂度为 O(n);当初始序列为逆序时,需要进行 n - 1 趟排序,第 i 趟排序要进行 n - i 次关键字的比较,而且每次比较后都必须移动元素 3 次来交换元素位置。这种情况下,移动次数 = n(n -1) / 2;比较次数 = 3n(n - 1) / 2,从而,最坏情况下的时间复杂度为 O(n ^ 2),平均时间复杂度为 O(n ^ 2)。
- 稳定性:i > j 且 A[i] = A[j] 时,不会发生交换,因此冒泡排序是一种稳定的排序算法。
- 适用性:冒泡排序适用于顺序存储和链式存储的线性表。
快速排序
快速排序的基本思想:在待排序表 L[1...n] 中任取一个元素 pivot 作为枢轴(或称基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分 L[1..k - 1] 和 [k + 1..n],使得 L[1...k - 1] 中的所有元素小于 pivot,L[k + 1..n] 中的所有元素大于或等于 pivot,则 pivot 放在了其最终位置 L(k) 上,这个过程称为一次划分。然后分别递归地对两个子表重复上述过程。直至每部分内只有一个元素或为空为止,即所有元素放在了其最终位置上。
一趟的图解如下:

代码实现
#include <iostream>
using namespace std;
#define MAX 100 // 数组最大容量
int a[MAX]; // 全局数组,a[0]未使用,从a[1]开始存储数据
int length; // 数组长度
// 划分函数:将数组划分为两部分,返回枢轴位置
int Partition(int a[], int i, int j) {
int pivot = a[i]; // 选择第一个元素作为枢轴
while (i < j) {
while (i < j && a[j] >= pivot) j--;// 从右向左找到第一个小于枢轴的元素
a[i] = a[j];
while (i < j && a[i] <= pivot) i++; // 从左向右找到第一个大于枢轴的元素
a[j] = a[i];
}
a[i] = pivot; // 将枢轴放到最终位置
return i; // 返回枢轴位置
}
// 快速排序递归函数
void QuickSort(int a[], int low, int high) {
if (low < high) {
int pivotpos = Partition(a, low, high); // 进行划分
QuickSort(a, low, pivotpos - 1); // 对左子表递归排序
QuickSort(a, pivotpos + 1, high); // 对右子表递归排序
}
}
// 辅助函数:打印数组元素
void printArray(int arr[], int len) {
for (int i = 1; i <= len; i++) cout << arr[i] << " ";
cout << endl;
}
// 辅助函数:初始化数组(1-based索引)
void initArray(int arr[], int len, const int source[]) {
for (int i = 1; i <= len; i++) arr[i] = source[i - 1];
length = len;
}
// 测试快速排序函数
void testQuickSort() {
cout << "=== 快速排序测试 ===" << endl;
// 测试用例1:随机无序数组
int test1[] = {39, 80, 76, 41, 13, 29, 50, 78, 30, 11};
int n1 = sizeof(test1) / sizeof(test1[0]);
initArray(a, n1, test1);
cout << "测试用例1(随机无序数组):" << endl;
cout << "排序前:";
printArray(a, n1);
QuickSort(a, 1, n1);
cout << "排序后:";
printArray(a, n1);
cout << endl;
// 测试用例2:已经有序的数组
int test2[] = {12, 23, 34, 45, 56, 67};
int n2 = sizeof(test2) / sizeof(test2[0]);
initArray(a, n2, test2);
cout << "测试用例2(已排序数组):" << endl;
cout << "排序前:";
printArray(a, n2);
QuickSort(a, 1, n2);
cout << "排序后:";
printArray(a, n2);
cout << endl;
// 测试用例3:逆序数组
int test3[] = {98, 76, 54, 32, 10};
int n3 = sizeof(test3) / sizeof(test3[0]);
initArray(a, n3, test3);
cout << "测试用例3(逆序数组):" << endl;
cout << "排序前:";
printArray(a, n3);
QuickSort(a, 1, n3);
cout << "排序后:";
printArray(a, n3);
cout << endl;
// 测试用例4:包含重复元素的数组
int test4[] = {55, 33, 77, 33, 99, 22, 55};
int n4 = sizeof(test4) / sizeof(test4[0]);
initArray(a, n4, test4);
cout << "测试用例4(含重复元素):" << endl;
cout << "排序前:";
printArray(a, n4);
QuickSort(a, 1, n4);
cout << "排序后:";
printArray(a, n4);
cout << endl;
// 测试用例5:单元素数组
int test5[] = {42};
int n5 = sizeof(test5) / sizeof(test5[0]);
initArray(a, n5, test5);
cout << "测试用例5(单元素数组):" << endl;
cout << "排序前:";
printArray(a, n5);
QuickSort(a, 1, n5);
cout << "排序后:";
printArray(a, n5);
}
int main() {
testQuickSort();
return 0;
}
性能
- 空间效率:快速排序是递归的,因此需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量与递归调用的最大层数一致。最好情况下为 O(log_2(n));最坏情况下,要进行 n - 1 次递归调用,因此栈的深度为 O(n);平均情况下,栈的深度为 O(log_2(n))。
- 时间效率:快速排序的运行时间与划分是否对称有关,快速排序的最坏情况发生在两个区域分别包含 n - 1 个元素和 0 个元素时,这种最大限度的不对称性若发生在每层递归上,即对应于始排序表基本有序或基本逆序时,就得到最坏情况下的时间复杂度为 O(n ^ 2)。有很多方法可以提高算法的效率,一种方法是尽量选取一个可以将数据中分的枢轴元素,如从序列的头尾及中间选取三个元素,再取这三个元素的中间值作为最终的枢轴元素;或者随机地从当前表中选取枢轴元素,这样做可使得最坏情况在实际排序中几乎不会发生。在最理想的状态下,即 Partition() 能做到最平衡的划分,得到的两个子问题的大小都不可能大于 n / 2,在这种情况下,快速排序的运行速度将大大提升,此时,时间复杂度为 O(nlog_2(n))。好在快速排序平均情况下的运行时间与其最佳情况下的运行时间很接近,而不是接近其最坏情况下的运行时间。快速排序是所有内部排序算法中平均性能最优的排序算法。
- 稳定性:在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左端区间后,它们的相对位置会发生变化,即快速排序是一种不稳定的排序算法。
- 适用性:快速排序仅适用于顺序存储的线性表。
- 在快速排序算法中,并不产生有序子序列,但每一趟排序后会将上一趟划分的各个无序子表的枢轴(基准)元素放到其最终的位置上。
选择排序
选择排序的基本思想是:每一趟(如第 i 趟)在后面 n - i + 1(i = 1, 2,.., n - 1)个待排序元素中选取关键字最小的元素,作为有序子序列的第 i 个元素,直到第 n - 1 趟做完,待排序元素只剩下 1 个,就不用再选。
简单选择排序
根据上面选择排序的思想,可以很直观地得出简单选择排序算法的思想:假设排序表为 L[1...n],第 i 趟排序即从 L[i...n] 中选择关键字最小的元素与 L(i) 交换,每一趟排序可以确定一个元素的最终位置,这样经过 n - 1 趟排序就可使得整个排序表有序。
代码实现
#include <iostream>
#include <algorithm> // 用于std::swap
using namespace std;
#define MAX 100 // 数组最大容量
int a[MAX]; // 全局数组,采用1-based索引
int length; // 数组长度
// 简单选择排序算法实现
void SelectSort(int a[], int n) {
for (int i = 1; i <= n - 1; i++) { // i从1开始,符合1-based索引
int minIndex = i; // 记录最小元素位置
// 在未排序部分查找最小元素
for (int j = i + 1; j <= n; j++) if (a[j] < a[minIndex]) minIndex = j; // 更新最小元素位置
// 如果最小元素不是当前位置,则交换
if (minIndex != i) swap(a[i], a[minIndex]);
}
}
// 辅助函数:打印数组元素
void printArray(int arr[], int len) {
for (int i = 1; i <= len; i++) cout << arr[i] << " ";
cout << endl;
}
// 辅助函数:初始化数组(1-based索引)
void initArray(int arr[], int len, const int source[]) {
for (int i = 1; i <= len; i++) arr[i] = source[i - 1];
length = len;
}
// 测试简单选择排序函数
void testSelectSort() {
cout << "=== 简单选择排序测试 ===" << endl;
// 测试用例1:随机无序数组
int test1[] = {39, 80, 76, 41, 13, 29, 50, 78, 30, 11};
int n1 = sizeof(test1) / sizeof(test1[0]);
initArray(a, n1, test1);
cout << "测试用例1(随机无序数组):" << endl;
cout << "排序前:";
printArray(a, n1);
SelectSort(a, n1);
cout << "排序后:";
printArray(a, n1);
cout << endl;
// 测试用例2:已经有序的数组
int test2[] = {12, 23, 34, 45, 56, 67};
int n2 = sizeof(test2) / sizeof(test2[0]);
initArray(a, n2, test2);
cout << "测试用例2(已排序数组):" << endl;
cout << "排序前:";
printArray(a, n2);
SelectSort(a, n2);
cout << "排序后:";
printArray(a, n2);
cout << endl;
// 测试用例3:逆序数组
int test3[] = {98, 76, 54, 32, 10};
int n3 = sizeof(test3) / sizeof(test3[0]);
initArray(a, n3, test3);
cout << "测试用例3(逆序数组):" << endl;
cout << "排序前:";
printArray(a, n3);
SelectSort(a, n3);
cout << "排序后:";
printArray(a, n3);
cout << endl;
// 测试用例4:包含重复元素的数组
int test4[] = {55, 33, 77, 33, 99, 22, 55};
int n4 = sizeof(test4) / sizeof(test4[0]);
initArray(a, n4, test4);
cout << "测试用例4(含重复元素):" << endl;
cout << "排序前:";
printArray(a, n4);
SelectSort(a, n4);
cout << "排序后:";
printArray(a, n4);
cout << endl;
// 测试用例5:单元素数组
int test5[] = {42};
int n5 = sizeof(test5) / sizeof(test5[0]);
initArray(a, n5, test5);
cout << "测试用例5(单元素数组):" << endl;
cout << "排序前:";
printArray(a, n5);
SelectSort(a, n5);
cout << "排序后:";
printArray(a, n5);
}
int main() {
testSelectSort();
return 0;
}
性能
- 空间效率:仅使用常数个辅助单元,所以空间效率为 O(1)。
- 时间效率:从上述伪码中不难看出,在简单选择排序过程中,元素移动的操作次数很少,不会超过 3(n - 1) 次,最好的情况是移动 0 次,此时对应的表已经有序;但元素间比较的次数与序列的初始状态无关,始终是 n(n - 1) / 2 次,因此时间复杂度始终是 O(n ^ 2)。
- 稳定性:在第 i 趟找到最小元素后,和第 i 个元素交换,可能会导致第 i 个元素与含有相同关键字的元素的相对位置发生改变。因此,简单选择排序是一种不稳定的排序算法。
- 适用性:简单选择排序适用于顺序存储和链式存储的线性表,以及关键字较少的情况。
堆排序
堆的定义如下,n 个关键字序列 L[1...n] 称为堆,当且仅当该序列满足:L(i) ≥ L(2i) 且 L(i) ≥ L(2i + 1) 或 L(i) ≤ L(2i) 且 L(i) ≤ L(2i + 1)(1 ≤ i ≤ n / 2(向下取整))
可以将堆视为一棵完全二叉树,满足条件“L(i) ≥ L(2i) 且 L(i) ≥ L(2i + 1)”的堆称为大根堆(大顶堆),大根堆的最大元素存放在根结点,且其任意一个非根结点的值小于或等于其双亲结点值。满足条件“L(i) ≤ L(2i) 且 L(i) ≤ L(2i + 1)”的堆称为小根堆(小顶堆),小根堆的定义刚好相反,根结点是最小元素。
堆排序的思路很简单:首先将存放在 L[1...n] 中的 n 个元素建成初始堆,因为堆本身的特点(以大顶堆为例),所以堆项元素就是最大值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点已不满足大顶堆的性质,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩一个元素为止。可见,堆排序需要解决两个问题:如何将无序序列构造成初始堆;输出堆顶元素后,如何将剩余元素调整成新的堆。
堆排序的关键是构造初始堆。建堆思路是从后往前检查所有分支结点,看是否满足堆的要求,若不满口,则对以该分支结点为根的子树进行调整。n 个结点的完全二叉树,最后一个结点是第 n / 2(向下取整)个结点的孩子。对以第 n / 2(向下取整)个结点为根的子树筛选(对于大根堆,若根结点的关键字小于左右孩子中关键字较大者,则交换),使该子树成为堆。之后向前依次对以各结点(n / 2(向下取整) - 1 ~ 1)为根的子树进行筛选,看该结点值是否大于其左右子结点的值,若不大于,则将左右子结点中的较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止。反复利用上述调整堆的方法建堆,直到根结点。输出堆顶元素后,将堆的最后一个元素与堆顶元素交换,此时堆的性质被破坏,需要向下进行筛选。
代码实现
#include <iostream>
#include <algorithm>
using namespace std;
#define MAX 100 // 数组最大容量
int a[MAX]; // 全局数组,采用1-based索引
int length; // 数组长度
// 辅助函数:打印数组元素
void printArray(int arr[], int len) {
for (int i = 1; i <= len; i++) cout << arr[i] << " ";
cout << endl;
}
// 辅助函数:初始化数组(1-based索引)
void initArray(int arr[], int len, const int source[]) {
for (int i = 1; i <= len; i++) arr[i] = source[i - 1];
length = len;
}
// 堆调整:将以k为根的子树调整为大根堆
void HeadAdjust(int a[], int k, int len) {
a[0] = a[k]; // a[0]暂存子树的根节点
for (int i = 2 * k; i <= len; i *= 2) { // 从左孩子开始
// 选择左右孩子中较大的一个
if (i < len && a[i] < a[i + 1]) i++;
// 如果根节点大于等于最大孩子,则无需调整
if (a[0] >= a[i]) break;
else {
a[k] = a[i]; // 将较大孩子上移
k = i; // 继续向下调整
}
}
a[k] = a[0]; // 将暂存的根节点放到最终位置
}
// 构建大根堆
void BuildMaxHeap(int a[], int len) {
// 从最后一个非叶子节点开始向上调整
for (int i = len / 2; i > 0; i--) HeadAdjust(a, i, len);
}
// 堆排序算法
void HeapSort(int a[], int len) {
BuildMaxHeap(a, len); // 构建初始大根堆
cout << "构建大根堆后:";
printArray(a, len);
for (int i = len; i > 1; i--) {
swap(a[1], a[i]); // 将堆顶元素与最后一个元素交换
HeadAdjust(a, 1, i - 1); // 调整剩余元素为大根堆
}
}
// 测试堆排序函数
void testHeapSort() {
cout << "=== 堆排序测试 ===" << endl;
// 测试用例1:随机无序数组
int test1[] = {39, 80, 76, 41, 13, 29, 50, 78, 30, 11};
int n1 = sizeof(test1) / sizeof(test1[0]);
initArray(a, n1, test1);
cout << "测试用例1(随机无序数组):" << endl;
cout << "排序前:";
printArray(a, n1);
HeapSort(a, n1);
cout << "排序后:";
printArray(a, n1);
cout << endl;
// 测试用例2:已经有序的数组
int test2[] = {12, 23, 34, 45, 56, 67};
int n2 = sizeof(test2) / sizeof(test2[0]);
initArray(a, n2, test2);
cout << "测试用例2(已排序数组):" << endl;
cout << "排序前:";
printArray(a, n2);
HeapSort(a, n2);
cout << "排序后:";
printArray(a, n2);
cout << endl;
// 测试用例3:逆序数组
int test3[] = {98, 76, 54, 32, 10};
int n3 = sizeof(test3) / sizeof(test3[0]);
initArray(a, n3, test3);
cout << "测试用例3(逆序数组):" << endl;
cout << "排序前:";
printArray(a, n3);
HeapSort(a, n3);
cout << "排序后:";
printArray(a, n3);
cout << endl;
// 测试用例4:包含重复元素的数组
int test4[] = {55, 33, 77, 33, 99, 22, 55};
int n4 = sizeof(test4) / sizeof(test4[0]);
initArray(a, n4, test4);
cout << "测试用例4(含重复元素):" << endl;
cout << "排序前:";
printArray(a, n4);
HeapSort(a, n4);
cout << "排序后:";
printArray(a, n4);
cout << endl;
// 测试用例5:单元素数组
int test5[] = {42};
int n5 = sizeof(test5) / sizeof(test5[0]);
initArray(a, n5, test5);
cout << "测试用例5(单元素数组):" << endl;
cout << "排序前:";
printArray(a, n5);
HeapSort(a, n5);
cout << "排序后:";
printArray(a, n5);
}
int main() {
testHeapSort();
return 0;
}
性能
- 空间效率:仅使用了常数个辅助单元,所以空间复杂度为 O(1)。
- 时间效率:建堆时间为 O(n),之后有 n - 1 次向下调整操作,每次调整的时间复杂度为 O(h),所以在最好、最坏和平均情况下,堆排序的时间复杂度为 O(nlog_2(n))。 - 稳定性:进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序算法。
- 适用性:堆排序仅适用于顺序存储的线性表。
归并排序
归并排序与上述基于交换、选择等排序的思想不一样,归并的含义是将两个或两个以上的有序表合并成一个新的有序表。假定待排序表含有 n 个记录,则可将其视为 n 个有序的子表,每个子表的长度为 1,然后两两归并,得到 n / 2(向上取整)个长度为 2 或 1 的有序表;继续两两归并...如此重复,直到合并成一个长度为 n 的有序表为止,这种排序算法称为二路归并排序。
以序列 49, 38, 65, 97, 76, 13, 27 为例:

Merge() 的功能是将前后相邻的两个有序表归并为一个有序表。设两段有序表 A[low..mid]、A[mid+1...high] 存放在同一顺序表中的相邻位置,先将它们复制到辅助数组 B 中。每次从 B 的两段中取出一个记录进行关键字的比较,将较小者放入 A 中,当 B 中有一段的下标超出其对应的表长(该段的所有元素都已复制到 A 中)时,将另一段的剩余部分直接复制到 A 中。
一趟归并排序的操作是,调用 n / 2(向上取整)次算法 Merge(),将 L[1..n] 中前后相邻且长度为 h 的有序段进行两两归并,得到前后相邻、长度为 2h 的有序段,整个归并排序需要进行 log_2(n)(向上取整)趟递归形式的二路归并排序算法是基于分治的,其过程如下。
- 分解:将含有 n 个元素的待排序表分成各含 n / 2 个元素的子表,采用二路归并排序算法对两个子表递归地进行排序。
- 合并:合并两个已排序的子表得到排序结果。
代码实现
#include <iostream>
using namespace std;
#define MAX 100 // 数组最大容量
int a[MAX]; // 全局数组,采用1-based索引
int length; // 数组长度
// 合并两个有序子序列:将a[low..mid]和a[mid+1..high]合并为有序序列
void Merge(int a[], int low, int mid, int high) {
int i = low, j = mid + 1, k = 0; // k为临时数组b的下标
int *b = new int[high - low + 1]; // 动态分配临时数组
// 将两个子序列中的元素按顺序放入临时数组
while (i <= mid && j <= high) {
if (a[i] <= a[j]) b[k++] = a[i++];
else b[k++] = a[j++];
}
// 将剩余元素复制到临时数组
while (i <= mid) b[k++] = a[i++];
while (j <= high) b[k++] = a[j++];
// 将临时数组中的元素复制回原数组
for (i = low, k = 0; i <= high; i++, k++) a[i] = b[k];
delete[] b; // 释放动态分配的内存
}
// 归并排序算法:递归划分并排序
void MergeSort(int a[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2; // 计算中间位置
MergeSort(a, low, mid); // 对左子序列递归排序
MergeSort(a, mid + 1, high); // 对右子序列递归排序
Merge(a, low, mid, high); // 合并两个有序子序列
}
}
// 辅助函数:打印数组元素
void printArray(int arr[], int len) {
for (int i = 1; i <= len; i++) cout << arr[i] << " ";
cout << endl;
}
// 辅助函数:初始化数组(1-based索引)
void initArray(int arr[], int len, const int source[]) {
for (int i = 1; i <= len; i++) arr[i] = source[i - 1];
length = len;
}
// 测试归并排序函数
void testMergeSort() {
cout << "=== 归并排序测试 ===" << endl;
// 测试用例1:随机无序数组
int test1[] = {39, 80, 76, 41, 13, 29, 50, 78, 30, 11};
int n1 = sizeof(test1) / sizeof(test1[0]);
initArray(a, n1, test1);
cout << "测试用例1(随机无序数组):" << endl;
cout << "排序前:";
printArray(a, n1);
MergeSort(a, 1, n1);
cout << "排序后:";
printArray(a, n1);
cout << endl;
// 测试用例2:已经有序的数组
int test2[] = {12, 23, 34, 45, 56, 67};
int n2 = sizeof(test2) / sizeof(test2[0]);
initArray(a, n2, test2);
cout << "测试用例2(已排序数组):" << endl;
cout << "排序前:";
printArray(a, n2);
MergeSort(a, 1, n2);
cout << "排序后:";
printArray(a, n2);
cout << endl;
// 测试用例3:逆序数组
int test3[] = {98, 76, 54, 32, 10};
int n3 = sizeof(test3) / sizeof(test3[0]);
initArray(a, n3, test3);
cout << "测试用例3(逆序数组):" << endl;
cout << "排序前:";
printArray(a, n3);
MergeSort(a, 1, n3);
cout << "排序后:";
printArray(a, n3);
cout << endl;
// 测试用例4:包含重复元素的数组
int test4[] = {55, 33, 77, 33, 99, 22, 55};
int n4 = sizeof(test4) / sizeof(test4[0]);
initArray(a, n4, test4);
cout << "测试用例4(含重复元素):" << endl;
cout << "排序前:";
printArray(a, n4);
MergeSort(a, 1, n4);
cout << "排序后:";
printArray(a, n4);
cout << endl;
// 测试用例5:单元素数组
int test5[] = {42};
int n5 = sizeof(test5) / sizeof(test5[0]);
initArray(a, n5, test5);
cout << "测试用例5(单元素数组):" << endl;
cout << "排序前:";
printArray(a, n5);
MergeSort(a, 1, n5);
cout << "排序后:";
printArray(a, n5);
}
int main() {
testMergeSort();
return 0;
}
二路归并排序性能
- 空间效率:Merge() 操作中,辅助空间刚好为 n 个单元,因此算法的空间复杂度为 O(n)。
- 时间效率:每趟归并的时间复杂度为 O(n),共需进行 log_2(n)(向上取整)趟归并,因此算法的时间复杂度为 O(nlog_2(n))。
- 稳定性:Merge() 操作不会改变相同关键字记录的相对次序,因此二路归并排序算法是一种稳定的排序算法。 - 适用性:归并排序适用于顺序存储和链式存储的线性表。
基数排序
基数排序是一种很特别的排序算法,它不基于比较和移动进行排序,而基于关键字各位的大小进行排序。基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。假设长度为 n 的线性表中每个结点 的关键字由 d 元组(
)组成,满足
。其中
为最高位关键字,
为最低位关键字。
为实现多关键字排序,通常有两种方法:第一种是最高位优先(MSD)法,按关键字位权重递减依次逐层划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列;第二种是最低位优先(LSD)法,按关键字位权重递增依次进行排序,最后形成一个有序序列。
下面描述以 r 位基数的最低位优先基数排序的过程,在排序过程中,使用 r 个队列 Q_0, Q_1, Q_2,...,Q_(r - 1)。基数排序的过程如下:
对 i = 0,1,...,d - 1,依次做一次分配和收集(其实是一次稳定的排序过程)。
分配:开始时,把 Q_0, Q_1, Q_2,...,Q_(r - 1) 各个队列置成空队列,然后依次考察线性表中的每个结点 a_j(j = 0, 1,...,n - 1),若 a_j 的关键字
,就把 a_j 放进 Q_k 队列中。
收集:把 Q_0, Q_1, Q_2,...,Q_(r - 1) 各个队列中的结点依次首尾相接,得到新的结点序列,从而组成新的线性表。。
代码实现
#include <iostream>
#include <queue>
#include <cmath>
using namespace std;
// 辅助函数:求数据的最大位数
int maxbit(int data[], int n) {
int maxData = data[1]; // 1-based索引,从第一个元素开始
// 先求出最大数
for (int i = 2; i <= n; ++i) if (maxData < data[i]) maxData = data[i];
// 求最大数的位数
int d = 1;
int p = 10;
while (maxData >= p) {
maxData /= 10;
++d;
}
return d;
}
// 基数排序(LSD法)
void RadixSort(int data[], int n) {
int d = maxbit(data, n); // 获取最大位数
for (int digit = 1; digit <= d; digit++) { // 修正循环变量名冲突
queue<int> q[10]; // 0-9共10个桶
int* temp = new int[n + 1]; // 临时数组(1-based)
int k = 1; // 临时数组指针
// 计算当前位权重(替换pow避免精度问题)
int weight = 1;
for (int i = 1; i < digit; i++) weight *= 10;
// 将数据分配到桶中
for (int j = 1; j <= n; j++) q[(data[j] / weight) % 10].push(data[j]);
// 从桶中收集数据
for (int j = 0; j < 10; j++) { // 修正桶索引变量
while (!q[j].empty()) { // 使用当前桶j而非digit
temp[k++] = q[j].front(); // 获取队首元素
q[j].pop(); // 弹出队首元素
}
}
// 将临时数组复制回原数组
for (int j = 1; j <= n; j++) data[j] = temp[j];
cout << "第 " << digit << " 轮结果:";
for (int j = 1; j <= n; j++) cout << data[j] << ((j == n) ? '\n' : ' ');
delete[] temp; // 释放临时数组
}
}
// 初始化测试数组
void initArray(int a[], int n, int type) {
switch (type) {
case 1: // 随机无序数组
a[1] = 64; a[2] = 8; a[3] = 216; a[4] = 512; a[5] = 27; a[6] = 125; a[7] = 343; a[8] = 1;
break;
case 2: // 已排序数组
a[1] = 1; a[2] = 8; a[3] = 27; a[4] = 64; a[5] = 125; a[6] = 216; a[7] = 343; a[8] = 512;
break;
case 3: // 逆序数组
a[1] = 512; a[2] = 343; a[3] = 216; a[4] = 125; a[5] = 64; a[6] = 27; a[7] = 8; a[8] = 1;
break;
case 4: // 重复元素数组
a[1] = 16; a[2] = 8; a[3] = 16; a[4] = 8; a[5] = 16; a[6] = 8; a[7] = 16; a[8] = 8;
break;
case 5: // 单元素数组
a[1] = 100; // 仅一个元素
break;
}
}
// 打印数组
void printArray(int a[], int n) {
for (int i = 1; i <= n; i++) cout << a[i] << " ";
cout << endl;
}
// 测试基数排序
void testRadixSort() {
const int MAX_SIZE = 100;
int a[MAX_SIZE];
int n;
// 测试场景1: 随机无序数组
cout << "测试场景1: 随机无序数组" << endl;
n = 8;
initArray(a, n, 1);
cout << "排序前: ";
printArray(a, n);
RadixSort(a, n);
cout << "排序后: ";
printArray(a, n);
cout << endl;
// 测试场景2: 已排序数组
cout << "测试场景2: 已排序数组" << endl;
n = 8;
initArray(a, n, 2);
cout << "排序前: ";
printArray(a, n);
RadixSort(a, n);
cout << "排序后: ";
printArray(a, n);
cout << endl;
// 测试场景3: 逆序数组
cout << "测试场景3: 逆序数组" << endl;
n = 8;
initArray(a, n, 3);
cout << "排序前: ";
printArray(a, n);
RadixSort(a, n);
cout << "排序后: ";
printArray(a, n);
cout << endl;
// 测试场景4: 重复元素数组
cout << "测试场景4: 重复元素数组" << endl;
n = 8;
initArray(a, n, 4);
cout << "排序前: ";
printArray(a, n);
RadixSort(a, n);
cout << "排序后: ";
printArray(a, n);
cout << endl;
// 测试场景5: 单元素数组
cout << "测试场景5: 单元素数组" << endl;
n = 1;
initArray(a, n, 5);
cout << "排序前: ";
printArray(a, n);
RadixSort(a, n);
cout << "排序后: ";
printArray(a, n);
cout << endl;
}
int main() {
testRadixSort();
system("pause");
return 0;
}
性能分析
- 空间效率:一趟排序需要的辅助存储空间为 r (r 个队列:r 个队头指针和 r 个队尾指针),但以后的排序中会重复使用这些队列,所以基数排序的空间复杂度为 O(r)。
- 时间效率:基数排序需要进行 d 趟“分配”和”收集”操作。一趟分配需要遍历所有关键字,时间复杂度为 O(n);一趟收集需要合并 r 个队列,时间复杂度为 O(r)。因此基数排序的时间复杂
度为 O(d(n +r)),它与序列的初始状态无关。 - 稳定性:每一趟分配和收集都是从前往后进行的,不会交换相同关键字的相对位置,因此基数排序是一种稳定的排序算法。
- 适用性:基数排序适用于顺序存储和链式存储的线性表。
计数排序
计数排序也是一种不基于比较的排序算法。计数排序的思想是:对每个待排序元素 x,统计小于 x 的元素个数,利用该信息就可确定 x 的最终位置。例如,若有 8 个元素小于 x,则 x 就排在第 9 号位置上。当有几个元素相同时,该排序方案还需做一定的优化。
在计数排序算法的实现中,假设输入是一个数组 A[n],序列长度为 n,我们还需要两个数组:B[n] 存放输出的排序序列,C[k] 存储计数值。用输入数组 A 中的元素作为数组 C 的下标(索引),而该元素出现的次数存储在该元素作为下标的数组 c 中。算法如下:
代码实现
#include <iostream>
#include <algorithm>
using namespace std;
// 计数排序(支持非负整数排序)
// 参数:a-待排序数组(1-based),b-输出数组(1-based),n-元素个数
void CountSort(int a[], int b[], int n) {
if (n <= 0) return;
// 找出数组中的最大值
int maxVal = a[1];
for (int i = 2; i <= n; i++) if (a[i] > maxVal) maxVal = a[i];
// 创建计数数组并初始化
int* c = new int[maxVal + 1](); // 初始化为0
// 1. 计数阶段:统计每个元素出现的次数
for (int i = 1; i <= n; i++) c[a[i]]++;
// 2. 累加阶段:计算每个元素在输出数组中的位置
for (int i = 1; i <= maxVal; i++) c[i] += c[i - 1];
// 3. 分配阶段:将元素放到输出数组的正确位置(从后往前保证稳定性)
for (int i = n; i >= 1; i--) {
b[c[a[i]]] = a[i];
c[a[i]]--;
}
// 释放临时内存
delete[] c;
}
// 初始化测试数组
void initArray(int a[], int n, int type) {
switch (type) {
case 1: // 随机无序数组
a[1] = 4; a[2] = 2; a[3] = 2; a[4] = 8; a[5] = 3; a[6] = 3; a[7] = 1; a[8] = 5;
break;
case 2: // 已排序数组
a[1] = 1; a[2] = 2; a[3] = 2; a[4] = 3; a[5] = 3; a[6] = 4; a[7] = 5; a[8] = 8;
break;
case 3: // 逆序数组
a[1] = 8; a[2] = 5; a[3] = 4; a[4] = 3; a[5] = 3; a[6] = 2; a[7] = 2; a[8] = 1;
break;
case 4: // 重复元素数组
a[1] = 5; a[2] = 5; a[3] = 5; a[4] = 5; a[5] = 5; a[6] = 5; a[7] = 5; a[8] = 5;
break;
case 5: // 单元素数组
a[1] = 10; // 仅一个元素
break;
}
}
// 打印数组
void printArray(int a[], int n) {
for (int i = 1; i <= n; i++) cout << a[i] << " ";
cout << endl;
}
// 测试计数排序
void testCountSort() {
const int MAX_SIZE = 100;
int a[MAX_SIZE]; // 原始数组
int b[MAX_SIZE]; // 排序结果数组
int n;
// 测试场景1: 随机无序数组
cout << "测试场景1: 随机无序数组" << endl;
n = 8;
initArray(a, n, 1);
cout << "排序前: ";
printArray(a, n);
CountSort(a, b, n);
cout << "排序后: ";
printArray(b, n);
cout << endl;
// 测试场景2: 已排序数组
cout << "测试场景2: 已排序数组" << endl;
n = 8;
initArray(a, n, 2);
cout << "排序前: ";
printArray(a, n);
CountSort(a, b, n);
cout << "排序后: ";
printArray(b, n);
cout << endl;
// 测试场景3: 逆序数组
cout << "测试场景3: 逆序数组" << endl;
n = 8;
initArray(a, n, 3);
cout << "排序前: ";
printArray(a, n);
CountSort(a, b, n);
cout << "排序后: ";
printArray(b, n);
cout << endl;
// 测试场景4: 重复元素数组
cout << "测试场景4: 重复元素数组" << endl;
n = 8;
initArray(a, n, 4);
cout << "排序前: ";
printArray(a, n);
CountSort(a, b, n);
cout << "排序后: ";
printArray(b, n);
cout << endl;
// 测试场景5: 单元素数组
cout << "测试场景5: 单元素数组" << endl;
n = 1;
initArray(a, n, 5);
cout << "排序前: ";
printArray(a, n);
CountSort(a, b, n);
cout << "排序后: ";
printArray(b, n);
cout << endl;
}
int main() {
testCountSort();
system("pause");
return 0;
}
性能分析
- 空间效率:计数排序是一种用空间换时间的做法。输出数组的长度为 n;辅助的计数数组的长度为 k,空间复杂度为 O(n + k)。若不把输出数组视为辅助空间,则空间复杂度为 O(k)。
- 时间效率:总时间复杂度为 O(n + k)。当 k = n 时,计数排序的时间复杂度为 O(n);但当 k > log_2(n) 时,其效率反而不如一些基于比较的排序(如快速排序、堆排序等)。
- 稳定性:上述代码中,循环从后往前遍历输入数组,相同元素在输出数组中的相对位置不会改变,因此计数排序是一种稳定的排序算法。
for (int i = n; i >= 1; i--) { b[c[a[i]]] = a[i]; c[a[i]]--; } - 适用性:计数排序更适用于顺序存储的线性表。计数排序适用于序列中的元素是整数且元素范围(0 ~ k - 1)不能太大,否则会造成辅助空间的浪费。
9.8 各种排序的比较与应用
排序算法性能对比
| 算法种类 | 时间复杂度(最好) | 时间复杂度(平均) | 时间复杂度(最坏) | 空间复杂度 | 是否稳定 |
|---|---|---|---|---|---|
| 直接插入排序 | O(n) | O(n ^ 2) | O(n ^ 2) | O(1) | 是 |
| 冒泡排序 | O(n) | O(n ^ 2) | O(n ^ 2) | O(1) | 是 |
| 简单选择排序 | O(n^2) | O(n ^ 2) | O(n ^ 2) | O(1) | 否 |
| 希尔排序 | 特殊情况优化 | O(n ^ 1.3) | O(n ^ 2) | O(1) | 否 |
| 快速排序 | O(nlog_2(n)) | O(nlog_2(n)) | O(n ^ 2) | O(log_2(n)) | 否 |
| 堆排序 | O(nlog_2(n)) | O(nlog_2(n)) | O(nlog_2(n)) | O(1) | 否 |
| 二路归并排序 | O(nlog_2(n)) | O(nlog_2(n)) | O(nlog_2(n)) | O(n) | 是 |
| 基数排序 | O(d(n + r)) | O(d(n + r)) | O(d(n + r)) | O(r) | 是 |
排序算法的选择与应用
选取排序算法需要考虑的因素
- 待排序的元素个数n。
- 待排序的元素的初始状态。
- 关键字的结构及其分布情况。
- 稳定性的要求。
- 存储结构及辅助空间的大小限制等
若 n 较小,可采用直接插入排序或简单选择排序。直接插入排序所需的记录移动次数较简单选择排序的多,因此当记录本身信息量较大时,用简单选择排序较好。若 n 较大,应采用时间复杂度为 O(nlog_2(n)) 的排序算法,快速排序、堆排序或归并排序,当待排序的关键字随机分布时,快速排序被认为是目前基于比较的内部排序算法中最好的算法,堆排序所需的辅助空间少于快速排序,且不会出现快速排序可能的最坏情况,这两种排序都是不稳定的,若要求稳定且时间复杂度为 O(nlog_2(n)),可选用归并排序。若文件的初始状态已按关键字基本有序,则选用直接插入或冒泡排序为宜。在基于比较的排序算法中,每次比较两个关键字的大小之后,仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,由此可以证明:当文件的 n 个关键字随机分布时,任何借助于“比较”的排序算法,至少需要 O(nlog_2(n)) 的时间。若 n 很大,记录的关键字位数较少且可以分解时,采用基数排序较好。当记录本身信息量较大时,为避免耗费大量时间移动记录,可用链表作为存储结构。

浙公网安备 33010602011771号