非比较排序

基于比较的排序时间复杂度下界

以下内容参考自《算法导论》
基于比较的排序算法可以用一颗完全二叉树(决策树)来表示,节点表示每一次的比较过程,

叶子节点表示最终的排序顺序,叶子节点一共有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,所以后面的数要先收集。

复杂度分析

时间复杂度为O(n+k),k是n个数的范围,范围越大,则时间越复杂。当n和k大概相等时,复杂度可以达到O(n).
空间复杂度n+O(k),比较耗费空间。

基数排序

我们可以发现,利用一个关键字的计数排序面对三四位数的时候已经明显很慢了。我们可以拆分关键字,按照多关键字排序。

就拿正整数举例,比大小的话,高位上的数明显比低位上的数重要。所以我们可以按多关键字排序。

基数排序就是这样,但要注意我们收集的时候是先收集低位的数据排好,再收集高位的数据排列。因为先收集高位,

排低位的时候可能会把高位的数排到低位后面去,而先收集低位的话,排高位的时候只有两种可能,高位不同或者高位相同,

如果高位相同,那么低位已经按次序排好了,高位不同,自然不需要考虑低位,高低位都相同,由于计数排序的稳定性,

基数排序也是稳定的。

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)

桶排序

桶排序其实和计数排序是一样的,只不过《算法导论》单独提出来,被排序的数组内的数值在0-1均匀分配的时候,桶排序有线性时间(Θ(n))。
其过程就是准备和高位关键字数量相同的桶,按高位关键字依次收集,高位关键字相同的就用数组或链表保存值,然后再用比较排序来排序。
桶排序的平均时间复杂度分析很复杂,要用到概率论的一些知识,这里就略过,有兴趣的同学可以自己查阅《算法导论》。
下面是使用链表的桶排序:

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;
}
为便于操作,使用带头结点的链表。

复杂度分析

桶排序的平均时间复杂度为线性的n+O(c),其中c=n*(logn-logk)。如果相对于同样的n,桶数量k越大,其效率越高,
最好的时间复杂度达到O(n)。当然桶排序的空间复杂度为n+O(k),如果输入数据非常庞大,而桶的数量也非常多,
则空间代价无疑是昂贵的。此外,桶排序是稳定的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值