目录
一、什么是线性时间非比较类排序
(1)排序的分类
按照排序排序时间复杂度是否与时间呈线性关系,以及在排序过程中是否使用比较的方法来区分排序种类,由此可知我们往期学的七大排序都是非线性时间比较类排序。
(2)有关线性时间非比较类排序
线性时间非比较排序是指一类排序算法,其时间复杂度为O(n),且不依赖于比较操作来确定元素的顺序。这种排序算法通常利用了元素的特定性质或者分布情况来实现排序,而非直接比较元素之间的大小。
我们接下来要介绍的计数排序、桶排序、基数排序都属于这个类别
(3)疑问
<1>是否存在非线性时间非比较类排序?
非线性时间复杂度且非比较的排序算法并不常见,因为排序的本质是通过比较来确定元素的顺序。比较排序的下界(最坏情况下的时间复杂度下限)为O(n log n),这意味着任何基于比较的排序算法在最坏情况下都至少需要这么多比较操作。
然而,在特定的约束条件下,有一些算法可以被视为非比较排序,尽管它们通常不被称为非比较排序。例如,计数排序、桶排序和基数排序虽然可以达到线性时间复杂度(O(n)),但它们的实现仍然依赖于对数据的某种形式的比较或者类似的操作来确定元素的顺序。这些算法利用了元素的分布特征或位数特征,而不是直接比较元素的值。
绝大多数排序算法要么是基于比较的(时间复杂度至少为O(n log n)),要么依赖于某种形式的“比较”来进行排序操作。因此,在传统意义上,非线性时间复杂度且非比较的排序算法并不普遍存在。
<2>是否存在线性时间比较类排序?
在传统的计算模型下,不存在能够在最坏情况下以线性时间复杂度完成的比较排序算法。这是因为比较排序算法的下界(最低可能的时间复杂度)为O(n log n),这一结论由计算理论中的决策树模型和信息论的基本原理所确定。
具体来说,决策树模型表明,对于任何基于比较的排序算法,其最坏情况下的比较次数至少为O(n log n)。这是由于在排序过程中,需要根据元素之间的大小关系进行比较来确定其顺序,而比较操作的数量决定了排序算法的时间复杂度下界。
因此,无论如何设计比较排序算法,都不可能在最坏情况下以线性时间复杂度(O(n))完成排序。任何声称实现了线性时间复杂度的比较排序算法,都会违背这一理论限制。
二、计数排序(Count Sort)
(1)原理
首先确定待排序数组中元素的范围,假设待排序的数组中的元素都属于一个已知范围。
创建一个计数数组,长度为k+1,用来记录待排序数组中每个元素出现的次数。计数数组的索引代表待排序元素的值,数组中的值表示该元素出现的次数。
统计计数:遍历待排序数组,统计每个元素出现的次数,存储在计数数组中对应的位置。
对计数数组进行累加操作,即将每个位置的值与前一个位置的值相加。这一步的目的是确定每个元素在排序后的数组中的起始位置。
创建一个与待排序数组相同大小的临时数组,遍历待排序数组,将每个元素根据其在计数数组中的累加值,放置到临时数组中的正确位置。 将临时数组中的数据复制回原始的待排序数组中,完成排序。
(2)代码实现
// 计数排序
// 时间:O(N+range)
// 空间:O(range)
void CountSort(int* a, int n){
int min = a[0], max = a[0];
for (int i = 1; i < n; i++){
if (a[i] < min)
min = a[i];
if (a[i] > max)
max = a[i];
}
int range = max - min + 1;
int* count = (int*)calloc(range, sizeof(int));//使用calloc的目的是将这个数组中的数据初始化为0
if (count == NULL){
printf("calloc fail\n");
return;
}
// 统计次数
for (int i = 0; i < n; i++){
count[a[i] - min]++; //相对映射
}
// 排序
int i = 0;
for (int j = 0; j < range; j++){
while (count[j]--){
a[i++] = j + min;
}
}
}
在代码实现中我们还使用了相对映射的思想,即将一个范围内的数据段映射到以0为开头的数据段上,因此我们需要找到目标数据段中的最小值。
(3)分析
<1>时间复杂度分析
计数排序的时间复杂度为O(n + k),其中n是待排序数组的元素个数,k是待排序数组中的元素范围。它的效率很高,特别适合对范围不大的整数进行排序,但是当范围k较大时,计数排序的空间复杂度可能会比较高。
<2>适用场景
(1)排序对象是整数(只有整数我们才可以用数组的下标表明指向数据)。
(2)排序范围已知,且范围不大。
三、桶排序(Bucket Sort)
(1)原理
桶的存在相当于一个区域划分,类似于在数字之间分区,将同一个范围内的数据划分到同一区域,以达到预排序的作用。
桶排序是一种非比较性的排序算法,适用于元素均匀分布在一个范围内的情况。它的基本思想是将待排序数组分到有限数量的桶中,对每个桶中的元素进行排序,然后按照顺序把各个桶中的元素依次取出来,即可得到排序好的结果。
确定桶的数量:首先确定桶的数量,以及每个桶所能容纳的元素范围。桶的数量可以根据待排序数组的特点来确定,一般来说,桶的数量与待排序元素的数量成正比。
分配元素到桶中:将待排序数组中的每个元素放入对应的桶中。具体的放置规则可以根据元素值和桶的范围来决定,例如可以采用哈希函数将元素映射到特定的桶中。
对每个桶中的元素进行排序:对每个非空的桶中的元素进行排序。通常可以使用插入排序、快速排序等方法对桶内的元素进行排序。
合并桶:将各个桶中排序好的元素依次取出,组成最终的排序结果。
(2)代码实现
<1>对于浮点数进行桶排序
#define numBuckets 10
// 定义桶的数据结构,包含一个动态数组和数组大小
struct Bucket {
int count;
int* values;
};
// 桶排序函数
void bucketSort(float arr[], int n) {
// 创建桶数组,这里假设桶的数量为10
struct Bucket buckets[numBuckets];
// 初始化每个桶的大小为0
for (int i = 0; i < numBuckets; ++i) {
buckets[i].count = 0;
buckets[i].values = (int*)malloc(sizeof(int) * n);
}
// 将元素分配到桶中
for (int i = 0; i < n; ++i) {
int bucketIndex = arr[i] * numBuckets;
buckets[bucketIndex].values[buckets[bucketIndex].count++] = arr[i];
}
// 对每个桶中的元素进行排序,这里简单使用插入排序
for (int i = 0; i < numBuckets; ++i) {
int bucketSize = buckets[i].count;
// 使用插入排序对桶中的元素进行排序
for (int j = 1; j < bucketSize; ++j) {
float key = buckets[i].values[j];
int k = j - 1;
while (k >= 0 && buckets[i].values[k] > key) {
buckets[i].values[k + 1] = buckets[i].values[k];
k--;
}
buckets[i].values[k + 1] = key;
}
}
// 将排序后的元素依次放回原数组中
int index = 0;
for (int i = 0; i < numBuckets; ++i) {
for (int j = 0; j < buckets[i].count; ++j) {
arr[index++] = buckets[i].values[j];
}
free(buckets[i].values); // 释放每个桶的动态数组
}
}
int main(void) {
return 0;
}
<2>对于整数类型进行桶排序
#define Max_len 10 //数组元素个数
// 打印结果
void Show(int arr[], int n){
int i;
for (i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
}
//获得未排序数组中最大的一个元素值
int GetMaxVal(int* arr, int len){
int maxVal = arr[0]; //假设最大为arr[0]
for (int i = 1; i < len; i++) {
if (arr[i] > maxVal)
maxVal = arr[i];
}
return maxVal; //返回最大值
}
//桶排序 参数:数组及其长度
void BucketSort(int* arr, int len){
int tmpArrLen = GetMaxVal(arr, len) + 1;
int tmpArr[10]; //获得空桶大小
int i, j;
for (i = 0; i < tmpArrLen; i++) //空桶初始化
tmpArr[i] = 0;
for (i = 0; i < len; i++) //寻访序列,并且把项目一个一个放到对应的桶子去。
tmpArr[arr[i]]++;
for (i = 0, j = 0; i < tmpArrLen; i++){
while (tmpArr[i] != 0) {
arr[j] = i; //从不是空的桶子里把项目再放回原来的序列中。
j++;
tmpArr[i]--;
}
}
}
这里我们用宏定义桶的个数,用结构体定义桶,使用结构体数组整合所有的桶,依据数组中数据大小将数组中的数据划分到相应的桶里面,就相当于进行了一次预排序。
然后再在桶内进行内排序,我使用插入排序方法。
(3)分析
<1>时间复杂度
由于在将原数组装入桶内后,桶内待排序数组的范围k值较小,因此桶内时间复杂度相对于桶外装载过程可以忽略不计。
桶排序的时间复杂度取决于桶的数量和每个桶内部的排序算法。如果每个桶内的排序算法具有较高的效率(如O(n log n)级别的快速排序),则桶排序的时间复杂度接近O(n)。但在实际中,桶的数量和每个桶内元素的分布情况对算法的性能影响较大。
<2>适用场景
(1)待排序数据的范围分布比较均匀。
(2)待排序数据的大小不是特别大,可以容易地放入内存中的多个桶中。
(3)可以用稳定的内部排序算法对桶内的元素进行排序。
四、基数排序(Radix Sort)
(1)原理
首先,找到待排序数组中的最大值,以确定需要进行多少轮排序。最大数的位数决定了需要进行排序的轮数。从最低有效位(个位)开始,
依次对每一位上的数字进行稳定的计数排序(或桶排序)。对每一位数字(个位、十位、百位等)进行计数排序。例如,对于当前位数 exp
,从右向左遍历数组,按照 (arr[i] / exp) % 10
的值将数字分配到对应的桶中桶内元素的顺序是先进先出的,因此通过计数数组可以确定每个元素在输出数组中的位置。对每一位数进行排序后,将排序好的数组作为下一轮排序的输入,直到对最高位数(数值最大的位数)进行排序完成。经过上述步骤,数组已经完成了按照每一位数排序的过程。最终,数组中的元素按照从小到大的顺序排列完成。
(2)代码实现
int getMax(int arr[], int n) {
int max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
// Function to perform counting sort based on digit represented by exp
void countSort(int arr[], int n, int exp) {
int output[10]; // Output array that will have sorted numbers
int i, count[10] = { 0 };
// Store count of occurrences in count[]
for (i = 0; i < n; i++) {
count[(arr[i] / exp) % 10]++;
}
// Change count[i] so that count[i] now contains actual position of this digit in output[]
for (i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// Build the output array
for (i = n - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
// Copy the output array to arr[], so that arr[] now contains sorted numbers according to current digit
for (i = 0; i < n; i++) {
arr[i] = output[i];
}
}
// Radix Sort function
void radixSort(int arr[], int n) {
// Find the maximum number to know number of digits
int max = getMax(arr, n);
// Do counting sort for every digit. exp is 10^i where i is the current digit number
for (int exp = 1; max / exp > 0; exp *= 10) {
countSort(arr, n, exp);
}
}
// Function to print an array
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
(3)分析
-
时间复杂度:基数排序的时间复杂度为O(d * (n + k)),其中d是位数,n是元素个数,k是每个位数可能的取值范围。因为我们使用的是固定的位数(根据最大数确定),所以时间复杂度可以看作O(n)。
-
空间复杂度:基数排序的空间复杂度为O(n + k),其中n是元素个数,k是每个位数可能的取值范围(这里是0-9)。