序言
前面的章节主要讲述的排序算法都是通过比较来得到已排序好的数列,我们通常称这一类排序算法为比较排序
。比如:插入排序(直接插入排序、折半插入排序、希尔排序),交换排序(冒泡排序,快速排序),选择排序(简单选择排序,堆排序),归并排序。这些排序算法都有一个共同的特点,就是基于比较。本篇博客主要介绍三个非比较排序算法:计数排序,基数排序,桶排序。他们是以线性时间运行,打破了以Ω(nlgn)为下界。
在讲下面的内容之前,我看到一篇博客,讲述的比较排序的算法以动图形式表示,便于快速理解:比较排序和决策树
排序算法的下界
上面说到比较排序是指通过比较来决定元素间的相对次序,而在比较算法中通常只有<=, >,因为通常等于在排序算法中是没有意义的,基于排序算法,我们划分为这两种形式,于是我们就可以用二叉树来表示,我们管这种二叉树叫做决策树
。
决策树模型(decision-tree model)
可以说任何一种比较排序算法都可以用决策树来表示,决策树的规模通常是随着输入规模n的一种指数级增长的二叉树。它可以表示在给定输入规模情况下,其一特定排序算法对所有元素的比较操作。其中的控制、数据移动等其他操作都被忽略了。有书中举例的输入规模为3的待排序数列抽象成决策树:
我们可以看出对于n个数的排序,我们可以得到 n!种排列顺序,在决策树中就是有 n!个叶结点。
最坏情况的下界
一个排序算法最坏情况的比较次数就等于其决策树的高度,同时,当决策树的每个排序情况都可以用可到达的叶结点表示时,该决策树高度的下界也就是该排序算法运行时间的下界。
定理8.1: 在最坏情况下,任何比较排序算法都需要做Ω(nlgn)次比较。
推论8.2: 堆排序和归并排序都是渐进最优的比较排序算法。
计数排序
计数排序假设n个输入元素中的每一个都是在0到k区间内的一个整数,其中k为某个整数。当k=O(n)时,排序的运行时间为Θ(n)。
计数排序的基本思想是:对每一个输入元素 x,确定小于x的元素个数。利用这一信息,就可以直接把x放到它在输出数组中的位置上了。例如,如果有17个元素小于x,则x就应该在第18个输出位置上。当有几个元素相同时,这一方案要略做修改。因为不能把它们放在同一个输出位置上。
同时计数排序算法是一种稳定的算法,它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法,当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(nlgn)的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)),如归并排序,堆排序)
算法的步骤如下:
1.找出待排序的数组中最大和最小的元素
2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项
3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
根据书中的伪代码为:
COUNTING-SORT(A, B, k)
let C[0..k] be a new array
for i = O to k
C[i] = 0
for j=1 to A.length
C[A[j]]=C[A[j]]+1
for i=1 to k
C[i] = C[i] + C[i-1]
for j=A.length downto 1
B[C[A[j]]]=A[j]
C[A[j]]=C[A[j]]- 1
我将其转化为C语言形式:
void Counting_Sort(int* a, int* b, int k, int length) {
/*
这是一个计数排序算法,数组a是一个待排数列,数组b是已经排好的数列,
k为数组中最大值,length为数组的长度
*/
int* c; //定义一个储存空间的数组
int i, j;
c = (int*)malloc((k + 1) * sizeof(int));
for (i = 0; i <= k; i++)
c[i] = 0;
for (j = 0; j < length; j++) //计数
c[a[j]] += 1;
for (i = 1; i <= k; i++)
c[i] = c[i] + c[i - 1];
for (j = length - 1; j >= 0; j--) {
b[c[a[j]]] = a[j];
c[a[j]] = c[a[j]] - 1;
}
free(c);
}
实现过程为:
基数排序
基数排序是一种用在卡片排序机上的算法,普通的卡片有80列,每一列有12个孔,操作员根据卡片给定列上的数字来选定应该放入哪个孔,从而对所有的列的数字完成排序,如下一种直观的显示:
对于10进制数来说,每列只会用到10个数字,有多少位数就表示有多少列,对于每一列的数字,我们可以采用任何排序算法,但最好使用稳定排序,由于每一列的输入的所有元素都为0~10之间的元素(对于10进制数),很明显,我们应该采用计数排序来进行(这里就是计数排序稳定性的体现了)。如下为使用计数排序来实现基数排序的代码:
#include<stdio.h>
#define MAX 20
//#define SHOWPASS
#define BASE 10
void print(int *a, int n) {
int i;
for (i = 0; i < n; i++) {
printf("%d\t", a[i]);
}
}
void radixsort(int *a, int n) {
int i, b[MAX], m = a[0], exp = 1;
for (i = 1; i < n; i++) {
if (a[i] > m) {
m = a[i];
}
}
while (m / exp > 0) {
int bucket[BASE] = { 0 };
for (i = 0; i < n; i++) {
bucket[(a[i] / exp) % BASE]++;
}
for (i = 1; i < BASE; i++) {
bucket[i] += bucket[i - 1];
}
for (i = n - 1; i >= 0; i--) {
b[--bucket[(a[i] / exp) % BASE]] = a[i];
}
for (i = 0; i < n; i++) {
a[i] = b[i];
}
exp *= BASE;
#ifdef SHOWPASS
printf("\nPASS : ");
print(a, n);
#endif
}
}
int main() {
int arr[MAX];
int i, n;
printf("Enter total elements (n <= %d) : ", MAX);
scanf("%d", &n);
n = n < MAX ? n : MAX;
printf("Enter %d Elements : ", n);
for (i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
printf("\nARRAY : ");
print(&arr[0], n);
radixsort(&arr[0], n);
printf("\nSORTED : ");
print(&arr[0], n);
printf("\n");
return 0;
}
过程用动态图表示,可以比较直观的看出:
桶排序
桶排序的思想
- 得到无序数组的取值范围
- 根据取值范围创建对应数量的"桶"
- 遍历数组,把每个元素放到对应的"桶"中
- 按照顺序遍历桶中的每个元素,依次放到数组中,即可完成数组的排序。
其中"桶"是一种容器,这个容器可以用多种数据结构实现,包括数组、队列或者栈。
复杂度
- 时间复杂度:遍历数组求最大值最小值为O(n),遍历数组放入"桶"中复杂度为O(n),遍历桶取出每个值的复杂度为O(n),最终的时间复杂度为O(3n),也就是O(n)
- 空间复杂度:额外的空间取决于元素的取值范围,总的来说为O(n)
- 稳定性:桶排序是否稳定取决于"桶"用什么数据结构实现,如果是
队列
,那么可以保证相同的元素"取出去"后的相对位置与"放进来"之前是相同的,即排序是稳定
的,而如果用栈
来实现"桶",则排序一定是不稳定
的,因为桶排序可以做到稳定,所以桶排序是稳定的排序算法
伪代码为:
过程大致是这样的:
例如要对大小为[1…1000]范围内的n个整数A[1…n]排序,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1…10]的整数,集合B[2]存储(10…20]的整数,……集合B[i]存储((i-1)10, i10]的整数,i = 1,2,…100。总共有100个桶。然后对A[1…n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 然后再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任何排序法都可以。最后依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这样就得到所有数字排好序的一个序列了。
C语言代码实现:
#include<stdio.h>
#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++) //遍历比较,找到大的就赋值给maxVal
{
if(arr[i] > maxVal)
maxVal = arr[i];
}
return maxVal; //返回最大值
}
//桶排序 参数:数组及其长度
void BucketSort(int* arr , int len)
{
int tmpArrLen = GetMaxVal(arr , len) + 1;
int tmpArr[tmpArrLen]; //获得空桶大小
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]--;
}
}
}
int main()
{ //测试数据
int arr_test[Max_len] = { 8, 4, 2, 3, 5, 1, 6, 9, 0, 7 };
//排序前数组序列
Show( arr_test, Max_len );
//排序
BucketSort( arr_test, Max_len);
//排序后数组序列
Show( arr_test, Max_len );
return 0;
结语
本篇博客参照算法导论,并用到其他人的博客帮助理解。由于自身知识水平有限,对于算法导论书中所提到的算法分析过程,推导过程,定理的证明过程不做解释。待日后,个人水平提高后再做补充。