数据结构(六)之各种排序算法总结(附源代码)

对于本文介绍的排序算法,我们假设整个排序工作能够在主存中完成,因此,元素的个数相对来说比较小(小于10的6次方)。当然,不能在主存中完成而必须在磁盘或磁带上的排序叫做外部排序,将在其它博文介绍。

一. 插入排序

  1. 基本思想: 
    对于N个元素,插入排序由N-1趟排序组成。对于P=1趟到P=N-1趟,插入排序保证从位置0到位置P上的元素为已排序状态。插入排序利用了这样的事实:位置0到位置P-1上的元素已排过序。
  2. 排序过程 
    【示例】: 
    [初始关键字] 49 38 65 97 76 13 27 49 
    J=1(49) [49] 38 65 97 76 13 27 49 
    J=2(38) [38 49] 65 97 76 13 27 49 
    J=3(65) [38 49 65] 97 76 13 27 49 
    J=4(97) [38 49 65 97] 76 13 27 49 
    J=5(76) [38 49 65 76 97] 13 27 49 
    J=6(13) [13 38 49 65 76 97] 27 49 
    J=7(27) [13 27 38 49 65 76 97] 49 
    J=8(49) [13 27 38 49 49 65 76 97]
  3. 源代码

    typedef int ElementType;
    
    //插入排序,默认从小到大排序
    void InsertionSort(ElementType A[], int N)
    {
    	ElementType Tmp;
    	int i, j;
    	//进行N-1趟遍历
    	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;
    	}
    }

  4. 复杂度分析 
    插入排序平均情况为O(N^2)。如果输入数据已预先排序,那么运行时间为O(N)。

二. 希尔排序

  1. 基本思想 
    也称递减增量排序算法。通过比较相距一定间隔的元素来工作,各趟比较所用的距离随着算法的进行而减小,直到只比较元素的最后一趟排序位置。希尔排序使用一个序列h1, h2, ..., ht,叫做增量序列。增量序列的一种流行的选择是使用shell建议的序列:ht=[N/2]和hk=[h(k+1)/2],该方法实质上是一种分组插入方法。
  2. 源代码

    void ShellSort(ElementType A[], int N)
    {
    	ElementType Tmp;
    	int i, j;
    	int Increment;
    
    	//Increment表示增量序列的取值,初始值为N/2,每次遍历减半直到1
    	for( Increment = N/2; Increment > 0; Increment/=2 )
    	{
    		//每次遍历就是增量为Increment的插入排序
    		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;
    		}	
    	}
    }

  3. 复杂度分析 
    希尔排序的平均情形比O(N^2)好一些,最坏情形为O(N^2)。 
    Hibbard提出形如1,3,7...,2^k-1的增量系列,平均情形为O(N^(5/4)),最坏情形为O(N^(3/2))。 Sedgewick提出了几种增量序列,其最坏的为O(N^(4/3)),平均运行时间为O(N^(7/6))。 
    希尔排序的性能在实践中是完全可以接受的,即使是对于数以万计的N仍是如此。编程的简单特点使得它成为对适度地大量的输入数据经常选用的算法。

三. 堆排序

  1. 基本思想 
    通过使用max堆(二叉堆),第一步以线性时间建立一个堆,然后通过将堆中的最后元素与第一个元素交换,缩减堆的大小进行下滤,来执行N-1次DeleteMax操作。不像二叉堆,当时数据是在数组下标1处开始,而此处堆排序的数组包含位置0处的数据。
  2. 源代码

    #define LeftChild(i) (2*i+1)
    //下滤
    void PercDown(ElementType A[], int N, int i)
    {
    	ElementType Tmp;
    	int Child;
    	
    	Tmp = A[i];
    	for( Tmp = A[i]; LeftChild(i) < N; i = Child )
    	{
    		Child = LeftChild(i);
    		if( Child < N-1 && A[Child+1] > A[Child])
    			Child++;
    		if( A[Child] > Tmp)	
    			A[i] = A[Child];
    		else
    			break;
    	}
    	A[i] = Tmp;
    }
    
    void HeapSort(ElementType A[], int N)
    {
    	ElementType Tmp;
    	int i;
    	
    	//建立一个堆
    	for( i = N/2; i >= 0; --i)
    		PercDown(A, N, i);
    	for( i = N-1; i > 0; --i)
    	{
    		//交换A[0]和A[i]的值
    		Tmp = A[0];
    		A[0] = A[i];
    		A[i] = Tmp;
    		//重新排序
    		PercDown(A, i, 0);
    	}
    }

  3. 复杂度分析 
    平均复杂度和最坏情况复杂度都为O(NlogN),然而,在实践中它却慢于使用Sedgewick增量序列的希尔排序。

