目录
前言
一、冒泡排序
二、选择排序
三、插入排序
四、希尔排序
五、归并排序
六、快速排序
七、 堆排序
八、计数排序
九、桶排序
十、基数排序
前言
排序算法可以大致分为两大类:比较类排序和非比较类排序。以下是这两大类中一些常见的排序算法示例:
比较类排序(非线性时间比较类排序)
-
冒泡排序(Bubble Sort):通过重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
-
选择排序(Selection Sort):首先在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
-
插入排序(Insertion Sort):将数组分为已排序和未排序两部分,初始时,已排序部分只包含一个元素,其余的元素都在未排序部分。然后,每次从未排序部分取出第一个元素,在已排序部分找到相应的位置并插入,直到所有元素均排序完毕。
-
希尔排序(Shell Sort):是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。
-
归并排序(Merge Sort):将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
-
堆排序(Heap Sort):是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。
-
快速排序(Quick Sort):通过一次排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
-
计数排序(Counting Sort)(虽然它通常被认为是非比较类排序,但在某些情况下,如当输入数据范围较小时,它可以作为比较类排序的替代):不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
非比较类排序(线性时间非比较类排序)
-
基数排序(Radix Sort):按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
-
桶排序(Bucket Sort):是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:首先,要使得数据分散得尽可能均匀;其次,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
-
计数排序(Counting Sort)(如上面提到的,虽然有时作为比较类排序的替代,但在更严格的分类中它属于非比较类排序):适用于待排序的值域不大且可以预估的情况。它的基本原理是将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
请注意,计数排序在上述分类中被提及了两次,因为它既可以作为比较类排序(当输入数据范围较大时),也可以作为非比较类排序(当输入数据范围较小时)。但在更严格的分类中,它通常被视为非比较类排序。
他们各自算法复杂度如下:
算法其中相关概念说明:
稳定与不稳定
在排序算法中,稳定性是一个重要的属性,它描述了在排序过程中具有相同值的元素之间的相对位置是否保持不变。
-
稳定:如果待排序的序列中存在两个或两个以上关键字相等的元素,排序后这些元素的相对次序保持不变,则称该排序方法是稳定的。换句话说,如果a原本在b的前面,并且a和b的值相等(即a=b),那么经过稳定的排序算法处理后,a仍然会在b的前面。
-
不稳定:如果待排序的序列中存在两个或两个以上关键字相等的元素,经过排序后,这些元素的相对次序发生变化,则称该排序方法是不稳定的。也就是说,如果a原本在b的前面,并且a和b的值相等(即a=b),那么经过不稳定的排序算法处理后,a可能会出现在b的后面。
时间复杂度
时间复杂度是评估算法执行效率的一个重要指标,它描述的是当数据规模增大时,算法所需时间的增长趋势。通常用O()表示,并基于算法中基本操作(如比较、交换等)的执行次数来估算。
- 常见的时间复杂度:
- O(1):常数时间复杂度,表示算法的执行时间不受数据规模n的影响。
- O(logn):对数时间复杂度,表示算法的执行时间与log(n)成正比。
- O(n):线性时间复杂度,表示算法的执行时间与数据规模n成正比。
- O(n^2):平方时间复杂度,表示算法的执行时间与n的平方成正比。
- O(nlogn):介于线性与平方之间的一种时间复杂度,常见于一些高效的排序算法。
空间复杂度
空间复杂度是评估算法所需额外存储空间的一个指标,它描述的是算法在计算机内执行时所需存储空间的度量,并通常以数据规模n的函数形式来表示。
- 常见的空间复杂度:
- O(1):常数空间复杂度,表示算法所需额外存储空间不随数据规模n的变化而变化。
- O(n):线性空间复杂度,表示算法所需额外存储空间与数据规模n成正比。
- O(n^2):平方空间复杂度,表示算法所需额外存储空间与n的平方成正比。
- 空间复杂度还可能涉及对数空间、指数空间等,但在实际应用中相对较少见。
需要注意的是,空间复杂度和时间复杂度在评估算法时都很重要,但在实际应用中往往需要根据具体问题和需求来权衡两者的关系。
一、冒泡排序
冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误(即前一个元素比后一个元素大,在升序排序中)就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
这个算法的名字“冒泡排序”非常形象地描述了算法的过程:越小的元素会经由交换慢慢“浮”到数列的顶端(或越大的元素“沉”到底部,这取决于排序是升序还是降序)。
1.1 算法描述
1)比较相邻的元素。如果第一个比第二个大,就交换它们两个;
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
3)针对所有的元素重复以上的步骤,除了最后一个;
4)重复步骤1~3,直到排序完成。
1.2 动图演示
1.3 C语言代码实现
#include <stdio.h>// 冒泡排序函数
void bubbleSort(int arr[], int n) {int i, j, temp;for (i = 0; i < n - 1; i++) { // 外层循环控制需要排序的轮数for (j = 0; j < n - i - 1; j++) { // 内层循环控制每一轮的比较次数if (arr[j] > arr[j + 1]) { // 如果前一个元素大于后一个元素,则交换temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}
}// 打印数组
void printArray(int arr[], int size) {int i;for (i = 0; i < size; i++)printf("%d ", arr[i]);printf("\n");
}int main() {int arr[] = {64, 34, 25, 12, 22, 11, 90};int n = sizeof(arr) / sizeof(arr[0]);printf("原始数组: ");printArray(arr, n);bubbleSort(arr, n);printf("排序后的数组: ");printArray(arr, n);return 0;
}
二、选择排序
2.1 算法描述
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理如下:
- 从未排序的序列中找出最小(或最大)的元素,存放到排序序列的起始位置。
- 从剩余未排序的元素中继续寻找最小(或最大)的元素,然后将其放到已排序序列的末尾。
- 重复步骤2,直到所有元素都排序完毕。
这种排序方法通过不断选择剩余未排序元素中的最小(或最大)元素,逐步构建有序序列,直到整个序列有序。
2.2 动图演示
2.3 代码演示
#include <stdio.h>// 选择排序函数
void selectionSort(int arr[], int n) {int i, j, min_idx;for (i = 0; i < n - 1; i++) {// 假设当前位置是最小值min_idx = i;for (j = i + 1; j < n; j++) {// 如果找到更小的值,则更新最小值的索引if (arr[j] < arr[min_idx]) {min_idx = j;}}// 将找到的最小值与当前位置的值交换if (min_idx != i) {int temp = arr[min_idx];arr[min_idx] = arr[i];arr[i] = temp;}}
}// 打印数组
void printArray(int arr[], int size) {int i;for (i = 0; i < size; i++)printf("%d ", arr[i]);printf("\n");
}int main() {int arr[] = {64, 34, 25, 12, 22, 11, 90};int n = sizeof(arr) / sizeof(arr[0]);printf("原始数组: ");printArray(arr, n);selectionSort(arr, n);printf("排序后的数组: ");printArray(arr, n);return 0;
}
选择排序(Selection Sort)是一种简单直观的排序算法,其稳定性表现在其性能不受输入数据分布的影响,始终保持O(n^2)的时间复杂度。这意味着无论输入数据是已经排序、逆序还是随机分布,选择排序都需要进行相同数量的比较和交换操作。因此,在实际应用中,选择排序更适合处理小规模数据集,因为随着数据规模的增大,其效率会显著下降。
选择排序的主要优点是它的空间复杂度为O(1),即它不需要额外的存储空间来存储临时数据,所有的操作都在原始数组上进行,这使得它在内存受限的环境中成为一个可行的选择。
从理论上讲,选择排序因其简单性,可能是非专业人士在需要手动排序时最容易想到的方法之一。它的基本思想是:在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(或最大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
总结来说,选择排序的稳定性和简单性是其主要特点,尽管它在处理大规模数据时效率不高,但在小规模数据或内存受限的情况下,它是一个实用的选择。
三、插入排序
插入排序(Insertion-Sort)的算法描述简洁明了,它是一种简单直观的排序方法。其工作原理基于构建有序序列的思想,对于每一个未排序的元素,它会在已排序的序列中从后向前进行扫描,直到找到该元素应该插入的位置,并将其插入。这个过程会持续进行,直到所有的元素都被插入到有序序列中,从而实现整个数组的排序。
3.1 算法描述
插入排序是一种通常采用in-place方式在数组上实现的排序算法。以下是其具体算法描述:
1)首先,我们假设数组的第一个元素是已排序的。
2)接着,我们取出数组中的下一个元素,并开始在已排序的元素序列中从后向前进行扫描。
3)在扫描过程中,如果当前已排序的元素大于新取出的元素,我们就将该已排序的元素向后移动一个位置,以便为新元素腾出空间。
4)我们持续重复上述步骤,直到找到已排序的元素序列中小于或等于新元素的位置。
5)一旦找到这样的位置,我们就将新元素插入到该位置之后。
然后,我们继续取出数组中的下一个元素,并重复上述步骤2到步骤5,直到数组中的所有元素都完成排序。
3.2 动图演示
3.3 代码演示
#include <stdio.h>// 插入排序函数
void insertionSort(int arr[], int n) {int i, key, j;for (i = 1; i < n; i++) {key = arr[i]; // 将当前元素设为keyj = i - 1;// 将大于key的元素向后移动while (j >= 0 && arr[j] > key) {arr[j + 1] = arr[j];j = j - 1;}arr[j + 1] = key; // 插入key到正确的位置}
}// 打印数组
void printArray(int arr[], int size) {int i;for (i = 0; i < size; i++)printf("%d ", arr[i]);printf("\n");
}int main() {int arr[] = {12, 11, 13, 5, 6};int n = sizeof(arr) / sizeof(arr[0]);printf("原始数组: ");printArray(arr, n);insertionSort(arr, n);printf("排序后的数组: ");printArray(arr, n);return 0;
}
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
四、希尔排序
希尔排序(Shell Sort)是由Donald Shell在1959年提出的一种排序算法,它是简单插入排序的一种改进版,也是第一个在平均时间复杂度上突破O(n^2)的排序算法。与插入排序的主要区别在于,希尔排序在比较元素时不是逐个地比较,而是采用增量序列,使每个子序列尽可能接近有序,然后逐渐缩小增量,直至增量为1,此时整个序列已经基本有序,再进行一次直接插入排序即可。由于这种排序算法在比较时优先比较距离较远的元素,因此有时也被称为缩小增量排序。
4.1 算法描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.2 动图演示
4.3 代码演示
#include <stdio.h>// 交换两个元素
void swap(int* a, int* b) {int t = *a;*a = *b;*b = t;
}// 希尔排序
void shellSort(int arr[], int n) {int gap, i, j, temp;for (gap = n / 2; gap > 0; gap /= 2) { // 增量序列可以是 n/2, n/4, ... , 1for (i = gap; i < n; i++) { // 对每个子序列进行插入排序temp = arr[i];for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {arr[j] = arr[j - gap];}arr[j] = temp;}}
}// 打印数组
void printArray(int arr[], int size) {int i;for (i = 0; i < size; i++)printf("%d ", arr[i]);printf("\n");
}int main() {int arr[] = {12, 34, 54, 2, 3};int n = sizeof(arr) / sizeof(arr[0]);printf("原始数组: ");printArray(arr, n);shellSort(arr, n);printf("排序后的数组: ");printArray(arr, n);return 0;
}
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。
五、归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
5.1 算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
5.2 动图演示
5.3 代码实现
#include <stdio.h>// 合并两个已排序的子数组
void merge(int arr[], int left, int mid, int right) {int n1 = mid - left + 1;int n2 = right - mid;/* 创建临时数组 */int L[n1], R[n2];/* 拷贝数据到临时数组 L[] 和 R[] */for (int i = 0; i < n1; i++)L[i] = arr[left + i];for (int j = 0; j < n2; j++)R[j] = arr[mid + 1 + j];/* 合并临时数组到 arr[left..right] */int 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[] 的剩余元素到 arr */while (i < n1) {arr[k] = L[i];i++;k++;}/* 拷贝 R[] 的剩余元素到 arr */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);}
}// 测试归并排序
int main() {int arr[] = {12, 11, 13, 5, 6, 7};int arr_size = sizeof(arr) / sizeof(arr[0]);printf("原始数组: \n");for (int i = 0; i < arr_size; i++)printf("%d ", arr[i]);printf("\n");// 调用归并排序函数mergeSort(arr, 0, arr_size - 1);printf("排序后的数组: \n");for (int i = 0; i < arr_size; i++)printf("%d ", arr[i]);printf("\n");return 0;
}
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
六、快速排序
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
6.1 算法描述:
快速排序是一种基于分治策略的排序算法,它通过将待排序的列表划分为两个子列表来实现。算法的主要步骤如下:
- 选取一个元素作为“基准”(pivot)。
- 通过一趟排序,将待排序的列表划分为两部分:所有比基准小的元素都移到基准的左边,所有比基准大的元素都移到基准的右边(与基准相等的元素可以放在任一边)。这个过程称为“分区”(partition)操作,完成后基准元素处于其最终排序位置。
- 递归地对基准左边和右边的两个子列表进行快速排序。
6.2 动图演示
6.3 代码实现
#include <stdio.h>// 交换数组中两个元素的位置
void swap(int* a, int* b) {int t = *a;*a = *b;*b = t;
}// 快速排序的分区函数
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 quickSort(int arr[], int low, int high) {if (low < high) {/* pi 是分区后基准元素的索引 */int pi = partition(arr, low, high);// 分别对基准元素左右两侧的子数组进行递归排序quickSort(arr, low, pi - 1);quickSort(arr, pi + 1, high);}
}// 测试快速排序函数
int main() {int arr[] = {10, 7, 8, 9, 1, 5};int n = sizeof(arr) / sizeof(arr[0]);quickSort(arr, 0, n - 1);printf("Sorted array: \n");for (int i = 0; i < n; i++)printf("%d ", arr[i]);printf("\n");return 0;
}
七、 堆排序
堆排序(Heapsort)是一种基于堆数据结构的排序算法。堆是一种近似完全二叉树的结构,其特点在于它满足堆的性质:对于任意节点,其值总是大于或等于(最大堆)或小于或等于(最小堆)其子节点的值。通过构建这种特殊的二叉树,并在排序过程中保持堆的性质,堆排序能高效地实现数组的排序。
7.1 算法描述
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
7.2 动图演示
7.3 代码实现
#include <stdio.h>// 交换两个元素
void swap(int* a, int* b) {int t = *a;*a = *b;*b = t;
}// 调整堆
void heapify(int arr[], int n, int i) {int largest = i; // 初始化largest为根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 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--) {// 将当前最大的元素arr[0]和arr[i]交换swap(&arr[0], &arr[i]);// 重新调整剩余未排序元素为最大堆heapify(arr, i, 0);}
}// 测试堆排序
int main() {int arr[] = {12, 11, 13, 5, 6, 7};int n = sizeof(arr) / sizeof(arr[0]);heapSort(arr, n);printf("Sorted array is \n");for (int i = 0; i < n; ++i)printf("%d ", arr[i]);printf("\n");return 0;
}
八、计数排序
计数排序(Counting Sort)是一种非基于比较的排序算法,它通过统计输入数据集中每个元素出现的次数,然后按照这些次数将元素放到输出序列的正确位置上。其核心思想是将输入的数据值转化为键(通常是整数),并将这些键作为索引来在额外开辟的数组空间中存储计数值。最后,根据这些计数值来确定原数组中每个元素在排序后数组中的位置。
计数排序是一种线性时间复杂度的排序算法(在数据值范围不是很大的情况下),它的时间复杂度为O(n+k),其中n是待排序数组的长度,k是输入数据的范围大小。然而,计数排序要求输入的数据必须是有确定范围的整数,并且该范围不宜过大,否则将消耗大量的额外空间。
在实际应用中,如果数据范围很大或者数据不是整数,计数排序可能不是最佳选择。但对于一些具有固定范围的小整数数组,计数排序可以非常高效地完成任务。
8.1 算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
8.2 动图演示
8.3 代码演示
#include <stdio.h>
#include <stdlib.h>void countingSort(int arr[], int n, int max) {int *count = (int *)malloc((max + 1) * sizeof(int));int *output = (int *)malloc(n * sizeof(int));// 初始化计数数组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];}// 构建输出数组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];}// 释放动态分配的内存free(count);free(output);
}int main() {int arr[] = {4, 2, 2, 8, 3, 3, 1};int n = sizeof(arr) / sizeof(arr[0]);int max = 8; // 假设已知最大值为8countingSort(arr, n, max);printf("Sorted array: \n");for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");return 0;
}
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
九、桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
9.1 算法描述
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
9.2 动图演示
9.3 代码实现
#include <stdio.h>
#include <stdlib.h>// 插入排序,用于桶内排序
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 = j - 1;}arr[j + 1] = key;}
}// 桶排序
void bucketSort(int arr[], int n) {// 找到数组中的最大值和最小值int minValue = arr[0];int maxValue = arr[0];for (int i = 1; i < n; i++) {if (arr[i] < minValue)minValue = arr[i];if (arr[i] > maxValue)maxValue = arr[i];}// 桶的个数int bucketSize = (maxValue - minValue) / n + 1;if (bucketSize == 0)bucketSize = 1;// 创建桶int bucketCount = (maxValue - minValue) / bucketSize + 1;int *buckets[bucketCount];for (int i = 0; i < bucketCount; i++) {buckets[i] = (int *)malloc(sizeof(int) * n);buckets[i][0] = 0; // 桶内元素计数器}// 将数据放到各个桶中for (int i = 0; i < n; i++) {int index = (arr[i] - minValue) / bucketSize;buckets[index][buckets[index][0]++] = arr[i];}// 对每个桶进行排序for (int i = 0; i < bucketCount; i++) {insertionSort(buckets[i] + 1, buckets[i][0]);}// 将桶中的数据放回原数组int index = 0;for (int i = 0; i < bucketCount; i++) {for (int j = 1; j <= buckets[i][0]; j++) {arr[index++] = buckets[i][j];}free(buckets[i]); // 释放桶的内存}
}// 测试桶排序
int main() {int arr[] = {0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68};int n = sizeof(arr) / sizeof(arr[0]);bucketSort(arr, n);printf("Sorted array is \n");for (int i = 0; i < n; i++)printf("%.2f ", arr[i]);printf("\n");return 0;
}// 注意:由于示例代码中的arr是浮点数,但桶排序更适合整数,所以这里假设我们实际上是对整数进行排序
// 如果要排序浮点数,需要更复杂的策略来确定桶的大小和数量
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
十、基数排序
基数排序是一种分步进行的排序算法,它首先根据数据的最低有效位进行排序,然后进行收集;接着根据次低有效位进行排序,再次收集;这一过程持续进行,直到处理完最高有效位。在某些情况下,数据的属性具有不同的优先级,基数排序会先按照较低优先级的属性进行排序,然后按照较高优先级的属性进行排序。最终的排序结果是,高优先级的数据项排在前面,如果高优先级相同,则低优先级较高的数据项排在前面。
10.1 算法描述
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
10.2 动图演示
10.3 代码实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 获取数组中最大数的位数
int getMaxDigit(int arr[], int n) {int maxVal = arr[0];for (int i = 1; i < n; i++) {if (arr[i] > maxVal) {maxVal = arr[i];}}int digit = 0;while (maxVal) {maxVal /= 10;digit++;}return digit;
}// 基数排序函数
void radixSort(int arr[], int n) {int maxDigit = getMaxDigit(arr, n);int *output = (int *)malloc(n * sizeof(int));// 从最低位到最高位进行排序for (int exp = 1; exp <= maxDigit; exp *= 10) {int outputIndex = 0; // 用于指向output数组的索引// 使用计数排序按当前位数排序int counts[10] = {0}; // 10个桶for (int i = 0; i < n; i++) {int digit = (arr[i] / exp) % 10; // 获取当前位的数字counts[digit]++; // 计数}// 修改计数数组,以便将元素放入正确的位置for (int i = 1; i < 10; i++) {counts[i] += counts[i - 1];}// 构建输出数组for (int i = n - 1; i >= 0; i--) {int digit = (arr[i] / exp) % 10;output[counts[digit] - 1] = arr[i];counts[digit]--;}// 复制排序后的数组回原数组for (int i = 0; i < n; i++) {arr[i] = output[i];}}free(output); // 释放临时数组
}// 测试基数排序
int main() {int arr[] = {170, 45, 75, 90, 802, 24, 2, 66};int n = sizeof(arr) / sizeof(arr[0]);radixSort(arr, n);printf("Sorted array: \n");for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");return 0;
}
基数排序通过依次对数据的不同位进行排序和收集,确保了排序的稳定性。尽管如此,与桶排序相比,基数排序的效率稍低。每次对关键字进行桶分配需要O(n)的时间,而分配后形成新的关键字序列同样需要O(n)的时间。如果数据可以被划分为d个关键字,那么基数排序的总时间复杂度大约是O(d*2n),尽管d通常远小于n,这使得基数排序的时间复杂度接近线性。
在空间使用方面,基数排序需要O(n+k)的额外空间,其中k代表桶的数目。由于通常情况下n远大于k,因此实际所需的额外空间大约为n个单位。