之前的排序算法都是比较排序:在排序的最终结果中,各元素的次序依赖于他们之间的比较。任何比较排序在最坏情况下,都要经历Ω(n lgn)次比较,所以,归并排序和堆排序都是渐进最优的。
除了比较排序之外,还有其他的排序方法,但是都必须满足一定的前提条件,这些排序算法的下界不再是Ω(n lgn),而可以达到线性的下界。
1:决策树模型
比较排序可以抽象成一颗决策树, 他表示在给定的输入规模的情况下,某一特定排序算法对所有元素的比较操作。其中,控制,数据移动等其他操作都已被忽略了。比如下图就是针对三个元素的插入排序算法所对应的决策树。
在决策树中,每个内部结点都以i:j进行标记,其中1 i, j n。i,j都代表数组元素的下标,n是输入数组的大小。每个叶节点都是一个序列,也就是排序的结果。
排序算法的执行对应于一条从根节点到叶子结点的路径。每个内部结点表示了 和 的比较。左子树表示 ,右子树表示 。对于任意的输入,正确的排序算法都可以生成正确的序列,所以,对于n个元素来说,有n!中可能的结果,也就是说,决策树的叶子节点有n!个。
在决策树中,从根节点到叶节点的路径长度表示了算法的执行的比较操作的次数。所以一个排序算法的最坏情况比较次数就等于决策树的高度,所以就有这样的结论:在最坏情况下,任何比较排序算法都需要Ω(n lgn)次比较。
证明:假设树的高度为h,决策树叶子节点数为l,对于n个元素的输入规模,l 。同时,完全二叉树叶子结点数为 所以有:n! 。所以,h lg(n!) =Ω(n lgn)。
2:计数排序
条件:n个输入元素,每个元素都是在[0,k]区间之内的整数,其中,k为整数。
基本思想:对于输入元素x,确定小于x的元素个数,利用该信息,就可以直接把x放到输出数组的正确位置上了,比如有17个元素小于x,则x的位置应该是18了。当输入元素有相同时,需要略作修改。
伪代码(在该算法中,输入数组为A,输出数组为B,数组C表示了小于x的元素个数):
COUNTING-SORT(A,B, k)
letC[0…k] be a new array
for I = 0 to k
C[i]= 0 //初始化C中元素为0
forj =1 to A.len
C[A[j]]= C[A[j]] + 1 // C[i]中的元素表示,在数组A中,等于i的元素个数。
forI = 1 to k
C[i]= C[i] + C[i-1] //C[i] 中的元素表示,在数组A中,小于等于i 的元素个数。
for j = A.len downto 1 //为了维持稳定性。
B[C[A[j]]]= A[j] //将A[j]放在数组B中某个位置上,该位置为C[A[j]],也就是小于等于A[j]元素个数
C[A[j]]= C[A[j]] -1 //为了防止A中相同元素放在同一位置上
图示:
时间复杂度:该算法总的时间复杂度为O(n+k),在实际工作中,如果k = O(n),则计数排序的时间复杂度为Θ(n)。
备注:计数排序具有稳定性的特点,也就是说,原数组中,具有相同值的元素的相对位置,在输出数组中,相对位置不变。一般情况下,稳定性对于需要排序的数据中还附带卫星数据的情况至关重要。比如计数排序通常作为基数排序的子过程,计数排序的稳定性是基数排序的关键所在。
void countsort(int *set, int *res, int len, intboundary)
{
int i;
int csize= (boundary+1) * sizeof(int);
int *count= malloc(csize);
memset(count,0, csize);
for(i = 0;i <= boundary; i++)
{
count[i]= 0;
}
for(i = 0;i < len; i++)
{
count[set[i]]= count[set[i]] + 1;
}
for(i = 1;i <= boundary; i++)
{
count[i]= count[i] + count[i-1];
}
for(i =len-1; i>=0; i--)
{
res[count[set[i]]- 1] = set[i];
count[set[i]]--;
}
free(count);
}
3:基数排序
基本思想:n个输入元素,每个元素最多是d位数,对这n个元素进行排序时,直观上可能会先比较高位,然后对相同高位元素在比较次高位。这种算法会需要保存很多临时数据。基数排序的基本思想是,从最低位开始比较,首先根据第0位对数组A进行排序,结果为 ,然后根据第1位对数组 进行排序,得到结果为 依次重复下去直到最高位。为了保证基数排序的正确性,每一位排序的算法必须具有稳定性。(比如123和145两个数排序,最低位是3和5,所以按照最低位排序的次序是123.145;第二位是2和4,所以第二位排序后的结果是123.145;第三位是1和1,如果没有稳定性,那么有可能最终的次序是145.123)
伪代码:
RADIX-SORT(A,d)
forI = 1 to d
usea stable sort to sort array A on digit i
图示:
时间复杂度:对于n个d位数,其中每一位的可能取值在[0,k]内,如果每一位的排序算法为O(n+k),则基数排序的时间复杂度为 (d(n+k))。如果d为常数,且k =O(n),则基数排序的时间复杂度为 O(n)。
备注:在更一般的情况下,可以将给定的元素分成若干位,比如对于b位数,可以将其当做d位数,其中d=b/r,每一位其中还有r位。对于“每一位”来说,使用计数排序的时间为O(n+ )。所以时间复杂度为 ((b/r)( n+ ))。
这样,对于给定的n和b,如何选择r值使得最小化表达式(b/r)(n+2r)。如果b< lgn,对于任何r<=b的值,都有(n+ )=Θ(n),于是选择r=b,使基数排序的时间为Θ((b/b)(n+2b)) = Θ(n)。 如果b>lgn,则选择r=lgn,可以给出在某一常数因子内的最佳时间:当r=lgn使,算法复杂度为Θ(bn/lgn),当r增大到lgn以上时,分子 增大比分母r快,于是运行时间复杂度为Ω(bn/lgn);反之当r减小到lgn以下的时候,b/r增大,而n+ 仍然是Θ(n)。
对于基数排序和快速排序哪个算法更好,需要取决于实际情况,虽然基数排序的时间Θ(n)看上去要比快速排序要好,但是常数项因子不同,而且基数排序每一步耗费时间都要比快速排序要高。而且基数排序不是原址排序。完整代码如下:
void radixsort(int *set, int len, int bitlen)
{
int i, j;
int size = len * sizeof(int);
int *res = malloc(size);
memset(res, 0, size);
for(i = 0; i < bitlen; i++)
{
countsort_bit(set, res, len, i);
memcpy(set, res, size);
memset(res, 0, size);
}
free(res);
}
void countsort_bit(int *set, int *res, int len ,int bit)
{
int boundary = 9;
int size = (boundary+1) * sizeof(int);
int *count = malloc(size);
int i;
int bitnum = -1;
int temp = (int)pow(10, bit);
memset(count, 0, size);
for(i = 0; i < len; i++)
{
bitnum = (set[i] / temp) % 10;//get the bit num of set[i]
count[bitnum] = count[bitnum] + 1;
}
for(i = 1; i <= boundary; i++)
{
count[i] = count[i] + count[i-1];
}
for(i = len-1; i >= 0; i--)
{
bitnum = (set[i] / temp) % 10;
res[count[bitnum] - 1] = set[i];
count[bitnum]--;
}
free(count);
}
4:桶排序
条件:输入是由一个随机过程产生的,将n个元素均匀,独立的分布在在[0,1)区间上。
基本思想:将n个数均匀的放在n个桶中,因为输入是均匀的,所以一般不会出现很多数落在一个桶中的情况,为了得到输出结果,先对每个桶中的数进行排序,然后遍历每个桶即可,桶一般用链表来实现。
伪代码:
BUCKET-SORT(A)
n =A.len
letB[0..n-1] be a new array
forI = 0 to n-1
makeB[i] an empty list
forI = 1 to n
insert A[i] into list B[n A[i]]
forI = 0 to n-1
sortlist B[i] with insertion sort
concatenatethe lists B[0],B[1],…,B[n-1] together in order
图示:
时间复杂度:桶排序的时间复杂度也是Θ(n)。完整代码如下:
typedef struct Node
{
double num;
struct Node *next;
}node;
void bucketsort(double *set, int len)
{
node ** bset = NULL;
node *p = NULL, *q = NULL;
int size = len * sizeof(node *);
int i, j;
node *e = NULL;
bset = malloc(size);
memset(bset, 0, size);
for(i = 0; i < len; i++)
{
e = malloc(sizeof(node));
e->num = set[i];
e->next = NULL;
p = bset[(int)(set[i] * len)];
if(p == NULL)
{
bset[(int)(set[i] * len)] = e;
}
else
{
q = p;
while(p != NULL)
{
if(p->num > e->num)
{
break;
}
q = p;
p = p->next;
}
if(p == bset[(int)(set[i] * len)])
{
bset[(int)(set[i] * len)] = e;
e->next = p;
}
else
{
q->next = e;
e->next = p;
}
}
}
i = 0;
j = 0;
while(i < len)
{
p = bset[i];
while(p != NULL)
{
set[j++] = p->num;
p = p->next;
free(p);
}
i++;
}
free(bset);
}
5:选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。(归基插帽)