排序Sort

基本概念

定义

排序:将一组杂乱无章的数据按一定规律顺次排列起来。即,将无序序列排成一个有序序列(由小到大或由大到小)的运算。如果参加排序的数据结点包含多个数据域,那么排序往往是针对其中某个域而言。

排序方法分类

✳按数据存储介质分类

内部排序:数据量不大、数据在内存,无需内外存交换数据。

外部排序:数据量较大、数据在外存(文件排序)。

  外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,显然外部排序要复杂得多。

✳按比较器个数分类

串行排序:(单处理机)(同一时刻比较一对元素)。

并行排序:(多处理机)(同一时刻比较多对元素)。

✳按主要操作分类

比较排序:用比较的方法,如插入排序、交换排序、选择排序、归并排序。

基数排序:不比较元素的大小,仅仅根据元素本身的取值确定其有序位置。

✳按辅助空间分类

原地排序:辅助空间用量为O(1)的排序方法。(所占的辅助存储空间与参加排序的数据量大小无关)。

非原地排疗:辅助空间用量超过O(1)的排序方法。

✳按稳定性分类

稳定排序:能够使任何数值相等的元素,排序以后相对次序不变。

非稳定性排序:不是稳定排序的方法。

✳按自然性分类

自然排序:输入数据越有序)排序的速度越快的排序方法。

非自然排序:不是自然排序的方法。

排序方法

以如下图的顺序表为例,来讨论各种排序方法。

✳插入排序 

基本思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。

基本操作:1.在有序序列中插入一个元素,保持序列有序,有序长度不断增加。2.起初,a[0]是长度为1的子序列。然后,逐一将a[1]至a[n-1]插入到有序子序列中。3.在插入a[i]前,数组a的前半段(a[0] ~a[i-1])是有序段,后半段(a[i]~a[n-1])是停留于输入次序的"无序段”。4.插入a[i]使a[0]~a[i-1]有序,也就是要为a[i]找到有序位置j (0≤j≤i),将a[i]们插入在a[j]的位置上。

 定位插入位置:直接插入排序,二分插入排序,希尔排序。

直接插入排序

采取顺序查找法查找插入位置,利用L.r[0]作为哨兵可以减少比较次数。

代码如下:

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

实现排序的基本操作有两个

1.比较 2.移动 

 最好的情况下,比较的次数为n-1,移动的次数为0;最坏的情况下,比较的次数为(n+2)*(n-1)/2,移动的次数为(n+4)*(n-1)/2,故最好情况下,时间复杂度为O(n),最坏为O(n^2),平均时间复杂度为O(n^2)。空间复杂度为O(1),稳定性稳定。

折半插入排序

基本思想:查找操作时进行折半查找法。

void BlnsertSort(SqList& L) {
	int low, high,mid;
	for (int i = 2; i < L.length; i++) {
		L.r[0] = L.r[i];
		low = 1;
		high = i - 1;
		while (low <= high) {
			mid = (low + high) / 2;
			if (L.r[0].key < L.r[mid].key)
				high = mid - 1;
			else
				low = mid + 1;
		}
		for (int j = i - 1; j >= high + 1; j--)
			L.r[j + 1] = L.r[j];
		L.r[high + 1] = L.r[0];
		}
	}

折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快;

它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。在插入第i个对象时,需要经过Llog2i] + 1次关键码比较,才能确定它应插入的位置;

当n较大时,总关键码比较次数比直接插入排序的最坏情况要好得多,但比其最好情况要差;在对象的初始排列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行的关键码比较次数要少;

折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列。

1.减少了比较次数,但没有减少移动次数。

2.平均性能优于直接插入排序。

●时间复杂度为O(n2)

●空间复杂度为O(1)

●是一种稳定的排序方法

希尔排序

基本思想:先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录"基本有序”时,再对全体记录进行一次直接插入排序

基本操作:1.定义增量序列Dk: Dm>Dm-1>...>D1=1,2.对每个Dk进行"Dk-间隔”插入排序(k=M, M-1, ..1)。

