华科大考研计算机系834大纲之数据结构(八)

本节大纲内容

  • 排序问题概述

  • 排序的基本概念

1、排序
排序是按关键字的非递减或非递增顺序对一组记录进行重新排列的操作。
2、排序的稳定性
当排序记录中的关键字都不相同时,则任何一个记录的无序序列经过排序后得到的结果唯一;反之,当待排序序列中存在两个或两个以上的关键字相等的记录时在,则排序所得的结果就不唯一了。

假设在待排序的文件中,存在两个具有相同关键字的记录R(i)与R(j),其中R(i)位于R(j)之前。在用某种排序法排序 之后,R(i)仍位于R(j)之前,则称这种排序方法是稳定的;否则,称这种排序方法是不稳定的。

注意:
排序算法的稳定性是针对所有记录而言的,也就是说,在所有的待排序记录中,只要有一组关键字的实例不满足稳定性的要求,则该排序算法就是不稳定。虽然稳定的排序算法和不稳定的排序算法排序结果不同,但不能说不稳定的排序算法就是不好的,它们各有各的适用场景。

3、内部排序和外部排序

由于待排序记录的数量不同,使得排序过程中的数据所占用的存储设备会有所不同。根据在排序过程中记录所占的存储设备,可将排序方法分为两大类:
一:内部排序,指的是待排序记录全部存放在计算机内存中进行排序的过程
二:外部排序,指的是待排序记录的数量很大,以致于内存一次不能容纳全部记录,在排序过程中尚需对外存进行访问的排序过程。

  • 内部排序方法的分类

内部排序的过程是一个逐步扩大记录的有序序列长度的过程。在排序的过程中,可以将排序记录区分为两个区域:有序序列区和无序序列区
使有序序列中记录的数目增加一个或几个的操作称为一趟排序。
根据逐步扩大记录有序序列长度的原则不同,可以将内部排序分为以下几类。
(1)插入类:直接插入排序、折半插入排序、希尔排序
(2)交换类:冒泡排序、快速排序
(3)选择类:简单选择排序、树形选择排序和堆排序
(4)归并类:2-路归并排序
(5)分配类:基数排序

  • 待排序记录的存储方式

(1)顺序表
(2)链表
(3)待排序记录本身存储在一组地址连续的存储单元内,同时令设一个指示各个记录存储位置的地址向量,在排序的过程中不移动记录本身,而移动地址向量中这些记录的“地址”,在排序结束之后再按照地址向量中的值调整记录的存储位置。

待排序记录的数据类型定义:

#define MAXSIZE 20
typedef int KeyType;
typedef struct{
	KeyType key;
	InfoType otherinfo;
}RedType;
typedef struct{
	RedType r[MAXSIZE+1];
	int length;
}SqList;
  • 排序算法效率的评价指标

(1)执行时间(时间复杂度)

对n个记录排序,所需比较关键字的次数;最好情况;最坏情况;平均情况
对n个记录排序,所需移动记录的次数;最好情况;最坏情况;平均情况

(2)辅助空间(空间复杂度)

排序过程中,除文件中的记录所占的空间外, 所需的辅助存储空间的大小

  • 插入排序法

基本思想:
每一趟将一个待排序的记录,按其关键字的大小插入到已经排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。

  • 直接插入排序

算法步骤:

1、设待排序的记录存放在数组r[1…n]中,r[1]是一个有序序列。
2、循环n-1次,每次使用顺序查找法,查找r[i]在已排好序的序列r[1…i-1]中的插入位置,然后将r[i]插入,直到将r[n]插入到前n-1个序列中,最后得到一个表长为n的有序序列。

算法描述:

void InsertSort(SqList &L){
	for(i=2;i<L.length;++i){
		if(L.r[i].key<L.r[i-1].key){
			L.r[0]=L.r[i];
			L.r[i]=L.r[i-1];
			for(j=i-2;L.r[0].key<L.r[j].key;--j){
				L.r[j+1]=L.r[j];
			}
			L.r[j+1]=L.r[0];
		}
	}
}

算法分析:
(1)时间复杂度
从时间上看,排序的基本操作为:比较两个关键字的大小和移动记录。

在最好的情况下:关键字本就正序。则每个关键字比较1次即可,不需要移动。总的比较次数为n-1。