四. 归并排序

  1. 基本思想 
    这个算法中基本的操作是合并两个已排序的表。归并排序算法可以用递归实现,递归地将前半部分数据和后半数据各自归并排序;得到排序后的两部分数据,然后使用上面描述的合并算法再将这两部分合并到一起。
  2. 源代码

    void MSort(ElementType A[], ElementType Tmp[], int N, int Left, int Right);
    void Merge(ElementType A[], ElementType Tmp[], int N, int LeftStart, int RightEnd);
    
    void MergeSort(ElementType A[], int N)
    {
    	ElementType *Tmp;
    	//申请一个原来数组大小的临时数组
    	Tmp = (ElementType *)malloc(N * sizeof(ElementType));
    	if( Tmp != NULL )
    	{
    		MSort(A, Tmp, N, 0, N-1);
    		free(Tmp);		//不要忘了释放内存
    	}
    }
    //递归归并程序
    void MSort(ElementType A[], ElementType Tmp[], int N, int Left, int Right)
    {
    	int Center;
    
    	if(Left < Right)
    	{
    		Center = (Left + Right) / 2;
    		MSort(A, Tmp, N, Left, Center);
    		MSort(A, Tmp, N, Center + 1, Right);
    		Merge(A, Tmp, N, Left, Right);
    	}
    }
    
    //合并两个数组
    void Merge(ElementType A[], ElementType Tmp[], int N, int LeftStart, int RightEnd)
    {
    	int LeftEnd, RightStart, LeftBound, P;
    
    	LeftEnd = (LeftStart + RightEnd) / 2;
    	RightStart = LeftEnd + 1;
    	LeftBound = P = LeftStart;
    	while(LeftStart <= LeftEnd && RightStart <= RightEnd)
    		if( A[LeftStart] <= A[RightStart])
    			Tmp[P++] = A[LeftStart++];
    		else
    			Tmp[P++] = A[RightStart++];
    
    	while(LeftStart <= LeftEnd)
    		Tmp[P++] = A[LeftStart++];
    	while(RightStart <= RightEnd)
    		Tmp[P++] = A[RightStart++];
    
    	while( RightEnd >= LeftBound)	
    	{
    		A[RightEnd] = Tmp[RightEnd];
    		RightEnd--;
    	}
    }

  3. 复杂度分析 
    归并排序以O(NlogN)最坏情形运行时间运行,而所使用的比较次数几乎是最优的。虽然归并排序的运行时间是O(NlogN),但是它很难用于主存排序,主要问题在于合并两个排序的表需求线性附加内存,在整个算法中还要花费将数据拷贝到临时数据再拷贝回来这样一些附加的工作,其结果严重放慢了排序的速度。

五. 快速排序

  1. 基本思想 
    不断寻找一个序列的枢纽元,然后对枢纽元左右的序列递归的进行排序,直至全部序列排序完成,使用了分治的思想。 
    选取枢纽元:一种错误的方法是选择第一个元素,可能输入是预排序或者是反序的。三数中值分割法是常用的,使用左端、右端和中心位置上的三个元素的中值作为枢纽元。对于很小的数组(N远远小于20),快速排序不如插入排序好。一种好的截止范围是N=10。
  2. 源代码

    void QSort(ElementType A[], int Left, int Right);
    ElementType Median3(ElementType A[], int Left, int Right);
    
    void QuickSort(ElementType A[], int N)
    {
    	QSort(A, 0, N-1);
    }
    
    void Swap(int *a, int *b)
    {
    	int Tmp;
    
    	Tmp = *a;
    	*a = *b;
    	*b = Tmp;
    }
    
    ElementType Median3(ElementType A[], int Left, int Right)
    {
    	int Center;
    
    	Center = (Left + Right) / 2;
    	if( A[Left] > A[Center] )
    		Swap(&A[Left], &A[Center]);
    	else if ( A[Left] > A[Right] )
    		Swap(&A[Left], &A[Right]);
    	else if ( A[Center] > A[Center] )
    		Swap(&A[Center], &A[Center]);
    
    	Swap(&A[Center], &A[Right-1]);
    	return A[Right-1];
    }
    
    #define Cutoff (3)
    void QSort(ElementType A[], int Left, int Right)
    {
    	ElementType Pivot;
    	int i, j;
    
    	if( Left + Cutoff <= Right)
    	{
    		Pivot = Median3(A, Left, Right);
    		i = Left;
    		j = Right -1;
    		while(1)
    		{
    			while( A[++i] < Pivot);
    			while( A[--j] > Pivot);
    			if( i < j )
    				Swap( &A[i], &A[j]);
    			else
    				break;
    		}
    		Swap(&A[i], &A[Right-1]);
    
    		QSort(A, Left, i-1);
    		QSort(A, i+1, Right);
    	}
    	//小于三个元素则采用插入排序
    	else
    		InsertionSort(A + Left, Right - Left + 1);
    }

  3. 复杂度分析 
    快速排序是在实践中最快的已知排序算法,它的平均运行时间是O(NlogN)。它的最坏情形的性能是O(N^2)。

