计数排序(Counting sort)
计数排序的基本思想:计数排序是一种非比较性质的排序算法,首先先用一块maximun - minimum + 1大小的额外空间存储序列中每个值出现的次数,然后遍历待排序序列,将每一个元素出现的次数记录到元素值对应的额外空间内,对额外空间内数据进行计算,得出每一个元素的正确位置,最后将待排序序列每一个元素移动到计算得出的正确位置上。
关于比较性质的算法:
比较性质排序算法的时间复杂度有一个理论边界,即 O(nlogn)。n 个元素的序列,能够形成的所有排列个数为n!,即该序列构成的决策树叶子节点个数为 n!,由叶子节点个数可知,决策树的高度为
log(n!),即由决策树根节点到叶子节点的比较次数为 log(n!),由斯特灵公式, n ! ≈ 2 π n ( n e ) n , n 充分大 {n! \approx \sqrt{{2 \pi n}}\mathop{{ \left( {\frac{{n}}{{e}}} \right) }}\nolimits^{{n}},n\text{充}\text{分}\text{大}}\\ n!≈2πn(en)n,n充分大
转换可得,比较性质的算法复杂度理论边界为 O(nlogn) 。
例如,如果对一组数据2,5,3,0,2,3,0,3从小到大排序,
由于maximum = 5, minimum = 0,则可以用数组C[6]记录每个值出现的次数。
我们对 C[6]数组按顺序求和,即C[k]里存储不大于minimum + k的元素数量。
当前数组也就是每个元素应该在最终位置,如C[2] = 4,说明小于等与2的元素有4个,即2应该是原序列第4位置的元素,放入后需要将C[2] --,这样将待排序序列每一个元素移动到计算得出的正确位置上,排序结束。
// 计数排序c/c++实现,a表示数组,n表示数组大小
/**
* Author: gamilian
*/
#include <stdio.h>
#include <stdlib.h>
void counting_sort(int a[], int n){
if (n <= 1)
return;
int maximum = a[0], minimum = a[0];
for (int i = 1; i < n; ++i){
if (maximum < a[i])
maximum = a[i];
if (minimum > a[i])
minimum = a[i];
} //求出最大值最小值
int length = maximum - minimum + 1;
int count_arr[length] = {0};
for (int i = 0; i < n; ++i) //记录数组中每个元素的次数
count_arr[a[i] - minimum]++;
for (int i = 1; i < length; ++i) //对额外空间的数据计算,得出每个元素的最终位置
count_arr[i] += count_arr[i - 1];
int temp_arr[length] = {0};
int count_index;
for (int i = n - 1; i >= 0; i--){ //反序遍历保证稳定性
count_index = a[i]- minimum;
temp_arr[count_arr[count_index] - 1] = a[i];
count_arr[count_index] --;
}
for (int i = 0; i < n; ++i)
{
a[i] = temp_arr[i];
}
}
# 计数排序python实现
"""
Author: gamilian
"""
from typing import List
def counting_sort(a: List[int]):
if len(a) <= 1: return
length = len(a)
maximum, minimum = max(a), min(a)
count_arr = [0] * (maximum - minimum + 1)
for x in a:
count_arr[x - minimum] += 1 # 记录数组中每个元素的次数
for i in range(1, len(count_arr)): # 计算每个元素的位置
count_arr[i] += count_arr[i - 1]
# 临时数组存储排序后的结果
temp_arr = [None] * length
for i in range(length - 1, -1, -1): # 反序遍历保持稳定性
count_index = a[i] - minimum
temp_arr[count_arr[count_index] - 1] = a[i]
count_arr[count_index] -= 1
a[:] = temp_arr
算法的稳定性:最后通过反序数组保持了算法的稳定性,所以计数排序是稳定的排序算法。
空间复杂度:算法过程中需要申请一个额外空间和一个与待排序集合大小相同的已排序空间,所以空间复杂度为 O(n+k).
时间复杂度:用于在额外空间中记录每一个元素出现的次数,复杂度为 O(n);用于计算每一个元素的最终位置,复杂度为 O(k),k为申请的额外空间大小;用于移动待排序集合中元素到已排序集合的正确位置上,复杂度为 O(n)。故不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(n+k),当k与n同数量级或者小于n时,时间复杂度即为 O(n),当k特别大时,时间复杂度即为O(k)。
由时间复杂度可以看出计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,建立映射,将其转化为非负整数。
桶排序(Bucket sort)
桶排序的基本思想:桶排序要求数据服从均匀分布,将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
快排与桶排序比较:
快排是将序列拆分为两个桶,再分别对两个桶进行排序,最终完成排序。桶排序则是将序列拆分为多个桶,对每个桶进行排序,最终完成排序过程。
两者不同之处在于,快排是在序列本身上进行排序,属于原地排序方式,且对每个桶的排序方式也是快排。桶排序则是提供了额外的操作空间,在额外空间上对桶进行排序,避免了构成桶过程的元素比较和交换操作,同时可以自主选择恰当的排序算法对桶进行排序。
也可以把桶排序看作是计数排序的改进:
计数排序申请的额外空间跨度从最小元素值到最大元素值,若待排序序列中元素不是依次递增的,则必然有空间浪费情况。桶排序则是弱化了这种浪费情况,将最小值到最大值之间的每一个位置申请空间,更新为最小值到最大值之间每一个固定区域申请空间,尽量减少了元素值大小不连续情况下的空间浪费情况。
也可以把计数排序看作是桶排序的特殊情况:
计数排序将数据分为maximum - minimum个桶。
桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。
// 桶排序c/c++实现,a表示数组,n表示数组大小
/**
* Author: gamilian
*/
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
#include<assert.h>
struct barrel {
int node[10];
int count;/* the num of node */
};
void bucket_sort(int a[], int n)
{
int max, min, num, pos;
int i, j, k;
struct barrel *pBarrel;
max = min = a[0];
for (i = 1; i < n; i++) {
if (a[i] > max) {
max = a[i];
} else if (a[i] < min) {
min = a[i];
}
}
num = (max - min + 1) / 10 + 1;
pBarrel = (struct barrel*)malloc(sizeof(struct barrel) * num);
memset(pBarrel, 0, sizeof(struct barrel) * num);
for (i = 0; i < n; i++) {
k = (a[i] - min + 1) / 10;// 计算index
(pBarrel + k)->node[(pBarrel + k)->count] = a[i];
(pBarrel + k)->count++;
}
pos = 0;
for (i = 0; i < num; i++) {
if ((pBarrel + i)->count != 0)
{
quick_sort_between((pBarrel+i)->node, 0, ((pBarrel+i)->count)-1);// 每个桶内快排
for (j = 0; j < (pBarrel+i)->count; j++) {
a[pos++] = (pBarrel+i)->node[j];
}
}
}
free(pBarrel);
}
# 桶排序python实现
"""
Author: gamilian
"""
from typing import List
def bucket_sort(a: List[int]):
maximum, minimum = max(a), min(a)
bucketa = [[] for i in range(maximum // 10 - minimum // 10 + 1)] # 建立空间映射规则
for i in a: # 将序列中的每个元素放相应的桶中
index = i // 10 - minimum // 10
bucketa[index].append(i)
a.clear()
for x in bucketa:
heap_sort(x) # 桶内元素堆排序
a.extend(x) # 把桶内的元素移动到数组
算法的稳定性:桶内通过合适的排序算法可以保持了算法的稳定性,所以桶排序是稳定的排序算法。
空间复杂度:由于需要申请额外的空间来保存元素,并申请额外的数组来存储每个桶。所以空间复杂度为 O(n+m).
时间复杂度:如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。
基数排序(Radix sort)
基数排序的基本思想:基数排序也可以称为多关键字排序,同计数排序类似,也是一种非比较性质的排序算法。将待排序序列中的每个元素拆分为多个总容量空间较小的对象,对每个对象执行桶排序后,则完成排序过程。
对于排序的数据不是等长时,比如对单词排序时,我们可以把所有的单词补齐到相同长度,位数不够的可以在后面补“0”,因为根据ASCII 值,所有字母都大于“0”,所以补“0”不会影响到原有的大小顺序,这样就可以继续用基数排序了。
// 基数排序c实现,a表示数组,n表示数组大小
/**
* Author: gamilian
*/
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
#include<assert.h>
#define NUM_OF_POS(a,pval) ((a)/pval)%10
void radix_sort(int a[],int size,int num_count)
{
int count[10] = {0}; /*计数*/
int *pres = NULL;
int i = 0;
int j = 0;
int pval = 10;
int index = 0;
int break_flg = 0;
pres = (int *)malloc(sizeof(int)*size);
assert(pres != NULL);
for (i = 0; i < num_count; i ++)
{
memset(count,0,sizeof(int)*10);
/*求当前的基数*/
pval = pow(10,i);
/*计数*/
for (j = 0; j < size; j++)
{
index = NUM_OF_POS(a[j],pval);
count[index]++;
}
/*小的优化,可能位数最大的就1,其他的位数差很多*/
if(count[0] == 9)
{
break_flg++;
}
if(break_flg >=2)
{
printf("\r\n %i",i);
break;
}
/*累加*/
for(j = 1; j < 10; j ++)
{
count[j] += count[j-1];
}
/*排序必须从后往前,否则不是稳定排序*/
for(j = size -1; j >= 0; j--)
{
index = NUM_OF_POS(a[j],pval);
pres[count[index] - 1] = a[j];
count[index]--;
}
/*本轮排序好的,拷贝到a中*/
memcpy(a,pres,sizeof(int)*size);
}
return;
}
# 基数排序python实现
"""
Author: gamilian
"""
from typing import List
def radixSort(a: List[int], radix=10:int): # 数组的元素时非负整数
k = math.ceil(math.log(max(a) + 1, radix)) # 计算长度
radixa = [[] for i in range(radix)]
for i in range(k): # 对每位排序
for j in a: # 把数组的每个元素加入到radixa
radixa[j // (radix ** i) % radix].append(j)
a.clear()
for a in radixa: # 将radixa里的元素加入到数组
a.extend(a)
a.clear()
算法的稳定性:对每位采用稳定的桶排序,可以保证算法的稳定性,所以基数排序是稳定的排序算法。
空间复杂度:算法过程中需要申请的空间大小为 n+r,其中 r 表示待排序元素的基数。
时间复杂度:基数排序的时间复杂度为 O(dn),其中 d 为元素最大位数,也就是维度。当d相比n较小的时候,基数排序的时间复杂度为 O(n)。
基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进的关系,如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了。