特点:(1)一次移动,移动位置较大,跳跃式地接近排序后的最终位置。

           (2)最后一次只需要少量移动。

           (3) 增量序列必须是递减的,最后一个必须是1。

           (4)增量序列应该是互质的。

void ShellSort(SqList& L, int dlta[], int t) {
	for (int k = 0; k < t; k++) {
		Shellinsert(L, dlta[k]);
	}
}
void Shellinsert(SqList& L, int dk) {
	for (int i = dk + 1; i < L.length; i++) {
		if (L.r[i].key < L.r[i - dk].key) {
			L.r[0] = L.r[i];
			for (j = i - dk; j > 0 && (L.r[0] < L.r[j].key); j = j - dk)
				L.r[j + dk] = L.r[j];
			L.r[j + dk] = L.r[0];
		}
	}
}

希尔排序的算法效率与增量序列的取值有关。希尔排序是一个不稳定的排序。希尔排序的按经验公式来说,时间复杂度为O(n^1.25)~O(1.6n^1.25),空间复杂度为O(1)如何选取最佳的D序列,目前尚未解决

✳交换排序

冒泡排序

基本思想:每趟不断进行两两比较,并按前小后大的顺序交换。

如6个记录{21,25,49,25,16,8},用1,2,3,4,5,6表示序列第几个数,第一躺,1和2比较,2>1不变,然后2和3比较,3>2不变,然后3和4比较,3>4,因此交换49和25的位置,即第3个数变成25,第四个数变成49,然后4和5比,4>5,因此交换49与16的位置,然后5和6比,5>6,因此交换49和8的位置。这时候,最大的数49已经来到序列末尾,故第二趟不需要比较5和6,同理依次类推:

void bubble_sort(SqList& L) {
	int i, j, n;
	n = L.length;
	int temp;
	for (i = 1; i <= n-1; i++) {
		for (int j = 1; j < n - i; j++) {
			if (L.r[j + 1].key > L.r[j].key)
				temp = L.r[j + 1].key;
			L.r[j + 1].key = L.r[j].key;
			L.r[j].key = temp;
		}
	}
}

优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素。 

如何提高效率?一旦某一趟比较时不出现记录交换,说明已排好序了,就可以结束本算法。

改进算法:增加flag,若flag为1表示次躺发生交换,为0则未发生交换

void bubble_sort(SqList& L) {
	int i, j, n,flag;
	n = L.length;
	int temp;
	flag = 1;
	for (i = 1; i <= n-1&&flag==1; i++) {
		flag = 0;
		for (int j = 1; j < n - i; j++) {
			if (L.r[j + 1].key > L.r[j].key)
				flag = 1;
				temp = L.r[j + 1].key;
			L.r[j + 1].key = L.r[j].key;
			L.r[j].key = temp;
		}
	}
}

冒泡排序最好情况,比较次数为n-1,移动次数为0,最坏情况,比较次数为n*(n-1)/2,移动次数为3*n*(n-1)/2,因此冒泡排序最好情况时间复杂度为O(n),最坏情况为O(n^2),平均时间复杂度为O(n^2),空间复杂度为O(1),冒泡排序是稳定的。

快速排序

基本思想:任取一个元素(如:第一个)为中心(pivot:枢轴、中心点),所有比它小的元素一律前放, 比它大的元素一律后放,形成左右两个子表。对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个

具体实现:选定一个中间数作为参考,所有元素与之比较,小的调到其左边,大的调到其右边。

(枢轴)中间数:可以是第一个数、最后一-个数、最中间一个数、任选一个数等。 

  但这样的操作需要创建新的表,空间需求大,我们可以利用两个指针low,high来节约空间。

由于0号元素是空的,所以我们可以先将中间数放在0号位置,选取第一个数为中间数,此时low指针所指就是“空”的,然后比较high指针的数值,与49相等,则high--,再比较27,比49小,放在49右边,我们就可以放在low的位置,27放过去后。high位置空了,此时low++,此时来比较low所指的38,比49小,不变,low++,再比较65,比49大,移动到high所指位置,此时low所指又空了,high--,依此类推,可以用low,high所指空位置放数值。

 当low==high时候,就可以存放中间数49了。

