引言:
排序算法是一种将数据元素集合按照特定顺序(通常是升序或降序)重新排列的算法。在计算机科学中,排序算法是基本且重要的一环,其目的在于将杂乱无章的数据按照非递减或非递增的顺序进行排列,以便数据的检索和管理。排序算法的稳定性是一个重要评估标准,稳定的排序算法会保持具有相等关键字记录的相对次序不变。总的来说,排序算法是数据处理过程中不可或缺的工具,它们各有特点和适用场景。在选择适合的排序算法时,不仅要考虑算法的时间和空间复杂度,还要考虑待排序数据的特点以及算法的稳定性等因素。
1.插入排序
插入排序法是一种简单直观的排序算法,适用于小规模数据的排序
算法步骤与过程:从第二个元素开始,将其与前面的元素依次比较并进行必要的交换,直到找到其在有序序列中的正确位置。这个过程一直重复,直到所有的元素都已经被插入到正确的位置,此时整个数组就变得有序了
代码样例:
#include <stdio.h>
void insertion_sort(int arr[], int n) {
int i, j, key;
for (i = 1; i < n; i++) {
key = arr[i];
j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
int main() {
int arr[] = {22, 56, 45, 54, 44};
int n = sizeof(arr) / sizeof(arr[0]);
insertion_sort(arr, n);
return 0;
}
性能分析:
时间复杂度:
最坏情况:在最坏的情况下,即数组完全逆序时,每一个元素都需要与它之前的所有元素进行比较和移动,因此时间复杂度为(n^2)。这种情况下,插入排序的性能较差,特别是对于大数据集来说,排序效率较低。
最好情况:如果数组已经是升序排列,那么插入排序的时间复杂度可以优化到(n),因为每个元素只需要比较和移动一次即可找到合适的位置。
平均情况:对于随机数据,插入排序的平均时间复杂度也是(n^2),但相比最坏情况会有所改善,因为并非每次比较都需要移动元素。
空间复杂度:
插入排序是一种原地排序算法,它只需要一个额外的存储空间来暂时存放当前被插入的元素,因此其空间复杂度为(1)。这意味着插入排序不需要额外的存储空间,对于空间资源有限的情况非常适用。
稳定性:
插入排序是稳定的排序算法,因为在排序过程中,相等的元素的相对位置不会改变。这对于某些需要保持等值元素顺序的应用场合非常重要
2.希尔排序
希尔排序是一种基于插入排序算法的改进版本,通过预排序数据组来提高整体排序效率。这一方法不仅优化了大规模数据的处理速度,还在某种程度上保持了插入排序的稳定性
算法步骤与过程:
设置增量:希尔排序首先需要定义一个增量序列,通常初始增量设置为数组长度的一半或其它值,随后逐渐减小直至为1。
分组排序:根据当前增量把数组分成相应的子数组,并在每个子数组内部执行直接插入排序。这一步骤在整个数组上反复进行,每次调整增量大小,直到增量为1,此时整个数组作为一个单一的组进行最后的插入排序。 子表元素在逻辑上是连续的,但在物理上是不连续的
代码样例:
#include <stdio.h>
void shell_sort(int arr[], int n) {
int gap, i, j, key;
for (gap = n / 2; gap > 0; gap /= 2) {
for (i = gap; i < n; i++) {
key = arr[i];
j = i;
while (j >= gap && arr[j - gap] > key) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = key;
}
}
}
int main() {
int arr[] = {23, 45, 56, 25, 36,48,96,89};
int n = sizeof(arr) / sizeof(arr[0]);
shell_sort(arr, n);
return 0;
}
性能分析:
希尔排序是一种基于插入排序的算法,通过“分而治之”的策略来提高排序效率
时间复杂度
最坏情况:希尔排序的时间复杂度依赖于增量序列的选择。传统的增量序列如{1,2,4,8,...}可能导致最坏情况下的时间复杂度为(n^2)。
最好情况:在最佳情况下,希尔排序的时间复杂度可以达到(nlog^2n),这通常发生在选择了最优增量序列时。
平均情况:平均情况下,希尔排序的时间复杂度通常认为在(n^1.3)到(n^1.5)之间,但这个值会根据增量序列的选择有所不同。
空间复杂度
希尔排序是原地排序算法,不需要额外的存储空间来进行排序(除非考虑输入输出参数和系统栈所占用的额外空间),因此其空间复杂度为(1)。这使得它在空间资源有限的环境中相当有用。
稳定性
尽管插入排序本身是稳定的排序算法,希尔排序由于其分组和跳跃性插入的特点,可能会导致相等元素的相对顺序发生变化,由于其性能表现依赖于增量序列的选择,是一种不稳定的排序算法
3.冒泡排序
冒泡排序法是一种简单的排序算法,它通过重复走访需要排序的数列,比较两个相邻元素并交换它们的位置。
算法步骤与过程:冒泡排序算法开始时会比较第一个和第二个元素,然后根据需要进行交换。随后,比较第二和第三个元素,依此类推,直到比较完最后一个元素和它的后一个元素。一轮比较后,最大的元素会被交换到序列的头序列。接着,算法会对剩下的元素重复同样的步骤,直至整个序列有序
代码样例:
#include <stdio.h>
void bubble_sort(int arr[], int n) {
int i, j;
for (i = n-1; i > 0; i--) {
for (j = 0; j < i ; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int arr[] = {23, 45, 65, 23, 48,89,75,12};
int n = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr,n);
return 0;
}
性能分析:
时间复杂度:
最坏情况:冒泡排序的时间复杂度在最坏的情况下是(n^2)。这发生在数据序列完全逆序时,因为每个元素都需要与它之前的所有元素进行比较和可能的交换。
最好情况:如果数组已经是升序排列,那么冒泡排序的最好情况时间复杂度是(n),因为每次遍历都发现不需要交换,可以立即结束排序过程。
平均情况:对于随机数据的数组,虽然冒泡排序的平均时间复杂度仍然是(n^2),但通常会比最坏情况要好一些,因为并非每次比较都需要交换元素。
空间复杂度:
冒泡排序的空间复杂度为(1),也就是说它是原地排序算法,只需要一个小量的额外存储空间用于交换元素,这使得冒泡排序在空间资源有限的环境中相当有用。
稳定性:
冒泡排序是稳定的排序算法,因为在排序过程中,相等元素的相对位置不会改变。这对于某些需要保持等值元素顺序的应用场合非常重要对于小规模数据集或基本教学场景,冒泡排序仍然是一个不错的选择。冒泡排序虽然在某些情况下(如部分有序的数据或小规模数据集)性能可接受,但由于其较高的时间复杂度,对于大规模数据的排序并不是最佳选择。
4.快速排序
快速排序法是一种高效、使用广泛的排序算法,它基于分治法和挖坑填数的思想进行数组的排序
算法步骤:
选择基准: 快速排序首先需要选择一个基准元素,这个基准通常选择第一个或最后一个元素,或者随机选择,甚至选择中值作为基准。
分区操作: 分区是快速排序的核心,它通过移动元素使得基准左边的所有元素都不大于它,而右边的所有元素都不小于它。这一过程涉及到两个指针,一个从左向右移动,另一个从右向左移动,直到两者相遇。
递归排序: 完成分区后,基准元素已经处于其最终位置,接下来对基准左边和右边的两个子数组递归执行上述排序过程,直至所有子数组都只包含单个元素,此时整个数组变得有序。
代码样例:
#include <stdio.h>
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j <= high - 1; j++) {
if (arr[j] < pivot) {
i++;
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[high]);
return i + 1;
}
void quick_sort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quick_sort(arr, low, pi - 1);
quick_sort(arr, pi + 1, high);
}
}
int main() {
int arr[] = {5, 2, 8, 12, 3};
int n = sizeof(arr) / sizeof(arr[0]);
quick_sort(arr, 0, n - 1);
return 0;
}
在这个样例中,我们定义了一个`swap`函数来交换两个整数的值。然后,我们定义了一个`partition`函数来实现分区操作,该函数接受一个整数数组、一个低索引和一个高索引作为参数,并返回一个枢轴元素的索引。接下来,我们定义了一个`quick_sort`函数来实现快速排序。该函数接受一个整数数组、一个低索引和一个高索引作为参数。在主函数中,我们声明一个整数数组并初始化它。然后,我们调用`quick_sort`函数对数组进行排序
性能分析:
时间复杂度:
最好情况:当每次划分点恰好位于中间位置时,快速排序的递归树是平衡的,此时时间复杂度为(nlogn)。这种情况通常发生在每次选取的枢轴都是数组中的中值时。
平均情况:平均情况下,快速排序的时间复杂度也被认为是(nlogn),这使其成为内排序算法中效率最高的之一。
最坏情况:如果数据已经部分有序或者选取的枢轴始终是最大或最小元素,那么快速排序的时间复杂度将退化到(n^2)。为了避免这种情况,可以通过随机化枢轴的选择来提高性能。
空间复杂度:
快速排序是递归进行的,每一层递归都需要栈来保存信息,因此其空间复杂度主要取决于递归调用的深度。在最好和平均情况下,递归的深度大约为logn,因此空间复杂度为O(logn)。但在最坏情况下,递归深度可能达到n,空间复杂度将变为(n)。
稳定性:
快速排序是不稳定的排序算法,因为在分区过程中,相等的元素可能会改变顺序。这对于某些需要保持等值元素顺序的应用场合来说是一个缺点
5.基数排序
基数排序算法是一种有效的非比较型整数排序算法,其基本原理是将整数按每个位数分别比较并分配到相应的桶中,然后根据桶的顺序来收集元素以达到排序的目的
始化状态:准备待排序的数据,确定数据的位数和最大值。
分配数据:根据每一位的数字将数据分配到对应的桶中,保证同一个桶内的数据在当前位上是相同的。
收集数据:按照桶的顺序,从小到大依次收集每个桶中的数据,形成有序的输出序列。
下一位处理:重复上述分配和收集过程,直至最高位处理完毕,此时数据已经完全有序。
代码样例:
#include <stdio.h>
#define MAX 10
void countSort(int arr[], int n, int exp) {
int output[n];
int i, count[MAX] = {0};
for (i = 0; i < n; i++)
count[(arr[i] / exp) % MAX]++;
for (i = 1; i < MAX; i++)
count[i] += count[i - 1];
for (i = n - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % MAX] - 1] = arr[i];
count[(arr[i] / exp) % MAX]--;
}
for (i = 0; i < n; i++)
arr[i] = output[i];
}
void radixsort(int arr[], int n) {
int max = arr[0];
for (int i = 1; i < n; i++)
if (arr[i] > max)
max = arr[i];
for (int exp = 1; max / exp > 0; exp *= MAX)
countSort(arr, n, exp);
}
int main() {
int arr[] = {170, 45, 75, 90, 802, 24, 2, 66};
int n = sizeof(arr) / sizeof(arr[0]);
radixsort(arr, n);
return 0;
}
样例中,我们定义了一个`countSort`函数来实现计数排序,该函数接受一个整数数组、数组的长度和一个指数作为参数。我们还定义了一个`radixsort`函数来实现基数排序,该函数接受一个整数数组和数组的长度作为参数。在主函数中,我们声明一个整数数组并初始化它。然后,我们调用`radixsort`函数对数组进行排序,并打印排序前后的数组。
性能分析:
时间复杂度:
在每一次散列中,每个元素都需要被分配到相应的桶中,即需要进行n次操作。如果最大的数有k位,那么需要进行k轮散列分配,因此时间复杂度为(n*k)。这意味着基数排序的效率与数字的位数密切相关。
空间复杂度:
基数排序需要额外的空间来存放临时的桶,这个空间的大小取决于待排序集合的大小n以及数值的范围m。因此,基数排序的空间复杂度为(n+m),这表明它需要与输入数据规模相当的额外空间。
稳定性:
由于基数排序是通过依次按每一位的数字来分配和收集元素,它不会改变相等元素的相对顺序。因此,基数排序是一种稳定的排序算法
6.外部排序
外部排序算法通常涉及处理无法完全装入内存的大规模数据集。这些算法在数据库管理、大数据分析和科学计算等领域中非常重要,因为在这些领域中经常需要对超出内存容量的数据进行排序外部排序通常采用归并排序的思想和方法。它包括将大文件分为若干小块,确保每一块都能装入内存中;然后对这些块使用内部排序算法进行排序;最后通过合并这些已排序的小块来完成整个文件的排序
代码样例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_SIZE 1000
void merge(int arr[], int l, int m, int r) {
int i, j, k;
int n1 = m - l + 1;
int n2 = r - m;
int L[n1], R[n2];
for (i = 0; i < n1; i++)
L[i] = arr[l + i];
for (j = 0; j < n2; j++)
R[j] = arr[m + 1 + j];
i = 0;
j = 0;
k = l;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
void mergeSort(int arr[], int l, int r) {
if (l < r) {
int m = l + (r - l) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}
}
void externalSort(char* inputFileName, char* outputFileName) {
FILE* inputFile = fopen(inputFileName, "r");
FILE* outputFile = fopen(outputFileName, "w");
int arr[MAX_SIZE];
int count = 0;
while (!feof(inputFile)) {
fscanf(inputFile, "%d", &arr[count]);
count++;
}
mergeSort(arr, 0, count - 1);
for (int i = 0; i < count; i++) {
fprintf(outputFile, "%d\n", arr[i]);
}
fclose(inputFile);
fclose(outputFile);
}
int main() {
char inputFileName[] = "input.txt";
char outputFileName[] = "output.txt";
externalSort(inputFileName, outputFileName);
return 0;
}
样例中,我们定义了一个`merge`函数来实现归并操作,该函数接受一个整数数组、左边界、中间值和右边界作为参数。我们还定义了一个`mergeSort`函数来实现归并排序,该函数接受一个整数数组、左边界和右边界作为参数。最后,我们定义了一个`externalSort`函数来实现外部排序,该函数接受输入文件名和输出文件名作为参数。在主函数中,我们调用`externalSort`函数对输入文件中的数据进行排序,并将排序后的结果写入输出文件中
性能分析:
外部排序是处理大规模数据集的有效方法,通常涉及处理无法完全装入内存的大规模数据集。这种排序方法主要应用于数据库和大数据领域,需要利用外存(如硬盘)来辅助排序过程。外部排序的性能取决于多个因素,包括归并段的个数、每个归并段内部的排序时间、磁盘IO读写次数以及每次归并中的比较次数。由于磁盘IO操作的时间成本远高于内部排序时间,因此优化外部排序的关键在于减少IO读写次数。
7.选择排序:
选择排序算法是一种简单直观的排序算法,其基本思想是在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后从剩余未排序元素中继续寻找最小(或最大)元素放到已排序序列的末尾,以此类推,直到所有元素均排序完毕
代码样例:
#include <stdio.h>
void selectionSort(int arr[], int n) {
int i, j, minIndex, temp;
for (i = 0; i < n - 1; i++) {
minIndex = i;
for (j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex])
minIndex = j;
}
temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
int main() {
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);
selectionSort(arr, n);
return 0;
}
性能分析:
选择排序的平均时间复杂度、最好时间复杂度和最差时间复杂度均为(N^2),其中N为数组的长度。这意味着无论数据的初始状态如何,选择排序的性能都差不多。空间复杂度方面,选择排序为(1),即只需要一个额外的存储空间来临时存放数据,是一个原地排序算法。由于选择排序可能会改变相等元素的相对顺序,因此它是一个不稳定的排序算法
8.计数排序:
计数排序算法是一种线性时间复杂度的排序算法,具有优秀的性能特点,特别是在整数范围有限时。计数排序算法是一种非比较型排序算法,它通过统计每个元素出现的次数来实现排序,适用于整数或有限范围内的非负整数排序,计数:遍历待排序的数组,统计每个元素出现的次数,并将统计结果存储在一个计数数组中。计数数组的索引对应着元素的值,而计数数组中的值表示该元素出现的次数
代码样例:
#include <stdio.h>
void countSort(int arr[], int n) {
int max = arr[0];
for (int i = 1; i < n; i++)
if (arr[i] > max)
max = arr[i];
int count[max + 1];
for (int i = 0; i <= max; ++i)
count[i] = 0;
for (int i = 0; i < n; i++)
count[arr[i]]++;
for (int i = 1; i <= max; i++)
count[i] += count[i - 1];
int output[n];
for (int i = n - 1; i >= 0; i--) {
output[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
for (int i = 0; i < n; i++)
arr[i] = output[i];
}
int main() {
int arr[] = {4, 2, 2, 8, 3, 3, 1};
int n = sizeof(arr) / sizeof(arr[0]);
countSort(arr, n);
return 0;
}
性能分析:
时间复杂度
平均、最坏和最佳情况:计数排序的时间复杂度在所有情况下均为(n + k),其中n是待排序数组的大小,k是整数范围(即数组中最大元素与最小元素的差值加一)
空间复杂度
辅助存储需求:计数排序的空间复杂度同样为(n + k),因为除了输入输出数组外,还需要一个大小为k的计数数组来记录每个元素出现的次数。
稳定性
元素相对顺序:计数排序是一个稳定的排序算法,因为它在排序过程中不会改变具有相同值的元素之间的相对顺序。
。