在最坏的情况下:关键字逆序。则每个关键字要比较i次(前i-1个记录加下标为0的位置的哨兵),移动i+1次(开始时将待插入记录移动到哨兵位,最后找到插入位置,再将哨兵移到插入位置,还有前i-1个记录的移动)。总的关键字比较次数KCN和记录移动次数RMN达到最大值,分别为:
K C N = ∑ i = 2 n i = ( n + 2 ) ( n − 1 ) / 2 ≈ n 2 / 2 KCN=\sum_{i=2}^ni=(n+2)(n-1)/2≈n^2/2 KCN=i=2ni=(n+2)(n1)/2n2/2
R M N = ∑ i = 2 n i + 1 = ( n + 4 ) ( n − 1 ) / 2 ≈ n 2 / 2 RMN=\sum_{i=2}^ni+1=(n+4)(n-1)/2≈n^2/2 RMN=i=2ni+1=(n+4)(n1)/2n2/2

若待排序序列中出现各种可能排列的概率相同,则可取上述最好情况和最坏情况的平均情况。在平均情况下,直接插入排序关键字的比较次数和记录移动次数均约为 n 2 / 4 n^2/4 n2/4

由此,直接插入排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2)

(2)空间复杂度
直接插入排序只需要一个记录的辅助空间,所以空间复杂度为 O ( 1 ) O(1) O(1)

算法特点:
(1)稳定排序
(2)简便,容易实现
(3)适用于链式存储结构
(4)当n记录较大时,时间复杂度较高,不宜采用

例如:

  • 折半插入排序

直接插入排序是利用顺序查找法查找应该插入的位置,如果采用折半查找应该插入的位置,由此可以引出折半插入排序。

算法描述:

void BInsertSort(Sqlist &L){
	for(i=2;i<=L.length;++i){
		L.r[0]=L.r[i];
		low=1;
		high=i-1;
		while(low<=high){
			m=(low+high)/2;
			if(L.r[0].key<L.r[m].key)
				high=m-1;
			else low=m+1;
		}
		for(j=i-1;j>=high+1;--j)
			L.r[j+1]=L.r[j];
		L.r[high+1]=L.[0];
	}
}

算法分析:
(1)时间复杂度

在平均情况下,折半插入排序仅减少了关键字的比较次数,而记录的移动次数不变。因此,折半插入排序的时间复杂度仍为 O ( n 2 ) O(n^2) O(n2)

(2)空间复杂度

和直接插入排序一样,只需要一个记录的辅助空间,所以空间复杂度为 O ( 1 ) O(1) O(1)

算法特点:

(1)稳定排序
(2)因为要折半查找,所以只能用于顺序结构
(3)适合初始记录无序、n较大的情况

  • 希尔排序(Shell)

希尔排序又称缩小增量排序,是插入排序的一种。直接插入排序,当待排序的记录个数较少且待排序序列基本有序时,效率较高。希尔排序基于以上两点,从“减少记录个数”和“序列基本有序”两个方面对直接插入排序进行改进。

算法步骤:

希尔排序实质上是采用分组插入的方法。现将整个待排序序列分割成几组,从而减少参与直接插入排序的数据量,对每组分别进行直接插入排序,然后增加每组的数据量,重新分组。这样经过几次分组排序后,整个序列就“基本有序”了,再对全体数据进行一次直接插入排序。

希尔对记录的分组,不是简单地“逐段分割”,而是将相隔某个“增量”的记录分为一组。
(1)第一趟取增量 d 1 ( d 1 < n ) d_1(d_1<n) d1(d1<n)把全部记录分成 d 1 d_1 d1个组,所有间隔为 d 1 d_1 d1的记录分在同一组,在各个组中进行直接插入排序。
(2)第二趟选取增量 d 2 ( d 2 < d 1 ) d_2(d_2<d_1) d2(d2<d1),重复上述分组和排序。
(3)以此类推,直到所取的增量 d t = 1 d_t=1 dt=1,所有记录在同一组进行直接插入排序为止

例如:已知待排序序列的关键字序列为{49,38,65,97,76,13,27,49_,55,04},增量选取5、3、1

希尔排序过程:

算法分析:
(1)时间复杂度
只能说它比直接插入排序提高了效率,但要具体分析则过于复杂,无法得出结果。只能得出局部的结论。
在大量的实验基础上推出:当n在某个特定的范围内,希尔所需的比较次数和移动次数约为 n 1.3 n^{1.3} n1.3,当n趋于无穷大时,可减少到 n ( log ⁡ 2 n ) 2 n(\log_2 n)^2 n(log2n)2
(2)空间复杂度
从空间上来看,希尔排序的空间复杂度为 O ( 1 ) O(1) O(1)