①每一趟的子表的形成是采用从两头向中间交替式逼近法

②由于每趟中对各子表的操作都相似,可采用递归算法

算法表示:

 可以证明,平均计算时间是O(n\log_{2}n)。Qsort( ): O(\log_{2}n),Partition(): O(n)

实验结果表明:就平均计算时间而言,快速排序是我们讨论所有排序方法中最好的一个

快速排序不是原地排序:由于程序中使用了递归,需要递归调用栈的支持,而栈的长度取决于递归调用的深度。(即使不用递归, 也需要用用户栈)。在平均情况下:需要O(logn)的栈空间,最坏情况下:栈空间可达O(n)快速排序是一种不稳定排序。

    快速排序不适合原本有序或基本有序的记录顺序进行排序。划分元素的选取是影响时间性能的关键。输入数据次序越乱,所选划分元素值的随机性越好,排序速度越快,快速排序不是自然排序方法。无法改变最坏情况下的时间性能。即最坏情况下,快速排序的时间复杂性无法改变最坏情况下的时间性能。即最坏情况下,快速排序的时间复杂性总是O(n^2)。

✳选择排序 

简单选择排序

基本思想:在待排序的数据中选取一个最大(最小)的元素放在其最终的位置。

基本操作:1.首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记录交换。

                  2.再通过n-2次比较,从剩余的n-1个记录中找出关键字次小的记录,将它与第二个记录交换。

                  3.重复上述操作,共进行n-1趟排序后,排序结束。

 算法表示:

最好情况,移动0次,最坏情况,移动3*(n-1) 次,无论待排序数据处于什么状态,都需要比较n*(n-1)/2。简单选择排序是不稳定的。

堆排序

堆的定义:若n个元素序列{a1,a2,a3,……}满足ai<=a2i和ai<=a(2i+1)ai>=a2i和ai>=a(2i+1),则分别称为序列{a1,a2,a3,……}为小根堆大根堆

从堆的定义可以看出,堆实质上是满足如下性质的完全二叉树:二叉树中的任一非叶子结点均小于(大于)它的孩子结点

 堆排序:若在输出堆顶的最小值(最大值)后,使得剩余n- 1个元素的序列重又建成一个堆,则得到n个元素的次小值(次大值) ...如此反复,便能得到一个有序序列,这个过程称之为堆排序

如何在输出堆顶元素后,调整剩余元素为一个新的堆?

小根堆:

1.输出堆顶元素之后,以堆中最后一个元素替代之。

2.然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换。

3.重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选"。

例:

首先输出13,并以最后一个元素97代替之。

比较左右孩子,与其中最小者27进行交换,再比较左右孩子,与49进行交换,得到一个新的堆。  

算法表示:

如何由一个无序序列建成一个堆? 

  (1)单结点的二叉树是堆;

  (2)在完全二叉树中所有以叶子结点(序号i > n/2)为根的子树是堆。这样,我们只需依次将以序号为n/2,n/2- 1, .... 1的结点为根的子树均调整为堆即可。即:对应由n个元素组成的无序序列,“筛选” 只需从第n/2个元素开始

  (3)由于堆实质上是一个线性表,那我们可以用顺序存储一个堆。

例:以{49,38,65,97,76,13,27,49}为例,创建一个小根堆。

1.先建立一个完全二叉树。

2. 从最后一个非叶子结点开始,以此向前调整:

①调整从第n/2个元素开始,将以该元素为根的二叉树调整为堆;

②将以序号为n/2 - 1的结点为根的二叉树调整为堆;

③再将以序号为n/2 - 2的结点为根的二叉树调整为堆;

④再将以序号为n/2 - 3的结点为根的二叉树调整为堆;

   (4) 将初始无序的R[1]到R[n]建成一个小根堆,可以用以下语言实现:

for(int i=n/2;i>=1;i--)

HeapAdjust(R,i,n);

  实质上,堆排序就是利用完全二叉树中父结点与孩子结点之间的内在关系来排序的。

堆排序算法:

初始堆化所需时间不超过O(n) 

