C--排序算法:shell排序、快速排序

 

1、shell排序

Shell排序是这样来分组并排序的:将序列分成子序列,然后分别对子序列进行排序,最后将子序列组合起来。Shell排序将数组元素分成“虚拟”子序列,每个子序列用插入排序方法进行排序;另一组子序列也是如此选取,然后排序,依此类推。在执行每一次循环时, Shell排序把序列分为互不相连的子序列,并使各个子序列中的元素在整个数组中的间距相同。例如,为方便起见,我们设数组中元素的个数n是2的整数次幂。Shell排序的一种可能的实现是首先将它分成n/2个长度为2的子序列,每个子序列中两,个元素的下标相差n/2,如果有数组下标为0-15的16个记录,那么首先是将它分成8个各有两个记录的子序列,第一个序列元素的下标是0和8,第二个下标是1和9,依此类推。每一个两元素的序列都采用插入排序法来排序。,第二轮Shell排序将处理数量少一些的子序列,但每个子序列都更长了。对于上面的例子来说会有n/4个长度为4的子序列,序列中的元素相隔n/4,因此,第二次分割的第一个子序列中有位于0,4,8,12的4个元素,第二个子序列的元素位于1.5,9,13,依此类推。每一个四元素的子序列仍然用插入排序法进行排序。第三轮将对两个子序列进行排序,其中一个包含原数组中的奇数位上的元素,另一个包含偶数位上的元素。最后一轮将是一次“正常的”的插入排序。

Shell排序并不关心分割的子序列中元素的间隔(尽管最后的间隔为1,是一个常规的插,人排序),如果Shell排序总是以一个常规的插入排序结束,又怎么会比插入排序的效率更高呢?我们希望经过每次对子序列的处理可以使待排序的数组更加有序。这一情况不一定准确,但实际确实如此。当最后一轮调用插入排序时,数组已经是基本有序的了,并产生一个相对所花时间较少的最终插入排序。

void insert(int data[], int n, int incr)
{
	int i,j;
	for(i=incr; i<n; i += incr){
		for(j=i; (j>=incr)&&(data[j]<data[j-incr]); j-=incr){
			data[j]      = data[j]^data[j-incr];
			data[j-incr] = data[j]^data[j-incr];
			data[j]      = data[j]^data[j-incr];
		}
	}
}
//shell排序
void fun(int data[],int n)
{
	int i,j;
	for(i=n/2; i>2; i/=2){
		for(j=0; j<i; j++){
			insert(&data[j], n-j, i);
		}
	}
	insert(data,n,1);
}

2、快速排序

快速排序首先选择一个轴值(pivot),假设输入的数组中有k个小于轴值的结点,于是这些结点被放在数组最左边的k个位置上,而大于轴值的结点被放在数组最右边的n-k个位置上。这称为数组的一个分割(partition),在给定分割中的值不必被排序,只要求所有结点都放到了正确的分组位置中。而轴值的位置就是下标k。快速排序再对轴值的左右子数组分别进行类似的操作,其中一个子数组有k个元素,而另一个有n-k-1个元素。那么,这些值又是如何进行排序的呢?由于快速排序是一个好算法,可以对这两个子数组继续使用快速排序法。

//快速排序,第一次调用时输入fun1(data,0,n-1)
void fun1(int data[], int i, int j)
{
	int pivotindex,m,k;
	if(j <= i)
		return;
	pivotindex = findpiot(data, i, j);
	m = data[pivotindex];
	data[pivotindex] = data[j];
	data[j] = m;
	k = partition(data, i-1, j, data[j]);
	m = data[k];
	data[k] = data[j];
	data[j] = m;
	fun1(data, i, k-1);
	fun1(data, k+1, j);
}

选择轴值有多种方法。最简单的方法是使用第一个记录的关键码。但是,如果输入的数组是正序的或者是逆序的,就会将所有结点分到轴值的一边。较好的方法是随机选取轴值。这样可以减少由于较差的原始输入对排序造成的影响。遗憾的是,随机选取轴值的开销较大,,所以可用选取数组中间点的方法代替。下面是一个简单的findpivot函数:

int findpiot(int data[], int i, int j)
{
	return (i+j)/2;
}

函数partition将记录移动到合适的分组,然后返回值k,这是分割后的右半部分的起始位置。注意在调用partition之前,轴值已被放在数组的最后一个位置上了(位置i)。因此,函数分割一定不会影响到数组中;所指的记录。然后轴值被放到下标为k的位置上,这就是它在最终排好序的数组中的位置。要做到这一点,必须保证在递归调用qsort的过程中轴值不再移动。即使是在最差情况下选择了一个不好的轴值,导致轴的一边分割出了一个空子数组,而另一个子数组中起码有n-1个记录,也不能移动。

现在来看函数partition。如果事先知道有多少个结点比轴值小,则partition只需将关键码值比轴值小的结点放到数组的低端,关键码值比轴值大的元素放到高端。由于事先并不知道 , 有多少关键码比中心点(轴值)小,所以可用一种较为巧妙的方法来解决:从数组的两端移动下标,必要时交换记录,直到数组两端的下标相遇为止。

int partition(int data[], int l, int r, int pivot)
{
	int m;
	do{
		while(data[++l] > pivot);
		while((r != 0)&&(data[--r]<pivot));
		m = data[l];
		data[l] = data[r];
		data[r] = m;
	}while(l < r);
	m = data[l];
	data[l] = data[r];
	data[r] = m;
	return l;
}

加入冒泡排序进行比较,在Linux下编译执行:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>


//冒泡排序:排序时间代价总是n^2;交换次数较多
void fun2(int data[], int n)
{
	int i,j;

	for(i=0; i<n-1; i++){
		for(j=n-1; j>1; j--){
			if(data[j] < data[j-1]){
				data[j]   = data[j]^data[j-1];
				data[j-1] = data[j]^data[j-1];
				data[j]   = data[j]^data[j-1];
			}
		}
	}
}


int main()
{
	int i;
	int data[100000] = {0};
	struct timespec time;
    clock_gettime(CLOCK_REALTIME, &time);

	srand((int)(time.tv_sec));
	for(i=0; i<100000; i++){
		data[i] = rand()%1000;
	}

	printf("shell排序\n");
	clock_gettime(CLOCK_REALTIME, &time);
	printf("%ld\n",time.tv_sec);
	fun(data,100000);
	clock_gettime(CLOCK_REALTIME, &time);
	printf("%ld\n",time.tv_sec);
	printf("end\n");

	srand((int)(time.tv_sec));
	for(i=0; i<100000; i++){	
		data[i] = rand()%1000;
	}

	printf("快速排序\n");
	clock_gettime(CLOCK_REALTIME, &time);
	printf("%ld\n",time.tv_sec);
	fun1(data, 0, 100000-1);
	clock_gettime(CLOCK_REALTIME, &time);
	printf("%ld\n",time.tv_sec);
	printf("end\n");

	srand((int)(time.tv_sec));
	for(i=0; i<100000; i++){
		data[i] = rand()%1000;
	}
	printf("冒泡排序\n");
	clock_gettime(CLOCK_REALTIME, &time);
	printf("%ld\n",time.tv_sec);
	fun2(data,100000);
	clock_gettime(CLOCK_REALTIME, &time);
	printf("%ld\n",time.tv_sec);
	printf("end\n");


}

可以得到结果:

在十万级的数据排序中可以看到shell排序和快速排序用时都不到1秒钟,而冒泡排序用了53秒,可见两种算法的差距是多大!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值