算法特点:
(1)不稳定排序
(2)只能用于顺序结构
(3)增量序列有各种取法,但应该取质数,并最后的一个增量必须为1。
(4)适用初始记录无序、n较大的情况

  • 交换排序法

基本思想:两两比较待排序记录的关键字,一旦发现两个记录不能满足次序要求则进行交换,直到整个序列全部满足要求为止。

  • 冒泡排序

冒泡排序是一种最简单的交换排序方法,它通过两两比较相邻的关键字,如果发生逆序则交换,从而使关键字最小的记录上浮,或者使关键字最大的记录下沉。

例如:

算法分析:
(1)时间复杂度
最好情况:只需进行一趟排序,在排序过程中进行n-1次关键字的比较,且不移动记录。

最坏情况:需进行n-1趟排序,总的关键字比较次数KCN和移动次数RMN(每次交换都要移动3次记录)分别为:

K C N = ∑ i = n 2 ( i − 1 ) = ( n ) ( n − 1 ) / 2 ≈ n 2 / 2 KCN=\sum_{i=n}^2(i-1)=(n)(n-1)/2≈n^2/2 KCN=i=n2(i1)=(n)(n1)/2n2/2
R M N = 3 ∑ i = n 2 ( i − 1 ) = 3 n ( n − 1 ) / 2 ≈ 3 n 2 / 2 RMN=3\sum_{i=n}^2(i-1)=3n(n-1)/2≈3n^2/2 RMN=3i=n2(i1)=3n(n1)/23n2/2

所以在平均情况下,冒泡排序关键字的比较次数和记录移动次数分别约为 n 2 / 4 n^2/4 n2/4 3 n 2 / 4 3n^2/4 3n2/4时,时间复杂度为 O ( n 2 ) O(n^2) O(n2)

(2)空间复杂度

只有两个记录交换位置时需要一个辅助空间用作暂存记录,所以空间复杂度为 O ( 1 ) O(1) O(1)

算法特点
(1)稳定排序
(2)可用于链式存储结构
(3)移动次数较多,算法平均时间性能比直接插入排序差。当记录无序,n较大时,不宜用。

  • 快速排序

快速排序是由冒泡排序改进而得的。在冒泡排序中一次交换只能消除一个逆序。如果能通过两个记录的一次交换,消除多个逆序,则会大大加快排序的速度。快速排序的一次交换可消除多个逆序。

例如:对于给定序列{49,38,65,97,76,13,27,49_}

快速排序的过程可以递归进行,其递归树为:

算法描述:

int Partition(SqList &L,int low,int high){
	//此函数功能是对顺序表L中的子表r[low..high]进行一趟排序,并返回中心位置
	L.r[0]=L.r[low];		//用子表的第一个记录作为中心位置(枢轴)
	pivotkey=L.r[low].key;	//枢轴记录关键字保存在pivotkey中
	while(low<high){		//从表的两端交替扫描
		while(low<high&&L.r[high].key>=pivotkey)
			--high;
		L.r[low]=L.r[high];	//将比枢轴小的移到low端
		while(low<high&&L.r[low].key<=pivotkey)
			++low;
		L.r[high]=L.r[low];	//将比枢轴大的移到high端
	}
	L.r[low]=L.r[0];		//把枢轴恢复到序列中
	return low;				//返回枢轴的位置
}
void QSort(SqList &L,int low,int high){
	//调用前置初值:low=1,high=L.length;
	//对顺序表的子序列L.r[low..high]快速排序
	if(low<high){
		pivotloc=Partition(L,low,high);		//调用函数进行一趟快速排序
		QSort(L,low,pivotloc-1);			//对左子表递归排序
		QSort(L,pivotloc+1,high);			//对右子表递归排序
	}	
}
void QuickSort(SqList &L){
	QSort(L,1,L.length);
}

算法分析:

(1)时间复杂度