总结

对于最一般的内部排序应用程序,选用的方法不是插入、希尔排序,就是快速排序,它们选用主要是根据输入的大小来决定的。高度优化的快速排序算法即使对于很少的输入数据也能和希尔排序一样快。如果需要对一些大型的文件排序,那么快速排序则是应选用的方法。希尔排序有些小缺陷,但可以接受,特别是需要简单明了的时候,希尔排序最坏的情况也只不过是O(N^(4/3)),平均是O(N^(7/6))。堆排序比希尔排序慢,尽管它是一个带有明显紧凑内循环的O(NlogN)算法,因为堆排序为了移动数据要进行两次比较。插入排序只用在小的或是非常接近排好序的输入数据上。而归并排序性对于主存排序不如快速排序那么好,但合并确是外部排序的中心思想。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1、冒泡排序属于稳定排序,是一种借助“交换”进行排序的方法。首先要将第一个记录的关键字和第二个记录的关键字进行比较,若为逆序,则将两个记录交换之,然后比较第二个记录与第三个记录的关键字,以此类推,直至第n-1个记录与第n个记录的关键字进行比较为止,这一过程称为第一趟冒泡排序,其结果使得关键字最大的记录被安置在最后一个记录的位置上;然后进行第二趟冒泡排序,对前N-1个记录进行同样操作;以此类推,直到在一趟排序过程中没有进行过交换记录的操作为止。 2、直接插入排序属于稳定的排序,每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。第一趟将待比较的数值与它的前一个数值进行比较,当前一数值比待比较数值大的情况下继续循环比较,依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程,结束该次循环。 3、快速排序属于不稳定排序,是对起泡排序的一种改进。它的基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。假设待排序的序列为{R.[s],R.[s+1],…….,R.[t]},首先任意选取一个记录,然后按下述原则从新排序记录:将关键字较他小的记录都安置在他的位置之前,将所有关键字较他大的记录都安置在他的位置后面。由此可以该“枢轴”记录最后所落的位置i作为分界线,将序列{R[s],R[s+1]…….R[t]}分割成两个子序列{R[s],R[s+1]…..R[i-1]}和{R[i+1]……R[t]},这个过程称作一趟快速排序。一趟快速排序的具体做法是:设两个指针low和high,它们的初值分别指向数组第一个数据和最后一个数据,将枢轴记录暂存在R[0]的位置上排序过程中只作R[low]或R[high]的单向移动,直至一趟排序结束后再将枢轴记录移至正确位置上。 4、简单选择排序属于不稳定排序,基本思想是,每一趟在n-i+1(i=1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。第i趟简单选择排序是指通过n-i次关键字的比较,从n-i+1个记录中选出关键字最小的记录,并和第i个记录进行交换。共需进行n-1趟比较,直到所有记录排序完成为止。例如:进行第i趟选择时,从当前候选记录中选出关键字最小的k号记录,并和第i个记录进行交换。 5、希尔排序属于不稳定排序,也是一种属插入排序类,它的基本思想是:先将整个待排记录序列分割称为若干个子序列分别进行直接插入排序,待整个序列中记录“基本有序”时,再对全体记录进行一次直接插入排序。希尔排序的一个特点是:子序列的构成不是简单的“逐段分割”,而是将相隔某个“增量”的记录组成一个子序列。 6、堆排序属于不稳定排序,它的基本思想是,先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区,再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key;由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆,然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n- 2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。直到无序区只有一个元素为止。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值