1、计数排序
计数排序 需要满足 n 个输入元素中的每一个元素都在 0 到 k 的区间内的整数。
计数排序的思想是:对每一个输入元素x,确定小于 x 的元素个数。利用这一信息,可以直接把 x 放到它在输出数组中的位置上。例如,有17个元素小于 x,则 x 就应该在第18个位置上。
计数排序 需要使用 k+n个辅助空间,k 个辅助空间是用来确定小于 x 的元素个数,n 个辅助空间是用来存放对原数组的排序输出。我们把 k 个辅助空间数组假设为 C, n个辅助空间数组假设为 B,需要排序的元素组假设为 A,具体代码如下:
//arrs 是需要排序的数组,inum是数组的个数,ik 是 输入元素的范围
Counting_Sort(int A[], int inum,int ik)
{
//1、首先对C数组进行初始化
int C[ik];
for(int i = 0; i<ik; i ++)
C[i] = 0;
//2、遍历A数组,会记录每个元素出现的次数,并放到C数组中
for(int i = 0; i< inum; i++)
C[A[i]] ++;
//3、计算小于等于某一个值的元素个数
for( int i = 1; i<ik; i++)
C[i] = C[i] + C[i-1]; //这个可以用 STL partial_sum 算法
//4、确定A数组中元素应该在的位置,主要需要从后往前遍历
for( int i = inum; i > 0; i--)
{
int index = C[A[i]]; //小于等于A[i] 元素的个数
B[index] = A[i]; //把A[i] 元素放入应该在的地方
C[A[i]] = --index; //因为减少了元素,对应放置的位置往前一位
}
}
如果光看代码可能有点绕,这是一个动态的图,方便理解:
2、桶排序
桶排序 的原理是将数组分到有限数量的桶子里,每个桶在进行单独排序(有可能在使用别的排序算法),最后把排序之后的数据串联起来。当要被排序的数组内的数值是均匀分配的话,桶排序的时间复杂度为 O(n),当然这是最理想的情况。要进行桶排序的数据必须介于 0~k 之间 或者 (0,1)的浮点数也可。
桶排序的过程可以描述成以下几个步骤:
1、根据数据大小分配合适的桶子个数 M,每个桶子的数据限定在一定范围内。
2、遍历将要排序的数据 N,将每个元素按照规定的范围分布到各个桶中。
3、对每个桶子中的元素进行排序,排序算法可选择其他算法
4、依次从每个桶中取出元素,按顺序放入到最初的输出序列中。
桶排序的时间复杂度,还跟每个桶选择的算法有关,假设有 M 个桶,每个桶的元素为 n/m
当桶中使用冒泡排序时,总的时间复杂度为 O(n)+mO((n/m)2).
当桶中使用快速排序时,总的复杂度为 O(n) + mO( n/m log(n/m));
当桶的个数越多,执行的效率越快,但是桶越多,空间消耗就越大,是一种通过空间换时间的方式。
桶排序的伪代码如下:
Bucket_sort( A )
{
int n = A.length
//1、创建桶,并初始化
let B[0... n-1] be a new array
for i = 0 to n-1
make B[i] an empty list
//2、遍历排序数组,并把对应元素放到对应桶中
for i = 1 to n
insert A[i] into list B[LnA[i]]
//3、对每个桶进行排序
for i = 0 to n -1
sort list B[i] with sort
//4、把数据从每个桶中提取出来
concatenate the lists B[0],B[1],B[2]...... together in order
}
3、基数排序
基数排序 其原理:是将整数按位数切割成不同的数字,然后按每个位数分别排序。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
从直观上看,会觉得应该按照 最高有效位进行排序(MSD),但是对于数字的排序是从最低有效位进行排序(LSD),即先对最次位关键字进行排序,再对高一位的关键字进行排序,以此类推。
代码如下:
//此函数的目的是取得数据每个位上的数值
//i为待取的数据
int getDigit(int i, int d) //d的值为1、2、3...,表示要求取的相应位的值,1表示求取个位,
{ //2表示十分位,类推
int val;
while (d--)
{
val = i % 10;
i /= 10;
}
return val;
}
//基数排序算法的具体实现
void Radix_Sort(int *list, int begin, int end, int digit)
{
int radix = 10; //基数
int i = 0, j = 0;
int * count = new int[radix]; //存放当前元素的个数数组,使用了计数排序的技巧
int * pB = new int[end - begin + 1];
//1、按照最多的位数,来进行遍历排序,如果位数不够,高位补0
for (int d = 1; d <= digit; d++)
{
for ( i = 0; i < radix; i++)
count[i] = 0; //置空辅助数组
for (i = begin; i <= end; i++)
{
j = getDigit(list[i], d);
count[j]++;
}
for (i = 1; i < radix; i++)
count[i] = count[i] + count[i - 1]; //计算小于等于当前元素的个数
//将数据依次装入桶中,保证数据的稳定性,此步即为基数排序的分配
for (i = end; i >= begin; i--)
{
j = getDigit(list[i], d);
pB[count[j] - 1] = list[i];
count[j]--;
}
//基数排序的收集
//把辅助数组的数据再倒出来
for (i = begin, j = 0; i <= end; i++, j++)
list[i] = pB[j];
}
}
便于理解,下面是是动态图片:
基数排序是否比快速排序更好呢?通常基数排序的时间复杂度为O(n),而快速排序则需要O(nlgn),从结果上看,基数排序更好一些。但是基数排序虽然循环的轮数比快速排序少,但每一轮所耗费的时间要长的多。哪一个排序算法更好一些,则需要看具体的情况了。
4、总结
今天总结的这三种排序时三种线性时间复杂度的排序算法,都没有使用 比较操作。但是同样的三者都需要使用辅助内存来进行排序,典型的以空间换时间的操作。
从网上看的资料,感觉现在的文章复制的较多,桶排序和计数排序都给搞混了,计数排序的举例也是使用了每个辅助空间只存储单一元素的这种特例,看起来和桶排序是比较类似。所以如果大家有时间,还是找本书来进行系统的学习,别让网上的片段资料给带偏了。
感谢大家,我是假装很努力的YoungYangD(小羊)。
参考资料:
《算法导论》
https://www.runoob.com/w3cnote/radix-sort.html
https://www.cnblogs.com/dwj411024/p/5978821.html
https://blog.csdn.net/qq_39942341/article/details/82379334