最好情况: T ( n ) < = n log ⁡ 2 n + n T ( 1 ) ≈ O ( n log ⁡ 2 n ) T(n)<=n\log_2n+nT(1)≈O(n\log_2n) T(n)<=nlog2n+nT(1)O(nlog2n)
最坏情况: K C N = ∑ i = 1 n − 1 n − i = n ( n − 1 ) / 2 ≈ n 2 / 2 KCN=\sum_{i=1}^{n-1}n-i=n(n-1)/2≈n^2/2 KCN=i=1n1ni=n(n1)/2n2/2
平均情况: O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n)

(2)空间复杂度

最好情况: O ( log ⁡ 2 n ) O(\log_2n) O(log2n)
最坏情况: O ( n ) O(n) O(n)

算法特点:
(1)记录非顺序次的移动导致排序方法是不稳定的
(2)适合顺序结构,很难用于链式结构
(3)适用初始记录无序,n较大的情况

  • 选择排序法

基本思想:每一趟从待排序的记录中选出关键字最小的记录,按顺序放在已排序的记录序列的最后,直到全部排完为止。

  • 简单选择排序

例如:已知待排序的关键字序列为{49,38,65,97,49_,13,27,76}

算法描述:

void	SelectSort(SqList &L){
	for(i=1;i<L.length;++i){
		k=i;
		for(j=i+1;j<=L.length;++j)
			if(L.r[j].key<L.r[k].key)
				k=j;
		if(k!=i){
			t=L.r[i];
			L.r[i]=L.r[k];
			L.r[k]=t;
		}
	}
}

算法分析:

(1)时间复杂度: O ( n 2 ) O(n^2) O(n2)
(2)空间复杂度: O ( 1 ) O(1) O(1)

算法特点:

(1)就选择排序方法本身,它是稳定的排序方法。
(2)可用于链式存储
(3)移动记录次数较少,当每一记录占用空间较多时,比较适宜

  • 树形选择排序

又称锦标赛排序,是一种按锦标赛思想进行选择排序的方法。
例如:

特点:从叶子结点开始两两比较选出最小值,那么树根是叶子结点中最小的。

选出最小的13后,把13变为♾,那么又可选出次最小,以此类推,可以完成从小到大的排序。

时间复杂度: O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n)
空间复杂度:辅助空间较多,这是它的缺点。为了改进这个缺点,因此引出堆排序。

  • 堆排序

堆排序是一种树形选择排序,在排序过程中,将待排序的记录r[1…n]看成一棵完全二叉树的顺序结构,利用完全二叉树中双亲结点和孩子之间的关系,在当前无序的序列中,选择关键字最大或最小的记录。

首先给出堆的定义

n个元素序列{k_1,k_2,…k_n},当且仅当满足以下条件时
(1) k i > = k 2 i k_i>=k_{2i} ki>=k2i k i > = k 2 i + 1 k_i>=k_{2i+1} ki>=k2i+1,大根堆
(2) k i < = k 2 i k_i<=k_{2i} ki<=k2i k i < = k 2 i + 1 k_i<=k_{2i+1} ki<=k2i+1,小根堆

例如:

大根堆:

小根堆:

如果要实现堆排序,就要解决以下两个问题:
(1)建处堆:将无序序列建成一个堆
(2)调整堆:去掉堆顶元素,重新调整为一个新的堆

1、先介绍调整堆:

例如:大根堆调整

上图已选出最大值97,开始去掉堆顶元素。
将97和最后一个元素38交换。

交换后,发现除了根结点外,其他都满足大根堆的特点,由此只需从上到下进行一条路的调整即可。
首先,38与76、65比较,调整:

再检查38与49_、49比较,调整:

到此,调整完毕,97不必比较。

上述过程就像筛子一样,把较小的关键字逐层筛下去,而将较大的关键字选上来。

2、建初堆

例如:一个序列{49,38,65,97,76,13,27,49_}

无序序列:

调整为大根堆:

从最后一个非终端结点开始筛选,逐层看,97为最后一个非终端结点
(1)97与49_比较,不必调整
(2)找到非终端结点65,不必调整
(3)找到非终端结点38,需要调整

再次从最后一个非终端结点开始检查,
(1)38与49_,调整:

(2)检查65,不必调整
(3)检查97,不必调整
(4)检查49,调整:

再次从最后一个非终端结点开始检查,
(1)49_,不必调整
(2)65,不必调整
(3)49,调整

到此已经建成大根堆

3、完整算法的实现过程:

例如:序列{49,38,65,97,76,13,27,49_}

建立初始大根堆:

97与38交换(根与最后一个元素交换,逐层,从左至右的顺序)

