排序

插入排序(insertion sort)

插入排序由N-1趟排序组成,对于P = 1到P = N-1趟,插入排序保证从位置0到位置P上的元素为已排序状态。插入排序的平均情形是θ(N^2)

假设不存在重复的元素,有以下定理:

【定理】N个互异的数的数组的平均逆序数是N(N-1)/4

【定理】通过交换相邻元素进行排序的任何算法平均需要Ω(N^2)

void insertionSort(int a[], int n)
{
	int i, j, tmp;
	for(i = 1; i < n; i++)
	{
		tmp = a[i];
		for(j = i; (j > 0) && (a[j - 1] > tmp); j--)
			a[j] = a[j - 1];
		a[j] = tmp;
	}
}


希尔排序(Shell sort)

希尔排序也叫缩小增量排序(diminishing increment sort),它使用一个序列h1,h2,...,ht,叫做增量序列(increment sequence)。在使用增量h k的一趟排序之后对于每一个i我们有A[i] <= A[i + hk],即所有相隔hk的元素都被排序,此时称文件是hk-排序的。希尔排序的一个重要性质是,一个hk-排序的文件(下一步是h k-1-排序)保持它的hk-排序性。hk-排序的一般做法是,对于h k,h k+1,...,N-1中的每一个位置i,把其上的元素放到i,i-hk,i-2hk...中间的正确位置上。一趟hk-排序的作用就是对h k个独立的子数组进行一次插入排序

【定理】使用希尔增量时,希尔排序的最坏情形运行时间为θ(N^2)

希尔增量的问题在于,这些增量未必互质,因此较小的增量可能影响很小。Hibbard提出一个稍微不同的增量序列,它在实践中(并且在理论上)能给出更好的结果。他的增量是1,3,7,...,2^k-1

【定理】Hibbard增量的希尔排序的最坏情形运行时间为θ(N^3/2)

希尔排序的性能在实践中是完全可以接受的,即使是对于数以万计的N仍是如此。编程的简单特点使得它称为对适度的大量输入数据经常选用的算法

void shellSort(int a[], int n)
{
	int i, j, increment;
	int tmp;

	for(increment = n / 2; increment >= 1; increment /= 2)
		for(i = increment; i < n; i++)
		{
			tmp = a[i];
			for(j = i; (j >= increment) && (a[j - increment] > tmp); j -= increment)
				a[j] = a[j - increment];
			a[j] = tmp;
		}
}


堆排序

优先队列可以用O(N log N)的时间进行排序,基于这种想法的算法叫堆排序。尽管它的O运行时间优于希尔排序,但在实践中却慢于Sedgewick增量序列的希尔排序。该算法的主要问题在于它使用了一个附加的数组,因此存储需求增加了一倍。避免使用第二个数组的聪明做法是,因为在每次DeleteMin之后堆缩小了1,所以缩小的单元可以用来存放刚刚删掉的元素

经验指出,堆排序是一个非常稳定的算法:它平均使用的比较只比最坏情形界指出的略少

void swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}

void percDown(int a[], int i, int N)
{
	int child, tmp;

	for(tmp = a[i]; 2*i + 1 < N; i = child)
	{
		child = 2*i + 1;
		if((child + 1 < N) && (a[child] < a[child + 1]))
			child++;
		if(tmp < a[child])
			a[i] = a[child];
		else
			break;
	}
	a[i] = tmp;
}

void heapSort(int a[], int N)
{
	int i;
	
	for(i = N / 2; i >= 0; i--)	// 从N/2开始,只要保证向前所有的节点都大于自己的儿子节点,则堆就成功建立。注意要从N/2向前建立堆
		percDown(A, i, N);
	for(i = N - 1; i >= 0; i--)
	{
		swap(&a[0], &a[i]);
		percDown(A, 0, i);	// 将原来堆的最后一个元素移动到正确的位置
	}
}




归并排序(merge sort)

归并排序的基本操作是合并两个已排序的表,它的最坏情形运行时间是O(N log N)。具体算法是递归地将前半部分数据和后半部分数据各自归并排序,得到排序后的两部分数据,再将它们进行合并。虽然归并排序的运行时间是O(N log N),但它很难用于主存排序,因为合并两个各排序的表需要线性附加内存,同时对数据的频繁移动也带来了附加工作,结果严重放慢了排序速度。合并排序的例程大多数是外部排序

void Msort(int a[], int* tmpArr, int start, int end)
{
	if(start < end)
	{
		int center = (start + end) / 2;
		
		Msort(a, tmpArr, start, center);
		Msort(a, tmpArr, center + 1, end);
		Merge(a, tmpArr, start, center, end);
	}
}

void Merge(int a[], int* tmpArr, int left, int leftEnd, int rightEnd)
{	
	int right = leftEnd + 1;
	int i;
	while((left <= leftEnd) && (right <= rightEnd))
	{
		if(a[left] < a[right])
			tmpArr[i++] = a[left++];
		else
			tmpArr[i++] = a[right++];
	}
	
	while(left <= leftEnd)
		tmpArr[i++] = a[left++];
	while(right <= rightEnd)
		tmpArr[i++] = a[right++];
	
	for(i = 0; i <= rightEnd; i++)
		a[i] = tmpArr[i];
}