排序阶段(不含初始堆化):(1)一次重新堆化所需时间不超过O(logn),(2)n-1次循环所需时间不超过O(nlogn)

  Tw(n)=O(n)+ O(nlogn)= O(nlogn)

堆排序的时间主要耗费在建初始堆和调整建新堆时进行的反复筛选上。堆排序在最坏情况下,其时间复杂度也为O(n\log_{2}n),这是堆排序的最大优点。无论待排序列中的记录是正序还是逆序排列,都不会使堆排序处于"最好"或”最坏”的状态。

另外,堆排序仅需一个记录大小供交换用的辅助存储空间。

然而堆排序是一种不稳定的排序方法, 它不适用于待排序记录个数n较少的情况,但对于n较大的文件还是很有效的。

✳归并排序

基本思想:将两个或两个以上的有序子序列“归并”为一个有序序列。在内部排序中,通常采用2-路归并排序,即:将两个位置相邻的有序子序列R[l..m]R[m+ 1..n]归并为一个有序序列R[l..n]

如何将两个有序序列归并为一个有序序列?

设R[low]-R[mid]与R[mid+1]-R[high]为相邻,归并为一个有序序列R1[low]-R1[high]

我们设两个整型变量i,j充当“指针”,i先指向R[low](R[i]==R[low]),j先指向R[high](R[j]==R[high]),然后比较i,j所指的值,值小的放入R1中,若i小,则将R[i]放入R1中,i++,指向low+1这个数即R[low+1](R[i]==R[low+1]),依此类推往下,直到所有元素放入R1中,归并为一个序列。

 时间效率: O(n\log_{2}n)

空间效率: O(n),因为需要一个与原始序列同样:大小的辅助序列(R1)。这正是此算法的缺点。

稳定性:稳定

✳基数排序

基本思想:分配+收集。

也叫桶排序箱排序:设置若干个箱子,将关键字为k的记录放入第k个箱子,然后在按序号将非空的连接。

基数排序: (数字是有范围的,均由0-9这十个数字组成,则只需设置十个箱子,相继按个、十、百..进行排序。

如:下列一组数{614,738,921,485,637,101,215,530,790,306}

第一趟,按个位大小排:

序列变为{530,790,921,101,614,458,215,306,637,738} ;

第二趟,按十位大小排:

 序列变为{101,306,614,215,921,530,637,738,485,790} ;

第二趟,按百位大小排:

 序列变为{101,215,306,485,530,614,637,738,790,921} ,即为有序序列。

时间效率: O(k*(n+m)):k:关键字个数,m:关键字取值范围为m个值。

空间效率: O(n+m)

稳定性:稳定

各种排序比较

时间性能

(1)时间复杂度为O(nlogn)的方法有:快速排序堆排序归并排序,其中以快速排序为最好。

(2)时间复杂度为O(n^2)的有:直接插入排序冒泡排序简单选择排序,其中以直接插入为最好,特别是对那些对关键字近似有序的记录序列尤为如此。

(3)时间复杂度为O(n)的有:基数排序

当待排记录序列按关键字顺序有序时,直接插入排序冒泡排序能达到O(n)的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间性能退化为O(n^2),因此是应该尽量避免的情况。

简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。

空间性能

指的是排序过程中所需的辅助空间的大小。

(1)所有的简单排序方法(包括:直接插入、冒泡和简单选择)和堆排序的空间复杂度为O(1)

(2)快速排序为O(logn),为栈所需的辅助空间。

(3)归并排序所需辅助空间最多,其空间复杂度为O(n)

(4)链式基数排序需附设队列首尾指针,则空间复杂度为O(rd)

稳定性能

稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变。

当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。对于不稳定的排序方法,只要能举出一个实例说明即可。快速排序和堆排序是不稳定的排序方法

排序方法时间复杂度下限

本章讨论的各种排序方法,除基数排序外,其它方法都是基于“比较关键字”进行排序的排序方法,可以证明,这类排序法可能达到的最快的时间复杂度为O(nlogn)。(基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制)。可以用一棵判定树来描述这类基于“比较关键字”进行排序的排序方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值