交换后,开始第一趟排序的调整:

第一趟排序结束,再次交换:

交换后,开始第二趟排序的调整:

第二趟排序结束,再次交换:

交换后,开始第三趟排序的调整:

第三趟排序结束,再次交换

交换后,开始第四趟排序的调整:

第四趟排序结束,再次交换:

交换后,开始第五趟排序的调整:

第五趟排序结束,再次交换:

交换后,开始第六趟排序的调整:

第六趟排序结束,再次交换:

此时已经只剩下根结点,算法结束。

算法分析:

(1)时间复杂度: O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n)
(2)空间复杂度: O ( 1 ) O(1) O(1)

算法特点:

(1)不稳定
(2)只能用于顺序结构
(3)初始堆建立所需比较次数较多,因此记录较少时不宜采用,当记录较多时,较为高效。

  • 归并排序法

归并排序就是将两个或两个以上的有序表合并成一个有序表的过程。将两个有序表合并成一个有序表的过程称为2-路归并。

例如:已知序列{49,38,65,97,76,13,27}

算法分析:
(1)时间复杂度: O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n)
(2)空间复杂度: O ( n ) O(n) O(n)

算法特点:
(1)稳定排序
(2)可用于链式结构

  • 基数排序法

前述各类排序的方法都是建立在关键字比较的基础上,而分配类排序不需要比较关键字的大小,它是根据关键字中各位的值,通过对待排序记录进行若干趟“分配”与“收集“来实现排序的,是一种借助于多关键字排序的思想对单关键字排序的方法。
基数排序就是典型的分配类排序。

  • 多关键字的排序

例如:扑克牌排序

扑克牌有花色和面值,相当于有两个关键字。如果规定“花色”的地位高于“面值”。那么比较时,先比较花色,后比较面值。

(1)最高位优先:先按“花色”分为有次序的4堆,然后再对每一堆进行“面值”排序
(2)最低位优先:先按“面值”分为13堆,然后将13堆自小到大排好。然后对每堆按“花色“排好。最后再重新对这些牌按“花色”分成4堆,再把4堆排序。

  • 链式基数排序

例如:序列{278,109,063,930,589,184,505,269,008,083}

1、对其按各位进行分类,然后把个位相同的收集到一起。按原序列顺序收集,个位链表用尾插法。

一趟收集后得到序列为:930,063,083,184,505,278,008,109, 589,269

2、对得到的新序列按十位分类,进行二次收集。

二趟收集后得到序列为:505,008,109,930,063,269,278,083, 184,589

3、对得到的新序列按百位分类,进行三次收集。

三趟收集后得到序列为:008,063,083,109,184,269,278,505, 589,930

算法分析:
(1)时间复杂度:对于n个记录(假设每个记录含d个关键字,每个关键字取值范围为rd个值)进行链式基数排序时,每一趟分配的时间复杂度为O(n),每一趟收集的时间复杂度为O(rd),整个排序过程需进行d趟分配和收集,所有时间复杂度为 O ( d ( n + r d ) ) O(d(n+rd)) O(d(n+rd))
(2)空间复杂度: O ( n + r d ) O(n+rd) O(n+rd)

算法特点:

(1)稳定排序
(2)可以链式结构,也可以顺序结构
(3)基数排序的使用条件有严格要求:需要知道各级关键字的主次关系和各级关键字的取值范围。

  • 小结

排序方法最好情况最坏情况平均情况空间复杂度稳定性
直接插入排序 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定
折半插入排序 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定
希尔排序 O ( n 1.3 ) O(n^{1.3}) O(n1.3) O ( 1 ) O(1) O(1)不稳定
冒泡排序 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定
快速排序 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n) O ( n 2 ) O(n^2) O(n2) O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n) O ( log ⁡ 2 n ) O(\log_2n) O(log2n)不稳定
简单选择排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)不稳定
堆排序 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n) O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n) O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n) O ( 1 ) O(1) O(1)不稳定
归并排序 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n) O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n) O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n) O ( n ) O(n) O(n)稳定
基数排序 O ( d ( n + r d ) ) O(d(n+rd)) O(d(n+rd)) O ( d ( n + r d ) ) O(d(n+rd)) O(d(n+rd)) O ( d ( n + r d ) ) O(d(n+rd)) O(d(n+rd)) O ( n + r d ) O(n+rd) O(n+rd)稳定
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值