void mergeSort(int a[], int N)	// 之所以将Msort和mergeSort写成两个函数是为了避免重复为tmpArr分配空间而耗尽内存
{
	int* tmpArr = (int*)malloc(N * sizeof(int));
	if(tmpArr == NULL)
	{
		printf("Not enough space\n");
		exit(1);
	}

	Msort(a, tmpArr, 0, N - 1);

	free(tmpArr);
}



快速排序(quick sort)

快速排序是在实践中最快的已知排序算法,它的平均运行时间是O(N log N)。尽管最坏情形的性能为O(N^2),但稍加努力就可以避免这种情形。将数组S进行快速排序基本的算法由4个步骤组成:

1. 如果S中的元素个数为0或1,返回

2. 取S中的任意元素v,称为枢纽元(pivot)

3. 将S中除pivot外所有的元素分为两个不相交的集合S1和S2, S1中的所有元素都小于pivot,S2中所有的元素都大于pivot

4. 返回quickSort(S1),pivot,quickSort(S2)

直观地看,我们希望把等于pivot的关键字平均分配到S1和S2中

选取pivot:不能选择第一个元素或前两个互异的关键词中的较大者作为pivot,因为输入可能是预排序的或是反序的。常用的安全做法是使用最左端,最右端和中心位置上的三个元素的中值作为pivot

分割策略:第一步是将pivot与最后的元素交换使得pivot离开要被分割的数据段,然后将所有小于pivot的元素向左移动,大于pivot的元素向右移动。如下图所示,i右移,j左移,当i和j停止时i指向一个大的元素而j指向一个小的元素,此时将这两个元素交换,直到i、j交错为止。如果i/j遇到的关键字和pivot相等,则i/j停止。最后,将pivot与i所指向的元素交换


对于很小的数组(N <= 20),快速排序不如插入排序

在实际操作中,选取pivot最好的方法是对a[start],a[end]和a[center]适当地排序,将最小者放到a[start],最大者放到a[end],将pivot放到a[end - 1],这样不仅简化了比较,还可以避免j越界

#define CUTOFF 3

void swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}

int median(int a[], int start, int end)
{
	int center = (start + end) / 2;
	if(a[start] > a[center])
		swap(&a[start], &a[center]);
	if(a[start] > a[end])
		swap(&a[start], &a[end]);
	if(a[center] > a[end])
		swap(&a[center], &a[end]);

	swap(&a[center], &a[end - 1]);
	return a[end - 1];
}

void insertionSort(int a[], int start, int end)
{
	int i, j;
	
	if(end - start > 0)
	{
		for(i = start + 1; i <= end; i++)
		{
			int tmp = a[i];
			for(j = i; (j > start) && (a[j - 1] > tmp); j--)
				a[j] = a[j - 1];
			a[j] = tmp;	
		}
	}
}

void Qsort(int a[], int start, int end)
{
	if(start + CUTOFF <= end)
	{
		int pivot = median(a, start, end);
		int i = start;
		int j = end - 1;

		while(1)
		{
			while(a[++i] < pivot);	// 注意此处不能用 while(i < pivot) i++;
			while(a[--j] > pivot);
			if(i < j)
				swap(&a[i], &a[j]);
			else
				break;
		}
		swap(&a[i], &a[end - 1]);

		Qsort(a, start, i - 1);
		Qsort(a, i + 1, end);
	}
	else
		insertionSort(a, start, end);
}

void quickSort(int a[], int size)
{
	Qsort(a, 0, size - 1);
}

快速排序的最坏情形是O(N^2),最好情况和平均情况都是O(N log N)



大型数据结构的中,由于交换两个结构可能是非常昂贵的操作,所以实际的做法是让数组包含指向结构的指针


对于一般的内部排序应用,选用的方法一般是插入排序,希尔排序或快速排序,主要根据输入的大小来决定。如果需要对一些大型文件排序,那么应该选用快速排序(或者希尔排序也可以)




外部排序

当外部存储设备中的数据太多无法一次全部载入内存时,需要进行外部排序

简单算法:一次读入M个记录,在内部将记录排序称为顺串(run),交替写出到两路外设再进行归并排序


多路合并:类似简单算法,只是写出到多路。找出k个元素中的最小者稍微有些复杂,可以通过优先队列实现。但k路合并需要2k个外设

多相合并:用k+1个外设实现k-路合并。将顺串不均等地分配到外设中。如果顺串个数是斐波那契数Fn,最好的分配方法是分成F n-1和F n-2;否则需要添加哑顺串(dummy run)来进行填补


产生顺串的算法:

替换选择(replacement selection):将M个记录读入内存并放到一个优先队列中,执行一次DeleteMin将最小的记录写出,再从外设中读取一个新的记录;如果它比刚刚写出的大,则加入优先队列,否则存入优先队列但变成死区(dead space),直到优先队列大小为零,则顺串构建完成,再建立新的顺串

有可能替换选择并不比标准算法好,但由于数据常常是几乎排序的,此时替换选择仅仅产生数量很少的长顺串



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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值