`
evasiu
  • 浏览: 168934 次
  • 性别: Icon_minigender_2
  • 来自: 广州
博客专栏
Fa47b089-e026-399c-b770-017349f619d5
TCP/IP详解卷一>阅读...
浏览量:12524
社区版块
存档分类
最新评论

编程珠玑 -- 关于排序

阅读更多

《编程珠玑》主要提到的排序方法是快排,并通过对基本算法思想的微调,以提高效率及保证最坏情况下的性能。我又回过头去看了Clifford A. Shaffer的《数据结构与算法分析》,总结了几种内排序(internal sorting)算法。(所谓的内排序,就是把所有的数据都加载到内存中后再进行排序)

 首先是插入排序。插入排序就跟我们平时在排放扑克牌的算法一样。每进来一张排,就从尾到前找,为它找到一个合适的位置,然后插入为止。把手里的牌看成一个已排序的子数组X,新插入的牌序号为i,数组X[0,i-1]是手里的牌,且是已经排好序的了,因此,对于新进来的牌,首先是从位置i-1起为它找到一个合适的位置,也就是找到第一个比他小的牌j(我们讨论升序排序),然后把牌 i 插入到牌 j 后面,这时在数组上就表现为X[j+1,i-1]都要往后面移一位。

//基本的插入排序算法思想
void insertSort( int* array, int n ){
	int t;
	for( int i=0; i<n; i++ )
		for( int j=i; j>0 && array[j-1]>array[j]; j-- ){
			t = array[j-1];
			array[j-1] = array[j];
			array[j] = t;
		}
	}
//对insertSort的一点小改进,减少不必要的交换			
void insertSort2( int* array, int n ){
	int t, j;
	for( int i=1; i<n; i++ ){
		t = array[i];		//把刚进来的牌拿在手上
		j = i;
		//从已经排好序的牌的后面开始,一直找到最后一张比它大的牌
		//这个过程中,比它大的牌都在向后移
		while( j>0 && array[j-1]>t ){	
			array[j] = array[j-1];
			j--;
		}
		array[j]=t;		//把这张牌放在这张牌的前面
	}
}

 

接下来是冒泡排序。冒泡排序的基本思想就是从数组的最后一个元素开始,比较相邻的两个数组元素,如果前面的元素大于后面的元素,那么就交换一下两个元素的位置。这样的话,第一次循环将使最小的元素冒泡到第一个元素,第二次将使第二小的元素冒泡。。。最后整个数组就完成了排序。

void bubbleSort( int*array, int n ){
	int t;
	for( int i=0; i<n; i++ ){	//循环i的最后第i小的元素就冒泡了
		for( int j=n-1; j>i; j-- ){ 
			if( array[j]<array[j-1] ){
				t = array[j];
				array[j] = array[j-1];
				array[j-1] =t;
			}
		}
	}
}

 

然后是选择排序。在冒泡排序中,其实第i个循环就是为了找到第i大的值,然而却因此而交换了很多次元素,无疑是一种浪费。选择排序是这样一种思想,他也在第i次循环的时候找第i小的元素,但是他通过使用一个临时变量来存放这个第i小的值,从而减少了一些不必要的交换。所以说它是对冒泡排序的一种改进,改进的思想就像对插入排序的改进思想一样。

 

void selectionSort( int* array, int n ){
	int small;
	for( int i=0; i<n; i++ ){
		small = i;
		for( int j=i+1; j<n; j++ ){
			if( array[j] < array[small] )
				small = j;
			}
		int t = array[small];
		array[small] = array[i];
		array[i] = t;
	}
}

 但是无论如何改进,这三种算法的算法复杂度都是O(n^2),他们的主要瓶颈都是在于,他们本质上就是通过相邻元素的比较和交换来达到排序的结果。这种算法可以称为“交换排序”。

 
快排的基本思想是,首先挑一个元素k,然后把剩下的元素按照分成两边,一边所有的元素都小于或等于元素k,另一个边所有的元素都大于元素k,这样,元素k的位置就确定了;接下来只需要对数组X[0...k-1]和X[k+1,u]做同样的工作就行了。因此快排的可以分成两个步骤:首先挑选某一特定元素k,并为k找到它排序后的位置,然后就是对X[0...k-1]和X[k+1,u]递归地做同样的事情,直到要处理的数组只有一个或根本没有元素了。

 

void qsort1( int* array, int l, int u ){
	//如果数组只有一个元素或者为空,就直接返回
	if( l>=u )
		return;
	int m = l;	
	int t;

//array[l...m]<=array[l], array[m+1,i-1]>array[l], array[i,u]未排序
	for( int i=l+1; i<=u; i++ ){
		if( array[i]<=array[l] ){
			m++;
			if( i != m ){
				t = array[m];
				array[m] = array[i];
				array[i] = t;
			}
		}
	}
	t = array[l];
	array[l] = array[m];
	array[m] = t;
	qsort1( array, l, m-1 );
	qsort1( array, m+1, u );
}
	

//qsort2从左到右找到第一个大于array[l]的元素
//然后再从右到左找到第一个小于array[l]的元素
//然后再做交换,直到[i,j]的数组为空
//这样就省了很多次不必要的交换
void qsort2( int* array, int l, int u, int thresh ){
	if( u-l < thresh )
		return;
	int i=l+1, j=u;
	int t;
	while( i<=j ){
		while( array[i]<=array[l] && i<=j )
			i++;
		while( array[j]>array[l] )
			j--;
		if( i > j )
			break;
		else{
			t = array[i];
			array[i] = array[j];
			array[j] = t;
			i++;
			j--;
		}
	}
	if( j>l ){
		t = array[j];
		array[j] = array[l];
		array[l] = t;
	}
	
	qsort2( array, l, j-1, thresh );
	qsort2( array, j+1, u, thresh );
}

 影响快排的效率的有两个方面,一个是元素k的选择,如果不幸元素k是数组中的最大或最小元素,那么一次分组就将得到一个n-1元素的子串和一个只有一个元素的子串,而为了让这个元素k到达它的正确的位置,我们比较了n次元素,如果每次对子串分组都这样,那么其复杂度也将到达O(n^2),而由于快排使用递归,因此开销可能比前面讲到的三种算法还要大,为了避免这种情况的出现,一般不拿第一个元素做中间元素,而是在数组中随机找一个元素。

第二个问题是递归,当数组很大时,将会递归很多次,我们知道每次递归都会在栈里保存信息,如果递归太多次了,可能会超出栈的最大范围;另一方面,随着数组的不断分组,会出现很多小的子数组,快排在这些小的子数组上花费了太多时间了。为了减少这方面的开销,当子数组很小时,我们停止使用快排,也就是说,快排的最后结果只是接近排序,但还有一些子序列没有排好,这个时候使用排入排序对整个数组再做一次排序,反而性能会更好。一般情况下,快排的期望复杂度为O(nlogn)。

ShellSort也是利用这个原理来提高排序的效率的。他先通过子串的排序来使整个数组接近排序状态,最后再用排入排序来对整个数组进行一次排序。不过ShellSort所谓的子串并不是相邻的元素,而是相邻x个元素元素组成的子数组。换个说法,在插入排序里面,一个数组的元素之间的距离为1,而在Shell排序里,一个数组的元素之间的数组由某个值x按比例地缩小到1.书中建议这个序列的比例最好为3(2是最差的情况)。如果以3为倍数递减的话,最后其平均复杂度为O(n^1.5).顺便说一下,ShellSort的名字的来源是发明这个算法的人叫D.L.Shell,跟壳什么的没有关系,我一直在想着它着壳排序有什么形象上的联系呢。

 

//首先修改一下insertSort,使它支持对隔x个元素的子数组进行排序
void insertNSort( int* array, int n, int x ){
	int t, j;
	for( int i=x; i<n; i=i+x ){
		t = array[i];
		j = i;
		while( j>=x && array[j-x]>t ){
			array[j] = array[j-x];
			j = j-x;
		}
		array[j] = t;
	}
}

void shellSort( int* array, int n ){
	for( int i=n/3; i>3; i=i/3 ){
		for( int j=0; j<i; j++ )
			insertNSort( &array[j], n-j, i );
		}
	insertNSort( array, n, 1 );
}

 

 MergeSort也是通过将数组分成几个小数组进行排序,然后再总体排序的,不过这一次他的子数组就是连在一起的了。首先把数组分成两份,对这两份子数组进行排序,然后再把他们merge合在一起。对子数组递归地进行相同的操作,直到子数组只有一个或根本没能元素。一般情况下,需要另一个数组来盛放待合并的数组。

void mergeSort1( int* array, int* temp, int l, int r ){
	if( l>=r )
		return;
	int m=(l+r)/2;
	mergeSort1( array, temp, l, m );
	mergeSort1( array, temp, m+1, r );
	for( int i=l; i<=r; i++ )	//把暂时排好的两个子数组复制到临时数组,
		temp[i] = array[i];
	//接下来进行合并
	int i=l, j=m+1;	//两个子数组的首部
	for( int cur=l; cur<=r; cur++ ){
		if( i > m )
			array[cur] = temp[j++];
		else if( j > r )
			array[cur] = temp[i++];
		else if( temp[i]<temp[j] )
			array[cur] = temp[i++];
		else
			array[cur] = temp[j++];
		}
	}
	
//mergeSort1为了判断两个子序列到达尾部而做了很多次if判断
//下面这种算法通过转变一下复制的顺序,减少了这些判断
//另外,跟quickSort一样,为了减少对小数组的递归,添加了thresh
//小于thresh长度的子数组将使用插入排序
void mergeSort2( int* array, int*temp, int l, int r, int thresh ){
	if( r-l < thresh ){
		insertSort( &array[l], r-l+1 );
		return;
	}
	int m=(l+r)/2;
	mergeSort2( array, temp, l, m, thresh );
	mergeSort2( array, temp, m+1, r, thresh );
	int i, j;
	for( i=l; i<=m; i++ )	temp[i]=array[i];	//正向拷贝
	for( j=r; j>m; j-- ) temp[j]=array[i++];	//逆向拷贝
	i=l; j=r;
	for( int k=l; k<=r; k++ ){
		if( temp[i]<temp[j] ) 
			array[k]=temp[i++];
		else
			array[k]=temp[j--];
		}
	}
 

考虑这样一种情况,假设一个数组有x个[0,n-1]范围内的元素,下面的语句将使数组在O(n)时间内排序:

for( int i=0; i<x; i++ )
    B[A[i]]=A[i];

相当于我有n个标了号的桶,每个桶可以放一个球,标号为x的球只能放在标号为x的桶里。那样的话,当把所有的球都放到相应的桶里面后,就实现了这个数组的排序了。就像我们前面用过的bitSort一样,只不过这次用的是一个数组元素的位置来表示这个数字是否存在,而在bitSort中只用了一个bit。如果我们把桶弄大一些,比方说,每个桶可以盛放一定标号范围内的球,比方说[0,10],[11,20]...。当把所有的球都放到相应的桶里面去后,如果数据是随机的,那么每个桶里面应该有差不多数量的球,而且数量也不多,这样对每个桶分别使用一次插入排序之类的,再按顺序从不同范围内的桶里面把球收集回来,就得到了排序好了的数组了。这就是桶排序。桶排序的性能很大程度上依赖于数据的分布,每个桶可以盛放的数据的数量应该怎么确定?还是说用链表而不是数组来盛放这些数据?每个桶可盛放的数据的范围如何确定?总共需要多少个桶?

RadixSort用一个基数做为桶的数量。比方说我们设基数为10,第一轮的时候把数组根据个位数从0到9排放,第二轮的时候把数组根据十位数从0到9排放,假设排序的数据范围为[0,99],那么经过两轮就能够把数据从小到大排序了。这个过程可以用下图来表达(很难用语言表达):

//这份代码中,有两个地方可以改进
//首先是(array[j]/rtok)%radix这个运算很耗时,可以通过把radix设为2的指数如16
//然后用与或操作来获得下标
//其次是每一轮做完后都要从temp向array赋值,其实交换使用temp,
//最后通过k的奇偶来确定是否需要从temp向A赋值。
//算法的复杂度是O(n*k+radix*k)
void radixSort( int* array, int n, int radix ){
	//用cnt[i]来保存桶bin[i]中的数据的个数
	int* cnt = new int[radix];
	//用temp[]数组来临时存放数组array
	int* temp = new int[n];
	//首先求得数组的最大值,从而确定要循环多少轮
	int large = array[0];
	for( int i=1; i<n; i++ ){
		if( large < array[i] )
			large = array[i];
		}
	int k=0;
	while( large>0 ){
		k++;
		large = large/radix;
	}
	for( int i=0, rtok=1; i<k; i++, rtok *=radix ){
		for( int j=0; j<radix; j++ )	cnt[j] = 0;
		for( int j=0; j<n; j++ )	cnt[(array[j]/rtok)%radix]++;
		for( int j=1; j<radix; j++ )	cnt[j]=cnt[j]+cnt[j-1];
		for( int j=n-1; j>=0; j-- )	temp[--cnt[(array[j]/rtok)%radix]]=array[j];
		for( int j=0; j<n; j++ )	array[j] = temp[j];
	}

	delete []cnt;
	delete []temp;
}

void radixSort2( int* array, int n, int shift ){
	//用cnt[i]来保存桶bin[i]中的数据的个数
	int bins = 1<<shift;
	int* cnt = new int[bins];
	int mask=0x1;
	for( int i=0; i<shift; i++ )
		mask |= (1<<i);
	//用temp[]数组来临时存放数组array
	int* temp = new int[n];
	//首先求得数组的最大值,从而确定要循环多少轮
	int large = array[0];
	for( int i=1; i<n; i++ ){
		if( large < array[i] )
			large = array[i];
		}
	int k=0;
	while( large>0 ){
		k++;
		large = large>>shift;
	}
	int *A=array, *B=temp, *C;
	for( int i=0, rtok=0; i<k; i++, rtok +=shift ){
		for( int j=0; j<bins; j++ )	cnt[j] = 0;
		for( int j=0; j<n; j++ )	cnt[(A[j]>>rtok)&mask]++;
		for( int j=1; j<bins; j++ )	cnt[j]=cnt[j]+cnt[j-1];
		for( int j=n-1; j>=0; j-- )	B[--cnt[(A[j]>>rtok)&mask]]=A[j];
		C = A; A = B; B = C;
	}
	if( A!=array ){
		for( int j=0; j<n; j++ )
			array[j] = temp[j];
		}
	delete []cnt;
	delete []temp;
}
 

最后一种排序算法是堆排序,我打算在下一篇文章总结堆的时候再提HeapSort。

  • 大小: 20.7 KB
分享到:
评论

相关推荐

    编程珠玑-中文第二版

    综上所述,《编程珠玑-中文第二版》不仅仅是一本关于编程技术的书籍,它更像是一位经验丰富的导师,引领着初学者进入编程的世界,并不断启发他们去探索更多的可能性。对于任何希望提升自己编程技能或对计算机科学感...

    编程珠玑-英文版+中文版+source code

    《编程珠玑》是计算机科学领域的一本经典之作,由Jon Bentley 编著,它以其深入浅出的方式探讨了程序设计中的许多核心问题。这本书主要关注如何有效地处理和存储大量数据,以及如何在时间和空间效率之间找到平衡。...

    编程珠玑 编程珠玑 编程珠玑 编程

    《编程珠玑》强调,优化不仅仅是关于速度,而是关于理解代码的运作方式和它对系统资源的影响。书中通过实际案例解释了如何通过分析和重构代码来减少内存消耗,提高程序响应速度,以及如何利用缓存机制来提升性能。 ...

    编程珠玑源码下载编程珠玑书后源代码

    《编程珠玑》是计算机科学领域的一本经典之作,由Jon Bentley 编著,它以其深入浅出的方式探讨了程序设计的问题和解决方案,尤其在数据结构、算法优化以及问题解决策略方面有着独到的见解。这本书的源代码是作者为了...

    编程珠玑 第2版(修订版)_编程珠玑修订_资料_

    《编程珠玑 第2版(修订版)》是一本深受程序员喜爱的经典著作,它不仅提供了丰富的编程实践经验,还深入探讨了程序设计的艺术与智慧。这本书的修订版更是在原版基础上进行了更新和完善,旨在帮助程序员提升编程技能,...

    编程珠玑 编程珠玑续

    《编程珠玑》和其续篇是两部深受程序员喜爱的经典著作,主要涵盖了程序设计、算法分析和数据结构等核心编程领域。这两本书以其深入浅出的讲解方式和丰富的实例,帮助读者提升编程技巧和解决问题的能力。 在《编程...

    编程珠玑(第二版)答案

    根据提供的标题“编程珠玑(第二版)答案”和描述“编程珠玑(第二版)答案”,我们可以推测出这是关于《编程珠玑》这本书的相关解答资料。《编程珠玑》是一本经典的计算机科学书籍,作者为Jon Bentley。本书旨在...

    编程珠玑之位图排序

    输入:一个最多包含n个正整数的文件,每个数都小于n,其中n=10^7,。如果在输入文件中有任何整数重复出现就是致命错误。没有其他数据与该整数关联。 输出:按升序排列的输入整数列表。 约束:最多有(大约)1MB的...

    编程珠玑.pdf

    2.3 拓扑排序 17 2.4 原理 20 2.5 习题 21 2.6 深入阅读 22 第3章 程序员的忏悔 23 3.1 二分搜索 24 3.2 选择算法 26 3.3 子程序库 28 3.4 原理 30 3.5 习题 31 第4章 自描述数据 33 4.1 名字—值对 33 4.2 记录来历...

    编程珠玑 Programming Pearls 第二版(中文版+源代码)

    2. **算法设计与分析**:《编程珠玑》强调了算法的重要性,不仅介绍了常见的排序和搜索算法,还讨论了如何在实际应用中选择和改进算法,以提高程序的运行效率。 3. **数据结构的应用**:书中详细讲解了各种数据结构...

    编程珠玑(PDF带目录)

    《编程珠玑》不仅涵盖了基础的排序、搜索算法,还深入探讨了更复杂的算法设计和分析。作者通过一系列精心挑选的问题,引导读者思考如何设计高效的算法,以及如何评估算法的性能。这些问题往往源于实际的编程场景,...

    编程珠玑 算法 数据结构

    通过对《编程珠玑》一书中关于算法和数据结构的学习,我们不仅可以了解到这些基础知识的重要性,还能掌握如何在实际场景中灵活运用它们来解决问题。无论是对于初学者还是有一定经验的程序员来说,《编程珠玑》都是一...

    编程珠玑源代码

    《编程珠玑源代码》是针对经典书籍《编程珠玑》第二版的源代码集合,主要涵盖C语言和C++编程。这本书以其独特的视角和深入浅出的讲解方式,深受程序员喜爱,尤其对于数据结构、算法和程序设计思维的提升有着重要的...

    编程珠玑(经典)

    《编程珠玑》是计算机科学领域的一本经典著作,作者是Jon Bentley。这本书以其深入浅出的讲解方式,探讨了程序设计中的一些核心问题,并提供了许多实用的编程技巧和算法,被誉为程序员的“智慧之石”。它不仅仅是一...

    编程珠玑总结笔记

    "编程珠玑总结笔记" 本资源笔记总结了编程珠玑中的一些重要知识点,包括优化程序、埃氏筛法和位图法等。 1. 打印出小于 10000 的素数 优化程序是编程珠玑中的一大主题,如何优化程序来提高效率是一个非常重要的...

    编程珠玑pdf+源代码

    1. **排序与搜索算法**:《编程珠玑》详细讲解了各种排序算法,如插入排序、选择排序、快速排序、归并排序以及堆排序,以及它们的性能分析。同时,书中也涉及到了查找算法,如二分查找和哈希表的应用,这些都是面试...

    编程珠玑 第二版 修订版 epub

    1. **编程艺术的探索**:《编程珠玑》不仅是一本关于编程技巧的书,更是一本关于编程艺术的书。它教导读者如何优雅地解决问题,如何通过优化算法提高程序性能,以及如何在面对复杂问题时保持清晰的思维。 2. **数据...

    编程珠玑(中文版).pdf

    ### 编程珠玑(中文版).pdf #### 知识点概览 《编程珠玑(中文版)》是一本经典的计算机科学著作,它作为《编程珠玑》系列的一部分,深入探讨了一系列对程序员非常重要的主题。这些知识点涵盖了算法、数据结构、...

Global site tag (gtag.js) - Google Analytics