基于比较的排序时间复杂度下界
以下内容参考自《算法导论》
基于比较的排序算法可以用一颗完全二叉树(决策树)来表示,节点表示每一次的比较过程,
叶子节点表示最终的排序顺序,叶子节点一共有n!个(n表示数据个数,一共有n!种排列方式)
二叉树的高度h就是比较的次数,由二叉树的性质,叶子节点的个数不大于2^h,所以有
2^h>=n!;
2^h>=n! = n*(n-1)*(n-2)*...*2*1 >= n*(n-1)*(n-2)*...(n/2) > (n/2)^(n/2);
两边同时取对数 h > (n/2)log2(n/2);
所以 O(n) = nlog2n
堆排序和归并排序的平均时间复杂度都是O(nlogn),所以它们是渐近最优的比较排序算法。
那么还有没有更快的排序算法呢?当然这里的更快是针对特定数据的,排序算法的时间复杂度下界是W(n),
因为至少每个元素都要被访问一次,根据hash表的思路我们可以建立一个映射关系,建立完关系后解析得到我们想要的序列。
这种算法以空间换时间,因此空间复杂度比较高,在某些时候未必就比基于比较的算法更快。
计数排序
计数排序就是准备和关键字范围相同长度的计数数组,先统计每个元素出现的个数,然后再求出小于等于该元素的元素个数,
也就是它应该放的地方,最后依次收集就可以了。
c代码
//计数排序
void count_sort(int a[], int size)
{
int i;
int c[10]; //计数数组
int *b = (int *)malloc(sizeof(int)*size);
for (i=0; i<10; i++)//初始化计数数组
{
c[i] = 0;
}
for (i=0; i<size; i++)
{
c[a[i]]++;
}
for (i=1; i<10; i++)
{
c[i] += c[i-1]; //统计小于等于的个数
}
for (i=size-1; i>=0; i--)
{
b[ --c[a[i]] ] = a[i];//从后面向前赋值,以保证排序的稳定性
}
for (i=0; i<size; i++)
{
a[i] = b[i];//写入原数组
}
free(b);
b = NULL;
}
int main()
{
int a[] = {1, 2, 0, 2, 4, 8, 9, 6};
int size = sizeof(a)/sizeof(int);
count_sort(a, size);
for (int i=0; i<size; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
这里我们举例0-9的元素排序,准备10个桶(0-9)然后统计每个数出现的次数,要注意最后的收集是从后往前的,
因为相同的元素收集完后个数要减1,所以后面的数要先收集。
复杂度分析
基数排序
我们可以发现,利用一个关键字的计数排序面对三四位数的时候已经明显很慢了。我们可以拆分关键字,按照多关键字排序。
就拿正整数举例,比大小的话,高位上的数明显比低位上的数重要。所以我们可以按多关键字排序。
基数排序就是这样,但要注意我们收集的时候是先收集低位的数据排好,再收集高位的数据排列。因为先收集高位,
排低位的时候可能会把高位的数排到低位后面去,而先收集低位的话,排高位的时候只有两种可能,高位不同或者高位相同,
如果高位相同,那么低位已经按次序排好了,高位不同,自然不需要考虑低位,高低位都相同,由于计数排序的稳定性,
基数排序也是稳定的。
c代码
//基数排序
void radix_sort(int a[], int size)
{
int i, j, r;
int c[10]; //计数数组
int *b = (int *)malloc(sizeof(int)*size);
for (j=0; j<3; j++)
{
for (i=0; i<10; i++)
{
c[i] = 0;
}
for (i=0; i<size; i++)
{
r = a[i]/int(pow(10, j))%10;
c[ r ]++;
}
for (i=1; i<10; i++)
{
c[i] += c[i-1]; //统计小于等于的个数
}
for (i=size-1; i>=0; i--)
{
r = a[i]/int(pow(10, j))%10;
b[ --c[r] ] = a[i];
}
for (i=0; i<size; i++)
{
a[i] = b[i];
}
}
free(b);
b = NULL;
}
int main()
{
int a[] = {112, 232, 0, 27, 4, 38, 99, 6};
int size = sizeof(a)/sizeof(int);
radix_sort(a, size);
for (int i=0; i<size; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
复杂度分析
时间复杂度:O( d(k+n) ) ,d是关键字的个数,k是每一个关键字的范围;
空间复杂度:n+O(k)
桶排序
c代码
#include <stdio.h>
#include <malloc.h>
typedef struct Node{
double value;
struct Node* next;
}Node, *pNode;
pNode findPos(pNode head, double val)
{
pNode p = head->next;
pNode pre = head;
while (p && p->value<val)
{
pre = p;
p = p->next;
}
return pre;
}
void insert_sort(pNode head)
{
pNode p = head->next;
pNode q = head;
while (p)
{
pNode nodePos = findPos(head, p->value);
q->next = p->next;
p->next = nodePos->next;
nodePos->next = p;
q = p;
p = p->next;
}
}
void insertValue(pNode head, double value)
{
pNode p= (pNode)malloc(sizeof(Node));
p->value = value;
p->next = NULL;
pNode q = head;
while (q->next) q=q->next;
q->next = p;
}
void bucket_sort(double a[], int size)
{
pNode bucket = (pNode)malloc(sizeof(Node)*size);//分配桶
int i;
for (i=0; i<size; i++)
{
bucket[i].value = 0;//头结点初始化值
bucket[i].next = NULL;
}
for (i=0; i<size; i++)
{
insertValue(&bucket[int(a[i]*10)], a[i]); //分配
}
for (i=0; i<size; i++)
{
insert_sort(bucket+i);//对每个桶使用插入排序
}
//收集
int j=0;
for (i=0; i<10; i++)
{
pNode p = bucket[i].next;
while (p)
{
a[j] = p->value;
p = p->next;
j++;
}
}
//释放内存
for (i=0; i<10; i++)
{
pNode p = bucket[i].next;
while (p){
pNode temp = p;
p = p->next;
free(temp);
temp = NULL;
}
}
free(bucket);
bucket = NULL;
}
int main(void){
double a[]={0.5,0.4,0.3,0.1,0.15,0.2,0.9,0.99,0.98,0.88, 0.23, 0.45};
int size = sizeof(a)/sizeof(double);
bucket_sort(a, size);
for (int i=0; i<size; i++)
{
printf("%.2f ", a[i]);
}
printf("\n");
return 0;
}
为便于操作,使用带头结点的链表。