1概述
一、定义
排序是将一组数据元素按照某个关键字的值递增或递减的次序重新排列的过程。这个关键字是数据元素中的某个数据项,通过比较关键字的大小来确定数据元素的先后顺序。
二、目的
- 便于查找
- 例如在一个有序数组中查找某个元素,使用二分查找等算法效率更高。如果数据是无序的,可能只能进行顺序查找,时间复杂度较高。
- 数据呈现的有序性
- 在很多应用场景中,如成绩排名、文件按时间顺序排列等,需要数据以有序的形式展示,以便于人们理解和分析。
三、分类
- 内部排序和外部排序
- 内部排序
- 指待排序的数据元素全部存放在计算机内存中的排序算法。常见的内部排序算法有冒泡排序、插入排序、选择排序、快速排序、归并排序、堆排序等。这些算法在处理相对较小规模的数据时效率较高,因为内存的读写速度相对较快。
- 外部排序
- 当待排序的数据量很大,内存无法一次性容纳全部数据时采用的排序方法。外部排序通常需要在内存和外部存储(如磁盘)之间进行数据交换,它会先将数据分成若干个小的数据块,分别进行内部排序,然后再将这些有序的数据块进行归并等操作,最终得到整体有序的序列。
- 内部排序
- 基于比较的排序和非基于比较的排序
- 基于比较的排序
- 这种排序算法通过比较数据元素的关键字大小来确定它们的相对顺序。例如冒泡排序,它通过不断比较相邻元素的大小并交换来使较大(或较小)的元素逐步 "浮" 到数组的一端。常见的基于比较的排序算法的时间复杂度下限是,其中是待排序数据的个数。
- 非基于比较的排序
- 不依赖于元素之间的比较操作来确定顺序。例如计数排序,它是通过统计每个元素出现的次数来确定元素的顺序;桶排序则是将数据分到不同的桶中,然后在每个桶内进行排序;基数排序是根据数据的各位数字来进行排序。非基于比较的排序算法在特定情况下可以实现线性时间复杂度,但它们往往对数据有一定的要求,如数据的取值范围等。
- 基于比较的排序
2插入类排序方法
插入类排序方法主要包括直接插入排序、折半插入排序和希尔排序。
2.1直接插入排序
1 基本思想:直接插入排序是一种简单的排序算法,其基本思想是将待排序的数组分为已排序和未排序两部分,通过不断将未排序部分的元素插入到已排序部分的合适位置,逐步建立起一个有序序列。以下是对直接插入排序的详细介绍,包括其工作原理、算法实现以及时间复杂度分析。
2工作原理
直接插入排序的基本步骤如下:
-
将数组的第一个元素看作已排序的初始状态,其他元素视为未排序。
-
从未排序的元素中选择一个元素,将其与已排序部分的元素进行比较,并找到合适的位置进行插入。
-
重复步骤 2,直到未排序部分的元素全部移动到已排序部分,从而完成排序。
3 示例
假设我们有一个数组 [5, 2, 9, 1, 5, 6],我们要对它进行直接插入排序。过程如下:
-
初始数组:[5, 2, 9, 1, 5, 6]
-
已排序部分:[5],未排序部分:[2, 9, 1, 5, 6]
- 将 2 插入已排序部分,结果:[2, 5]
- 已排序部分:[2, 5],未排序部分:[9, 1, 5, 6]
- 9 已在正确位置,无需移动。
- 已排序部分:[2, 5, 9],未排序部分:[1, 5, 6]
- 将 1 插入,结果:[1, 2, 5, 9]
- 已排序部分:[1, 2, 5, 9],未排序部分:[5, 6]
- 将 5 插入,结果:[1, 2, 5, 5, 9]
- 已排序部分:[1, 2, 5, 5, 9],未排序部分:[6]
- 将 6 插入,结果:[1, 2, 5, 5, 6, 9]
最后得到排序后的数组:[1, 2, 5, 5, 6, 9]。
4C 语言实现
下面是直接插入排序的 C 语言实现代码:
cs
#include <stdio.h>
// 函数声明
void insertionSort(int arr[], int n);
// 主函数
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
insertionSort(arr, n);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
// 插入排序的函数定义
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) { // 从第二个元素开始
int key = arr[i]; // 当前待插入的元素
int j = i - 1;
// 找到适当的位置插入当前元素
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j]; // 向右移动元素
j--;
}
arr[j + 1] = key; // 插入当前元素
}
}
5时间复杂度
-
最坏情况:O(n²)(数组是倒序的情况下需要进行最多的比较和移动)。
-
最好情况:O(n)(数组已经是有序的情况下,只需进行 n-1 次比较)。
-
平均情况:O(n²)。
6空间复杂度
直接插入排序是原地排序算法,因此其空间复杂度为 O(1)。
7稳定性
直接插入排序是稳定的,因为相等的元素在排序完成后依然保持它们在原始数组中的相对顺序。
8 总结
直接插入排序是一种简单、直观的排序方法,适用于数据量较小的场景。虽然在大规模数据上表现不佳,但由于其简单性和稳定性,常常会用于某些特定应用或作为其他复杂排序算法的基础。在许多实战场景中,直接插入排序用于处理几乎已排序的数组时表现尤其良好。
2.2希尔排序
**1基本思想:**希尔排序(Shell Sort)是一种改进的插入排序算法,由Donald Shell于1959年提出。它通过比较相隔一定间隔的元素来工作,而不是直接进行插入排序。随着算法的进行,间隔逐渐减小,直到变为1,此时算法退化为标准的插入排序。希尔排序的主要优点是其相对较高的效率和简单性。
2工作原理
希尔排序的核心思想是将数组分成若干子序列,分别对这些子序列进行插入排序。随着算法的进行,子序列的长度逐渐减小,最终整个数组变为一个子序列,完成最终的排序。主要步骤如下:
-
选择一个增量序列(间隔序列),通常是递减的整数序列。
-
根据当前的增量,将数组划分为若干个子序列。
-
对每个子序列进行插入排序。
-
重复步骤2和3,直到增量减小到1。
-
最后,对整个数组进行一次标准的插入排序。
3示例
假设我们有一个数组 [5, 2, 9, 1, 5, 6],我们要对它进行希尔排序,使用增量序列 [3, 1](即先按间隔3进行排序,然后按间隔1进行排序)。过程如下:
-
初始数组:[5, 2, 9, 1, 5, 6]
-
第一步(间隔3):
-
子序列1:[5, 1] → [1, 5]
-
子序列2:[2, 5] → [2, 5]
-
子序列3:[9, 6] → [6, 9]
第一次排序后数组:[1, 2, 6, 5, 5, 9]
- 第二步(间隔1):
-
子序列:[1, 2, 6, 5, 5, 9]
-
直接进行插入排序(由于间隔为1,此时退化为标准插入排序)
最终排序后的数组:[1, 2, 5, 5, 6, 9]
4 C 语言实现
下面是希尔排序的 C 语言实现代码:
cs
#include <stdio.h>
// 函数声明
void shellSort(int arr[], int n);
// 主函数
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
shellSort(arr, n);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
// 希尔排序的函数定义
void shellSort(int arr[], int n) {
// 初始增量为 n / 2,并对增量进行逐步缩小
for (int gap = n / 2; gap > 0; gap /= 2) {
// 对每个子序列进行插入排序
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j;
// 插入排序
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
}
}
5时间复杂度
希尔排序的时间复杂度取决于所选的增量序列。不同的增量序列会导致不同的时间复杂度:
-
最坏情况:对于某些增量序列,最坏情况下的时间复杂度为 O(n²)。
-
最好情况:对于某些增量序列,时间复杂度可以达到 O(n log² n)。
-
平均情况:取决于增量序列的选择,通常在 O(n*1.25) 到 O(n*1.5) 之间。
6空间复杂度
希尔排序是原地排序算法,因此其空间复杂度为 O(1)。
7稳定性
希尔排序是不稳定的,因为在排序过程中,相同的元素可能会在不同的子序列中进行交换,从而改变它们在原始数组中的相对顺序。
8 总结
希尔排序是一种高效的排序算法,特别适用于中等规模的数据。它通过减少内循环的比较和移动次数来提高效率,同时保持了插入排序的简单性。尽管其时间复杂度不是最优的,但在实践中它通常比其他 O(n²) 级别的排序算法有更好的性能。
2.3二分插入排序
1基本思想:二分插入排序(Binary Insertion Sort)是插入排序的一种优化版本。它通过使用二分查找来确定插入的位置,从而减少比较的次数,提高插入排序的效率。标准插入排序在查找插入位置时是线性查找,而二分插入排序则是使用二分查找,这在大规模数据上可以显著减少比较次数。
2工作原理
二分插入排序的基本步骤如下:
-
将数组分为已排序和未排序两部分。
-
从未排序部分的第一个元素开始,使用二分查找在已排序部分中找到合适的插入位置。
-
将元素插入到找到的位置,并移动已排序部分中的其他元素。
-
重复步骤2和3,直到未排序部分的元素全部插入到已排序部分。
3示例
假设我们有一个数组 [5, 2, 9, 1, 5, 6],我们要对它进行二分插入排序。过程如下:
-
初始数组:[5, 2, 9, 1, 5, 6]
-
当前元素:5(第一个元素,直接视为已排序),已排序部分:[5],未排序部分:[2, 9, 1, 5, 6]
-
插入元素:2
-
二分查找插入位置:low = 0, high = 0, mid = 0
-
插入位置:0
-
结果:[2, 5]
- 插入元素:9
-
二分查找插入位置:low = 1, high = 1, mid = 1
-
插入位置:2
-
结果:[2, 5, 9]
- 插入元素:1
-
二分查找插入位置:low = 0, high = 2, mid = 1, low = 0, high = 1, mid = 0
-
插入位置:0
-
结果:[1, 2, 5, 9]
- 插入元素:5
-
二分查找插入位置:low = 1, high = 3, mid = 2
-
插入位置:3
-
结果:[1, 2, 5, 5, 9]
- 插入元素:6
-
二分查找插入位置:low = 2, high = 4, mid = 3
-
插入位置:3
-
结果:[1, 2, 5, 5, 6, 9]
最终得到排序后的数组:[1, 2, 5, 5, 6, 9]。
4C 语言实现
下面是二分插入排序的 C 语言实现代码:
cs
#include <stdio.h>
// 函数声明
void binaryInsertionSort(int arr[], int n);
// 主函数
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
binaryInsertionSort(arr, n);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
// 二分插入排序的函数定义
void binaryInsertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];
int left = 0;
int right = i - 1;
// 二分查找插入位置
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] > key) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 找到插入位置后,移动元素
for (int j = i - 1; j >= left; j--) {
arr[j + 1] = arr[j];
}
arr[left] = key;
}
}
5时间复杂度
-
最坏情况:O(n²)(在最坏情况下,二分查找可以减少比较次数,但移动元素的次数仍然与标准插入排序相同)。
-
最好情况:O(n log n)(数组已经是有序的情况下,比较次数减少为 O(log n) 次,但移动元素的次数仍为 O(n) 次)。
-
平均情况:O(n²)(相比于标准插入排序,比较次数减少,但移动元素的次数仍为 O(n²) 次)。
6 空间复杂度
二分插入排序是原地排序算法,因此其空间复杂度为 O(1)。
7稳定性
二分插入排序是稳定的,因为相等的元素在排序完成后依然保持它们在原始数组中的相对顺序。
8总结
二分插入排序是一种对标准插入排序的优化,通过减少比较次数提高了算法的效率。尽管在平均和最坏情况下的时间复杂度仍然是 O(n²),但在某些情况下,二分插入排序比标准插入排序有更好的表现,特别是在数据量较大时。
3交换类排序方法
交换类排序方法是一类通过不断交换元素位置来实现排序的算法,主要包括冒泡排序和快速排序。
3.1冒泡排序
1基本思想:冒泡排序(Bubble Sort)是一种简单的排序算法,它通过重复地遍历要排序的列表,比较相邻的元素并交换它们的位置,直到整个列表排序完成。由于在排序过程中,较小的元素会逐渐"浮"到列表的顶部,较大的元素会"沉"到底部,因此得名"冒泡排序"。
2工作原理
冒泡排序的基本步骤如下:
-
从列表的第一个元素开始,依次比较相邻的两个元素。
-
如果前一个元素大于后一个元素,则交换它们的位置。
-
继续遍历列表,直到没有需要交换的元素为止。
-
重复上述步骤,直到整个列表排序完成。
3示例
假设我们有一个数组 [5, 2, 9, 1, 5, 6],我们要对它进行冒泡排序。过程如下:
-
初始数组:[5, 2, 9, 1, 5, 6]
-
第一轮遍历:
-
比较 5 和 2,交换位置:[2, 5, 9, 1, 5, 6]
-
比较 5 和 9,不交换位置:[2, 5, 9, 1, 5, 6]
-
比较 9 和 1,交换位置:[2, 5, 1, 9, 5, 6]
-
比较 9 和 5,交换位置:[2, 5, 1, 5, 9, 6]
-
比较 9 和 6,交换位置:[2, 5, 1, 5, 6, 9]
- 第二轮遍历:
-
比较 2 和 5,不交换位置:[2, 5, 1, 5, 6, 9]
-
比较 5 和 1,交换位置:[2, 1, 5, 5, 6, 9]
-
比较 5 和 5,不交换位置:[2, 1, 5, 5, 6, 9]
-
比较 5 和 6,不交换位置:[2, 1, 5, 5, 6, 9]
- 第三轮遍历:
-
比较 2 和 1,交换位置:[1, 2, 5, 5, 6, 9]
-
比较 2 和 5,不交换位置:[1, 2, 5, 5, 6, 9]
-
比较 5 和 5,不交换位置:[1, 2, 5, 5, 6, 9]
- 第四轮遍历:
-
比较 1 和 2,不交换位置:[1, 2, 5, 5, 6, 9]
-
比较 2 和 5,不交换位置:[1, 2, 5, 5, 6, 9]
- 第五轮遍历:
- 比较 1 和 2,不交换位置:[1, 2, 5, 5, 6, 9]
最终得到排序后的数组:[1, 2, 5, 5, 6, 9]。
4 C 语言实现
下面是冒泡排序的 C 语言实现代码:
cs
#include <stdio.h>
// 函数声明
void bubbleSort(int arr[], int n);
// 主函数
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
bubbleSort(arr, n);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
// 冒泡排序的函数定义
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
// 标记是否发生了交换
int swapped = 0;
// 每一轮遍历
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = 1;
}
}
// 如果没有发生交换,说明数组已经有序,提前退出
if (swapped == 0) {
break;
}
}
}
5时间复杂度
-
最坏情况:O(n²)(当数组完全逆序时,需要进行 n(n-1)/2 次比较和交换)。
-
最好情况 :O(n)(当数组已经有序时,只需要进行一次遍历,比较 n-1 次,但不进行交换)。
-
平均情况:O(n²)(平均情况下,需要进行 n(n-1)/4 次比较和交换)。
6 空间复杂度
冒泡排序是原地排序算法,因此其空间复杂度为 O(1)。
7稳定性
冒泡排序是稳定的,因为相等的元素在排序完成后依然保持它们在原始数组中的相对顺序。
8总结
冒泡排序是一种简单但效率较低的排序算法,特别适合用于教学和理解排序算法的基本概念。尽管它在实际应用中很少使用,但它的实现简单,易于理解和调试。对于小规模数据或部分有序的数据,冒泡排序可能是一个合适的选择。
3.2快速排序
**1基本思想:**快速排序(Quick Sort)是一种高效的排序算法,采用分治法(Divide and Conquer)策略,将一个数组分为两个子数组,再分别对这两个子数组进行排序。快速排序的平均时间复杂度为 (O(n log n)),通常在实际应用中表现优越,因此被广泛使用。
2工作原理
快速排序的基本步骤如下:
-
选择基准:从数组中选择一个元素作为"基准"(pivot),通常可以选择第一个元素、最后一个元素或者随机选择一个元素。
-
分区:
-
将数组重新排列,使得所有比基准小的元素位于基准的左边,所有比基准大的元素位于基准的右边。
-
此时,基准元素就处于它最终的位置。
- 递归排序:对基准左边和右边的子数组进行递归调用快速排序。
3示例
假设我们有一个数组 [9, 7, 5, 11, 12, 2, 14]。我们将使用快速排序对其进行排序,步骤如下:
-
原始数组:[9, 7, 5, 11, 12, 2, 14]
-
选择基准:选择最后一个元素12作为基准。
-
分区过程:
-
将小于12的元素移到左侧,>12的移到右侧。
-
排序后:[9, 7, 5, 11, 2, 12, 14](12位于正确的位置)。
-
递归对左边子数组 [9, 7, 5, 11, 2] 和右边子数组 `[14]` 进行快速排序。
-
继续分区和递归,最终得到排序完成的数组 [2, 5, 7, 9, 11, 12, 14]。
4C 语言实现
快速排序的 C 语言实现代码如下:
cs
#include <stdio.h>
// 函数声明
void quickSort(int arr[], int low, int high);
int partition(int arr[], int low, int high);
// 主函数
int main() {
int arr[] = {9, 7, 5, 11, 12, 2, 14};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
quickSort(arr, 0, n - 1);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
// 快速排序的函数定义
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high); // 获取分区索引
// 递归排序
quickSort(arr, low, pi - 1); // 排序基准左侧
quickSort(arr, pi + 1, high); // 排序基准右侧
}
}
// 分区函数
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 选择最后一个元素为基准
int i = (low - 1); // 小于基准的元素索引
for (int j = low; j < high; j++) {
if (arr[j] < pivot) { // 如果当前元素小于基准
i++; // 增加小于基准的元素索引
// 交换元素
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 交换基准元素到正确位置
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1; // 返回基准的索引
}
5时间复杂度
-
最坏情况:(O(n*2))(当输入数组已经是有序的,且每次选择的基准是最大或最小元素时)。
-
最好情况:(O(n log n))(每次划分都将数组平均分成两部分)。
-
平均情况:(O(n log n))。
6 空间复杂度
快速排序是一个原地排序算法,空间复杂度为 (O(log n))(递归调用栈的深度)。最大情况下可能为 (O(n))。
7稳定性
快速排序是非稳定的。相同的元素在排序后可能会改变其相对位置。
8总结
快速排序是一种高效且广泛使用的排序算法,特别适合大规模数据的排序。它的优点是平均情况下速度较快,然而在最坏情况下可能会出现性能问题。因此,在实际应用中,常常结合其他排序方法(如随机化基准选择)来提高性能。
4选择类排序方法
选择类排序方法是一类通过不断选择最值元素来实现排序的算法,主要包括简单选择排序和堆排序。
4.1简单选择排序
**1基本思想:**简单选择排序(Simple Selection Sort)是选择排序算法的一种实现方式。其基本思想是通过逐步选择未排序部分的最小(或最大)元素并将其放到已排序部分的末尾。这个过程重复进行,直到整个数组都有序。
2工作原理
简单选择排序的基本步骤如下:
-
找到最小元素:在未排序部分中找到最小(或最大)元素。
-
交换位置:将找到的最小(或最大)元素与未排序部分的第一个元素交换位置。
-
缩小范围:将已排序部分的边界向右扩展一位,未排序部分缩小一位。
-
重复过程:重复步骤1-3,直到所有元素都被排序。
3 示例
假设我们有一个数组 [64, 25, 12, 22, 11],我们将使用简单选择排序对其进行排序,步骤如下:
-
找到最小元素 11,和第一个元素 64 交换:[11,25, 12, 22, 64]
-
找到最小元素 12,和第二个元素 25 交换:[11, 12,25, 22, 64]
-
找到最小元素 22,和第三个元素 25 交换:[11, 12, 22, 25, 64]
-
已排序的部分为 [11, 12, 22],剩下的 [25, 64] 依次到最后,不需要交换。
最终得到排序后的数组:[11, 12, 22, 25, 64]。
4C 语言实现
下面是简单选择排序的 C 语言实现代码:
cs
#include <stdio.h>
// 函数声明
void selectionSort(int arr[], int n);
// 主函数
int main() {
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
selectionSort(arr, n);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
// 简单选择排序的函数定义
void selectionSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
// 假设当前 i 位置是最小值
int minIndex = i;
// 在剩余部分寻找最小值
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j; // 更新最小值索引
}
}
// 交换元素
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
5 时间复杂度
-
最坏情况:(O(n*2))(无论输入顺序如何,选择排序都需要进行 (n-1) 次查找)。
-
最好情况:(O(n*2))(同上述情况)。
-
平均情况:(O(n*2))。
6空间复杂度
简单选择排序是原地排序算法,空间复杂度为 \(O(1)\)。
7稳定性
简单选择排序是不稳定的,因为在交换时,相同的元素可能会改变相对顺序。
8总结
简单选择排序是一种简单易懂的排序算法,适合小规模的数据排序。在实际使用中,由于其时间复杂度较高,通常不用于大规模数据的排序。然而,选择排序的原理和实现过程对于学习其他更复杂的排序算法有很大的帮助。
4.2堆排序
堆的基本概念
-
堆:堆是一种完全二叉树,分为最大堆和最小堆。
- 最大堆:每个节点的值都大于或等于其子节点的值。
- 最小堆:每个节点的值都小于或等于其子节点的值。
-
完全二叉树:除了最后一层外,其他层的节点都是满的,且最后一层的节点都尽量靠左。
1基本思想:利用堆这种数据结构的性质,将待排序序列构建成一个堆,然后不断取出堆顶元素,并调整堆使其继续满足堆的性质,从而实现排序。
2算法步骤
-
构建初始堆
- 首先将待排序的数组看作是一个完全二叉树。
- 从最后一个非叶子节点开始,逐步向上调整,使每个节点都满足堆的性质(对于大根堆,父节点的值大于等于子节点的值;对于小根堆,父节点的值小于等于子节点的值)。
-
排序过程
- 取出堆顶元素,将其与当前无序序列的最后一个元素交换位置。
- 对交换后的堆进行调整,使其重新成为一个堆。
- 重复上述步骤,直到所有元素都被取出并排序完毕。
例如,对于数组 [4, 6, 8, 5, 9]
-
构建大根堆:
-
首先将数组看作完全二叉树:
4 / \ 6 8 / \ / 5 9
-
-
从最后一个非叶子节点 6 开始调整,6 比其子节点 5 和 9 都大,无需调整。
-
接着调整节点 4,4 小于其子节点 6 和 8,不满足大根堆性质。交换 4 和 8,得到:
8 / \ 6 4 / \ / 5 9
-
继续调整,8 大于其子节点 6 和 5,无需调整。此时大根堆构建完成。
-
排序过程:
- 取出堆顶元素 8,与最后一个元素 9 交换,得到 [9, 6, 4, 5, 8]。
- 对剩余元素调整为大根堆,从节点 6 开始调整,交换 6 和 9,得到 [9, 6, 4, 5, 8](此时无需继续调整)。
- 取出堆顶元素 9,与最后一个元素 5 交换,得到 [5, 6, 4, 9, 8]。
- 调整堆,交换 6 和 5,得到 [6, 5, 4, 9, 8]。
- 取出堆顶元素 6,与最后一个元素 4 交换,得到 [4, 5, 6, 9, 8]。
- 调整堆,交换 5 和 4,得到 [5, 4, 6, 9, 8]。
- 取出堆顶元素 5,与最后一个元素 6 交换,得到 [6, 4, 5, 9, 8](此时无需继续调整)。
- 取出堆顶元素 6,与最后一个元素 8 交换,得到 [8, 4, 5, 9, 6]。
- 调整堆,交换 4 和 8,得到 [8, 4, 5, 9, 6](此时无需继续调整)。
- 取出堆顶元素 8,与最后一个元素 9 交换,得到 [9, 4, 5, 8, 6]。
- 调整堆,交换 4 和 9,得到 [9, 4, 5, 8, 6](此时无需继续调整)。
- 取出堆顶元素 9,与最后一个元素 6 交换,得到 [6, 4, 5, 8, 9]。
- 调整堆,交换 4 和 6,得到 [6, 4, 5, 8, 9](此时无需继续调整)。
- 取出堆顶元素 6,与最后一个元素 8 交换,得到 [8, 4, 5, 6, 9]。
- 调整堆,交换 4 和 8,得到 [8, 4, 5, 6, 9](此时无需继续调整)。
- 取出堆顶元素 8,与最后一个元素 9 交换,得到 [9, 4, 5, 6, 8]。
- 调整堆,交换 4 和 9,得到 [9, 4, 5, 6, 8](此时无需继续调整)。
- 取出堆顶元素 9,排序完成,得到 [4, 5, 6, 8, 9]。
-
C 语言实现
下面是堆排序的 C 语言实现代码
cs
#include <stdio.h>
// 函数声明
void heapSort(int arr[], int n);
void heapify(int arr[], int n, int i);
void swap(int *a, int *b);
// 主函数
int main() {
int arr[] = {4, 10, 3, 5, 1};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
heapSort(arr, n);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
// 堆排序的函数定义
void heapSort(int arr[], int n) {
// 构建最大堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 逐个抽取元素
for (int i = n - 1; i > 0; i--) {
// 将当前根节点(最大值)与数组末尾元素交换
swap(&arr[0], &arr[i]);
// 重新调整堆
heapify(arr, i, 0);
}
}
// 调整堆的函数定义
void heapify(int arr[], int n, int i) {
int largest = i; // 初始化最大值为根节点
int left = 2 * i + 1; // 左子节点
int right = 2 * i + 2; // 右子节点
// 如果左子节点比根节点大
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点比当前最大值大
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值不是根节点
if (largest != i) {
swap(&arr[i], &arr[largest]);
// 递归调整受影响的子树
heapify(arr, n, largest);
}
}
// 交换两个元素的函数定义
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
时间复杂度
- 构建堆:时间复杂度为 O(n)。
- 排序:每次交换和调整堆的时间复杂度为 O(logn),总共需要 n 次,因此总时间复杂度为 O(nlogn)。
空间复杂度
堆排序是原地排序算法,空间复杂度为 O(1)。
稳定性
堆排序是不稳定的,因为在交换时,相同的元素可能会改变相对顺序。
总结
堆排序是一种高效的排序算法,适合用于大规模数据的排序。它的时间复杂度为 O(nlogn),并且是原地排序算法,空间复杂度为 O(1)(。由于其非稳定的特性,堆排序在某些场景下可能不如其他排序算法适用,但对于内存有限且需要高效排序的场景,堆排序是一个很好的选择。
5归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。
1、基本原理
归并排序采用分治(Divide and Conquer)策略,将待排序序列分成若干个子序列,分别对每个子序列进行排序,然后将已排序的子序列合并成一个有序序列。
2、算法步骤
-
分割:
- 把长度为 n 的输入序列分成两个长度为 n/2 的子序列。
- 如果子序列的长度不为 1,则继续分割,直到每个子序列的长度为 1。
-
归并:
- 比较两个已排序的子序列的最小元素,将较小的元素取出并放入新的有序序列中。
- 重复这个过程,直到两个子序列中的所有元素都被合并到新的有序序列中。
例如,对于序列 [5, 3, 8, 2, 7, 1, 6, 4]:
- 分割过程:
- 第一次分割得到 [5, 3, 8, 2] 和 [7, 1, 6, 4]。
- 继续分割得到 [5, 3]、[8, 2]、[7, 1]、[6, 4]。
- 再分割得到 [5]、[3]、[8]、[2]、[7]、[1]、[6]、[4]。
- 归并过程:
- 合并 [5] 和 [3] 得到 [3, 5]。
- 合并 [8] 和 [2] 得到 [2, 8]。
- 合并 [7] 和 [1] 得到 [1, 7]。
- 合并 [6] 和 [4] 得到 [4, 6]。
- 继续合并 [3, 5] 和 [2, 8] 得到 [2, 3, 5, 8]。
- 合并 [1, 7] 和 [4, 6] 得到 [1, 4, 6, 7]。
- 最后合并 [2, 3, 5, 8] 和 [1, 4, 6, 7] 得到 [1, 2, 3, 4, 5, 6, 7, 8]。
3C 语言中的归并排序实现
以下是一个用 C 语言实现的归并排序的示例代码:
cs
#include <stdio.h>
// 合并两个已排序的子数组
void merge(int arr[], int left, int mid, int right) {
int i, j, k;
int n1 = mid - left + 1; // 左半部分的大小
int n2 = right - mid; // 右半部分的大小
// 创建临时数组
int L[n1], R[n2];
// 将数据复制到临时数组 L[] 和 R[]
for (i = 0; i < n1; i++) {
L[i] = arr[left + i];
}
for (j = 0; j < n2; j++) {
R[j] = arr[mid + 1 + j];
}
// 合并临时数组 L[] 和 R[]
i = 0; // 初始化左半部分的索引
j = 0; // 初始化右半部分的索引
k = left; // 初始化合并后的数组的索引
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
// 复制 L[] 的剩余部分(如果有的话)
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
// 复制 R[] 的剩余部分(如果有的话)
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
// 归并排序的递归实现
void mergeSort(int arr[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2; // 防止溢出
// 递归排序左半部分和右半部分
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
// 合并已排序的部分
merge(arr, left, mid, right);
}
}
// 打印数组的函数
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 主函数
int main() {
int arr[] = {38, 27, 43, 3, 9, 82, 10};
int arr_size = sizeof(arr) / sizeof(arr[0]);
printf("原始数组:\n");
printArray(arr, arr_size);
mergeSort(arr, 0, arr_size - 1);
printf("排好序的数组:\n");
printArray(arr, arr_size);
return 0;
}
4、时间复杂度
归并排序的时间复杂度始终为 O(nlogn),其中 n 是待排序元素的个数。这是因为无论原始序列的状态如何,每次分割都将问题规模减半,而每次归并操作需要O(n) 的时间。
5、空间复杂
归并排序的空间复杂度为O(n) ,因为在归并过程中需要额外的空间来存储临时的有序序列
6、稳定性
归并排序是稳定的排序算法。在归并过程中,当比较两个相等的元素时,会优先将来自前面子序列的元素放入新的有序序列中,从而保证了相等元素的相对顺序不变。
6线性时间排序算法
线性时间排序算法是指能够在与输入规模成线性关系的时间内完成排序的算法。常见的线性时间排序算法有计数排序、基数排序和桶排序。
6.1计数排序
-
基本原理:
- 计数排序是一种非比较型整数排序算法。它通过统计每个元素在序列中出现的次数,然后根据元素的出现次数确定其在排序后的序列中的位置。
-
算法步骤:
- 找出待排序数组中的最大和最小元素,确定计数范围。
- 创建一个计数数组,其长度为计数范围,初始值为 0。
- 遍历待排序数组,统计每个元素出现的次数,将次数存入计数数组中对应位置。
- 对计数数组进行累加操作,使得每个位置上的数值表示小于等于该位置对应元素的个数。
- 创建一个结果数组,用于存储排序后的结果。
- 从后往前遍历待排序数组,根据计数数组确定每个元素在结果数组中的位置,并将元素放入结果数组中,同时对计数数组对应位置的值减 1。
-
实例:
- 待排序数组为 [2, 5, 3, 0, 2, 3, 0, 3]。
- 首先确定计数范围为 0 到 5。
- 创建计数数组 [0, 0, 0, 0, 0, 0]。
- 统计每个元素出现的次数后,计数数组变为 [2, 0, 0, 3, 1, 1]。
- 进行累加操作后,计数数组变为 [2, 2, 2, 5, 6, 7]。
- 创建结果数组,从后往前遍历待排序数组,第一个元素 3 在计数数组中对应位置的值为 5,将 3 放入结果数组的第 4 个位置,同时计数数组对应位置的值减 1,变为 [2, 2, 2, 4, 6, 7]。依次类推,最终结果数组为 [0, 0, 2, 2, 3, 3, 3, 5]。
-
C 语言代码实现:
cs
#include <stdio.h>
#include <stdlib.h>
void countingSort(int arr[], int size) {
int max = arr[0];
int min = arr[0];
// 找到数组中的最大值和最小值
for (int i = 1; i < size; i++) {
if (arr[i] > max) {
max = arr[i];
}
if (arr[i] < min) {
min = arr[i];
}
}
// 计数数组的大小为 max - min + 1
int range = max - min + 1;
int count[range];
int output[size];
// 初始化计数数组为 0
for (int i = 0; i < range; i++) {
count[i] = 0;
}
// 统计每个元素的出现次数
for (int i = 0; i < size; i++) {
count[arr[i] - min]++;
}
// 累加计数数组
for (int i = 1; i < range; i++) {
count[i] += count[i - 1];
}
// 根据累加后的计数数组放置元素
for (int i = size - 1; i >= 0; i--) {
output[count[arr[i] - min] - 1] = arr[i];
count[arr[i] - min]--;
}
// 将排序后的元素复制回原数组
for (int i = 0; i < size; i++) {
arr[i] = output[i];
}
}
// 打印数组的函数
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 主函数
int main() {
int arr[] = {4, 2, 2, 8, 3, 3, 1};
int size = sizeof(arr) / sizeof(arr[0]);
printf("原始数组:\n");
printArray(arr, size);
countingSort(arr, size);
printf("排好序的数组:\n");
printArray(arr, size);
return 0;
}
6.2基数排序
-
基本原理:
- 基数排序是一种非比较型整数排序算法。它按照位数将整数进行分组,然后对每一位分别进行排序,从最低位到最高位依次进行,最终得到有序序列。
-
算法步骤:
- 确定待排序数组中的最大元素,确定排序的位数。
- 从最低位开始,依次对每一位进行排序。
- 对于每一位的排序,可以使用稳定的计数排序或其他线性时间排序算法。
- 重复步骤 2 和 3,直到对最高位进行排序完成。
-
实例:
- 待排序数组为 [123, 45, 678, 90, 234]。
- 首先确定最大元素为 678,有三位数,所以进行三次排序。
- 第一次对个位进行排序,得到 [90, 45, 123, 234, 678]。
- 第二次对十位进行排序,得到 [45, 90, 123, 234, 678]。
- 第三次对百位进行排序,得到 [45, 90, 123, 234, 678],排序完成。
-
C 语言代码实现:
cs#include <stdio.h> #include <stdlib.h> // 获取数组中的最大值 int getMax(int arr[], int size) { int max = arr[0]; for (int i = 1; i < size; i++) { if (arr[i] > max) { max = arr[i]; } } return max; } // 基数排序中的计数排序部分 void countingSortForRadix(int arr[], int size, int exp) { int output[size]; int count[10] = {0}; // 统计每个数字的出现次数 for (int i = 0; i < size; i++) { count[(arr[i] / exp) % 10]++; } // 累加计数数组 for (int i = 1; i < 10; i++) { count[i] += count[i - 1]; } // 根据累加后的计数数组放置元素 for (int i = size - 1; i >= 0; i--) { output[count[(arr[i] / exp) % 10] - 1] = arr[i]; count[(arr[i] / exp) % 10]--; } // 将排序后的元素复制回原数组 for (int i = 0; i < size; i++) { arr[i] = output[i]; } } // 基数排序 void radixSort(int arr[], int size) { int max = getMax(arr, size); // 对每一位进行计数排序 for (int exp = 1; max / exp > 0; exp *= 10) { countingSortForRadix(arr, size, exp); } } // 打印数组的函数 void printArray(int arr[], int size) { for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } printf("\n"); } // 主函数 int main() { int arr[] = {170, 45, 75, 90, 802, 24, 2, 66}; int size = sizeof(arr) / sizeof(arr[0]); printf("原始数组:\n"); printArray(arr, size); radixSort(arr, size); printf("排好序的数组:\n"); printArray(arr, size); return 0; }
6.3桶排序
-
基本原理:
- 桶排序是一种分布式排序算法。它将待排序元素分配到不同的桶中,然后对每个桶中的元素进行单独排序,最后将所有桶中的元素依次取出,得到有序序列。
-
算法步骤:
- 确定桶的数量和每个桶的范围。
- 将待排序元素分配到各个桶中。
- 对每个桶中的元素进行排序,可以使用其他排序算法。
- 依次取出各个桶中的元素,得到有序序列。
-
实例:
- 待排序数组为 [0.89, 0.56, 0.65, 0.12, 0.68, 0.34]。
- 假设将数据分为 5 个桶,每个桶的范围为 [0, 0.2), [0.2, 0.4), [0.4, 0.6), [0.6, 0.8), [0.8, 1.0)。
- 将元素分配到各个桶中后,第一个桶中有 0.12,第二个桶中有 0.34,第三个桶中有 0.56 和 0.65,第四个桶中有 0.68,第五个桶中有 0.89。
- 对每个桶中的元素进行插入排序(这里可以使用任何排序算法)。
- 依次取出各个桶中的元素,得到有序序列 [0.12, 0.34, 0.56, 0.65, 0.68, 0.89]。
-
C 语言代码实现:
cs#include <stdio.h> #include <stdlib.h> // 定义链表节点结构 typedef struct ListNode { double data; struct ListNode* next; } ListNode; // 插入排序函数,用于对链表进行排序 void insertionSortList(ListNode* head) { if (head == NULL || head->next == NULL) { return; } ListNode* sortedList = NULL; ListNode* current = head; while (current!= NULL) { ListNode* next = current->next; if (sortedList == NULL || current->data < sortedList->data) { current->next = sortedList; sortedList = current; } else { ListNode* temp = sortedList; while (temp->next!= NULL && temp->next->data < current->data) { temp = temp->next; } current->next = temp->next; temp->next = current; } current = next; } head = sortedList; } // 桶排序函数 void bucketSort(double arr[], int n) { // 创建桶数组,并初始化每个桶为 NULL ListNode** buckets = (ListNode**)malloc(n * sizeof(ListNode*)); for (int i = 0; i < n; i++) { buckets[i] = NULL; } // 将元素分配到各个桶中 for (int i = 0; i < n; i++) { int bucketIndex = arr[i] * n; ListNode* newNode = (ListNode*)malloc(sizeof(ListNode)); newNode->data = arr[i]; newNode->next = buckets[bucketIndex]; buckets[bucketIndex] = newNode; } // 对每个桶中的元素进行排序 for (int i = 0; i < n; i++) { insertionSortList(buckets[i]); } // 将各个桶中的元素依次取出,得到有序序列 int index = 0; for (int i = 0; i < n; i++) { ListNode* current = buckets[i]; while (current!= NULL) { arr[index++] = current->data; ListNode* temp = current; current = current->next; free(temp); } } free(buckets); }
7通用类型数据的排序
通用类型数据的排序是指将一组包含不同数据类型(如整数、浮点数、字符等)或者自定义数据类型的数据按照特定的规则进行重新排列的过程。
C 语言中的通用类型数据排序
在 C 语言中,没有像 Python 中的 sorted()
函数那样的内置排序函数。通常,你需要自己实现排序算法。C 标准库中提供了 qsort()
函数,可以对任意类型的数组进行排序。
qsort()
函数
qsort()
函数可以对任何类型的数组进行排序。它的原型如下:
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
base
:指向要排序的数组的指针。nmemb
:数组中元素的数量。size
:每个元素的大小(以字节为单位)。compar
:比较函数的指针,用于指定排序的顺序。
示例:对整数数组进行排序
#include <stdio.h>
#include <stdlib.h>
// 比较函数:用于比较两个整数
int compare_int(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}
int main() {
int numbers[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
int n = sizeof(numbers) / sizeof(numbers[0]);
// 使用 qsort 对数组进行排序
qsort(numbers, n, sizeof(int), compare_int);
// 输出排序后的数组
for (int i = 0; i < n; i++) {
printf("%d ", numbers[i]);
}
printf("\n");
return 0;
}
示例:对字符串数组进行排序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 比较函数:用于比较两个字符串
int compare_string(const void *a, const void *b) {
return strcmp(*(const char **)a, *(const char **)b);
}
int main() {
const char *words[] = {"apple", "banana", "cherry", "date"};
int n = sizeof(words) / sizeof(words[0]);
// 使用 qsort 对字符串数组进行排序
qsort(words, n, sizeof(const char *), compare_string);
// 输出排序后的字符串数组
for (int i = 0; i < n; i++) {
printf("%s ", words[i]);
}
printf("\n");
return 0;
}
示例:学生数据排序
假设我们要根据学生的成绩对学生数据进行排序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义学生结构体
typedef struct {
char name[50]; // 学生姓名
int age; // 学生年龄
float score; // 学生成绩
} Student;
// 比较函数:按成绩排序
int compare_scores(const void *a, const void *b) {
Student *studentA = (Student *)a;
Student *studentB = (Student *)b;
// 按分数升序排列,如果成绩相同则按姓名排序
if (studentA->score != studentB->score) {
return (studentA->score > studentB->score) ? 1 : -1;
} else {
return strcmp(studentA->name, studentB->name);
}
}
int main() {
// 创建学生数据
Student students[] = {
{"Alice", 20, 88.5},
{"Bob", 22, 95.0},
{"Charlie", 21, 85.0},
{"David", 23, 90.0},
{"Eva", 19, 88.5}
};
int n = sizeof(students) / sizeof(students[0]);
// 排序学生数据
qsort(students, n, sizeof(Student), compare_scores);
// 输出排序后的学生数据
printf("Sorted student data (by score):\n");
for (int i = 0; i < n; i++) {
printf("Name: %s, Age: %d, Score: %.2f\n", students[i].name, students[i].age, students[i].score);
}
return 0;
}
总结:
排序算法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 | 所需额外空间说明 |
---|---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n²) | O(n) | O(1) | 稳定 | 仅需几个临时变量用于交换元素。 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 | 仅需几个临时变量用于交换元素。 |
插入排序 | O(n²) | O(n²) | O(n) | O(1) | 稳定 | 仅需几个临时变量用于交换元素。 |
快速排序 | O(nlogn) | O(n²) | O(nlogn) | O(logn) - O(n) | 不稳定 | 递归调用需要栈空间,取决于递归深度。 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 | 需要额外的与原数组大小相同的空间用于临时存储。 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 | 仅需几个临时变量用于交换元素和调整堆。 |
希尔排序 | O (nlog²n) - O (n²)(较复杂) | O(n²) | O(nlog²n) | O(1) | 不稳定 | 仅需几个临时变量用于交换元素。 |
计数排序 | O (n + k)(k 为数据范围) | O(n + k) | O(n + k) | O(k) | 稳定 | 需要额外的空间存储计数数组和临时数组,大小取决于数据范围 k。 |
桶排序 | O (n + k)(k 为桶的数量) | O(n²) | O(n + k) | O(n + k) | 稳定(取决于桶内排序算法) | 需要额外的空间创建桶和临时存储,大小取决于桶的数量和数据分布。 |
基数排序 | O (d (n + k))(d 为位数,k 为进制) | O(d(n + k)) | O(d(n + k)) | O(n + k) | 稳定 | 需要额外的空间存储计数数组和临时数组,大小取决于数 |