数据结构学习之排序

一、插入类的算法

在一个已经有序的序列中,插入一个新的关键字,就好比军训排队,已经排好了一个纵队。这时,有人要临时加到这个队里来,于是教官大声喊道:“新来的,迅速找到你的位置,入队!”于是新来的“插入”到这个队伍的合适位置中。这就是“插入”类的排序。属于这类排序的有直接插入排序、折半插入排序、希尔排序。

1、直接插入排序

1、执行流程

        ~~~~~~~        先通过一个例子来体会一下插入排序的执行流程。例如,对原始序列{49,38,65,97,76,13,27, 49}进行插入排序的具体流程如下(序列中有两个49,其中一个加下划线,加以区分)。
原始序列:49 38 65 97 76 13 27 49
1)一开始只看49,一个数当然是有序的。

49 38 65 97 76 13 27 49

2)插入38。38<49,所以49向后移动一个位置,38插入到49原来的位置,这趟排序后的结果为:

38 49 65 97 76 13 27 49

3)插入65。65>49,所以不需要移动,65就应该在49之后,这趟排序后的结果为:

38 49 65 97 76 13 27 49

4)插入97。97>65,所以不需要移动,97就应该在65之后,这趟排序后的结果为:

38 49 65 97 76 13 27 49

5)插入76。76<97,所以97向后移动一个位置;继续比较,76>65,65不需要移动,76应该插入 在65之后、97之前,这趟排序后的结果为:

38 49 65 76 97 13 27 49

6)插入13。13<97,97后移;13<76,76后移;这样逐个向前比较,发现13应该插入在最前面, 这趟排序后的结果为:

13 38 49 65 76 97 27 49

7)插入27。还是从后向前进行比较,确定27应该插入在13之后、38之前,这趟排序后的结果为:

13 27 38 49 65 76 97 49

8)最后插入49,同样从后向前逐个比较,直到49=49<65,它的位置确定,直接插入排序全过程完 成。最后的排序结果为:

13 27 38 49 49 65 76 97

2、算法思想

        ~~~~~~~        根据上述例子可以总结出直接插入排序的算法思想:每趟将一个待排序的关键字按照其值的大小插 入到已经排好的部分有序序列的适当位置上,直到所有待排关键字都被插入到有序序列中为止。
由此可以写出直接插入排序的算法代码:

void  InsertSort(int  R[],int  n)//待排关键字存储在R[] 中,默认为整型,个数为n
(
	int i,j;
	int temp;
	for(int i = 1; i < n; i++)
	{
		temp = R[i];  / / 将 待 插 入 关 键 字 暂 存 于temp 中
		j = i - 1;
/*下面这个循环完成了从待排关键字之前的关键字开始扫描,如果大于待排关键字,则后移一位*/ 
		while(j >= 0 && temp < R[j])
		{														          								   
		R[j+1] = R[j];
		--j;
		R[j+1] = temp;//     找到插入位置,将temp 中暂存的待排关键字插入
		}
	}
}

3、算法性能分析

核心语句:R[j+1]=R[j];

3.1时间复杂度分析
        ~~~~~~~         1)考虑最坏的情况,即整个序列是逆序的,则内层循环中 temp<R[j]这个条件是始终成立的。此时 对于每一次外层循环,最内层循环的执行次数(也是基本操作的执行次数)达到最大值,为 i 次(如当 外层循环进行到i等于5时,内层循环j 取从4到0,执行5次)。 i取值为1到n-1, 由此可得基本操作 总的执行次数为n(n-1)/2, 可以看出时间复杂度为 O(n²)。
        ~~~~~~~         2)考虑最好的情况,即整个序列已经有序,则对于内层循环中temp<R[j]这个条件是始终不成立的。 此时内层循环始终不执行,双层循环就变成了单层循环,循环内的操作皆为常量级,显然时间复杂度为 0(n)。

综合上述两种情况,本算法的平均时间复杂度为O(n²)。

3.2空间复杂度分析
算法所需的辅助存储空间不随待排序列规模的变化而变化,是个常量,因此空间复杂度为O(1)。

对于直接插入排序, 一趟排序后并不能确保使一个关键字到达其最终位置(甚至有的序列在最后一趟排序前,这个序列没有任何一个关键字到达其最终位置)。这是插入类排序算法的共同特点。

2、折半插入排序

        ~~~~~~~        折半查找法的一个基本条件是序列已经有序,而从直接插入排序的流程中可以看出,每次都是在一 个已经有序的序列中插入一个新的关键字,因此可以用折半查找法在这个有序序列中查找插入位置。

1、执行流程

举一趟排序为例:
现在的序列是13 38 49 65 76 97 27 49
将要插入27,此时序列在数组中的情况为:

已经排序未排序
关键字13 38 49 65 76 9727 49
数组下标0 1 2 3 4 56 7

1)low=0,high=5,m=L(0+5)/2」=2, 下标为2的关键字是49,27<49,所以27应该插入到49的低 半区,改变 high=m-1=1,low 仍然是0。
2)10w=0,high=1,m=L(0+1)/2」=0, 下标为0的关键字是13,27>13,所以27应该插入到13的高 半区,改变10w=m+1=1,high 仍然是1。
3)low=1,high=1,m=L(1+1)/2」=1, 下标为1的关键字是38,27<38,所以27应该插入到38的低 半区,改变 high=m-1=0,low 仍然是1,此时 low>high, 折半查找结束,27的插入位置在下标为 high 的关键字之后,即13之后。
4)依次向后移动关键字97,76,65,49,38,然后将27插入,这一趟折半插入排序结束。 执行完这一趟排序的结果为:

13 27 38 49 65 76 97 49

2、算法性能分析

2.1时间复杂度分析
        ~~~~~~~        折半插入排序适合关键字数较多的场景,与直接插入排序相比,折半插入排序在查找插入位置上面 所花的时间大大减少。折半插入排序在关键字移动次数方面和直接插入排序是一样的,所以其时间复杂 度和直接插入排序还是一样的。
折半插入排序的关键字比较次数和初始序列无关。因为每趟排序折半查找插入位置时,折半次数是 一定的(都是在low>high 时结束),折半一次就要比较一次,所以比较次数是一定的。

由此可知折半插入排序的时间复杂度最好情况为O(nlog2n), 最差情况为O(n²³), 平均情况为O(n²)。

2.2空间复杂度分析
空间复杂度同直接插入排序一样,为O(1)。

3、希尔排序

1、算法介绍

        ~~~~~~~        希尔排序又叫作缩小增量排序,其本质还是插入排序,只不过是将待排序列按某种规则分成几个子 序列,分别对这几个子序列进行直接插入排序。这个规则的体现就是增量的选取,如果增量为1,就是 直接插入排序。例如,先以增量5来分割序列,即将下标为0、5、10、15 … 的关键字分成一组,将下 标为1、6、11、16 … 的关键字分成另一组等,然后分别对这些组进行直接插入排序,这就是一趟希尔 排序。将上面排好序的整个序列,再以增量2分割,即将下标为0、2、4、6、8 … 的关键字分成一组, 将下标为1、3、5、7、9 … 的关键字分成另一组等,然后分别对这些组进行直接插入排序,这又完成了 一趟希尔排序。最后以增量1分割整个序列,其实就是对整个序列进行一趟直接插入排序,从而完成整 个希尔排序。

        ~~~~~~~        注意到增量5、2、1是逐渐缩小的,这就是缩小增量排序的由来。前面讲过,直接插入排序适合于 序列基本有序的情况,希尔排序的每趟排序都会使整个序列变得更加有序,等整个序列基本有序了,再 来一趟直接插入排序,这样会使排序效率更高,这就是希尔排序的思想。

2、执行流程

原始序列:49 38 65 97 76 13 27 49 55 04
1)以增量5分割序列,得到以下几个子序列。
子序列1:49                           ~~~~~~~~~~~~~~~~~~~~~~~~~                           13
子序列2:       ~~~~~       38                           ~~~~~~~~~~~~~~~~~~~~~~~~~                           27
子序列3:              ~~~~~~~~~~~~              65                          ~~~~~~~~~~~~~~~~~~~~~~~~                          49
子序列4:                   ~~~~~~~~~~~~~~~~~                   97                          ~~~~~~~~~~~~~~~~~~~~~~~~                          55
子序列5:                         ~~~~~~~~~~~~~~~~~~~~~~~                         76                          ~~~~~~~~~~~~~~~~~~~~~~~~                          04
分别对这5个子序列进行直接插入排序,得到:
子序列1:13                            ~~~~~~~~~~~~~~~~~~~~~~~~~~                            49
子序列2:      ~~~~      27                            ~~~~~~~~~~~~~~~~~~~~~~~~~~                            38
子序列3:            ~~~~~~~~~~            49                            ~~~~~~~~~~~~~~~~~~~~~~~~~~                            65
子序列4:                   ~~~~~~~~~~~~~~~~~                   55                            ~~~~~~~~~~~~~~~~~~~~~~~~~~                            97
子序列5:                          ~~~~~~~~~~~~~~~~~~~~~~~~                          04                           ~~~~~~~~~~~~~~~~~~~~~~~~~                           76
一趟希尔排序结束,结果为:
13 27 49 55 04 49 38 65 97 76
2)再对上面排序的结果以增量3分割,得到以下几个子序列。
子 序 列 1 : 13                ~~~~~~~~~~~~~~                38              ~~~~~~~~~~~~              55                ~~~~~~~~~~~~~~                76
子 序 列 2 :       ~~~~~       27                ~~~~~~~~~~~~~~                04              ~~~~~~~~~~~~              65
子 序 列 3 :              ~~~~~~~~~~~~              49               ~~~~~~~~~~~~~               49               ~~~~~~~~~~~~~               97
分 别 对 这 3 个 子 序 列 进 行 直 接 插 入 排 序 , 得 到
子 序 列 1 : 13                ~~~~~~~~~~~~~~                55              ~~~~~~~~~~~~              38                ~~~~~~~~~~~~~~                76
子 序 列 2 :       ~~~~~       04                ~~~~~~~~~~~~~~                27              ~~~~~~~~~~~~              65
子 序 列 3 :              ~~~~~~~~~~~~              49               ~~~~~~~~~~~~~               49               ~~~~~~~~~~~~~               97
又一趟希尔排序结束 , 结果为 :
13 04 49 38 27 49 55 65 97 76
观察发现,现在已经基本有序了。
3)最后以增量1分割,即对上面结果的全体关键字进行一趟直接插入排序,从而完成整个希尔排序。 最后希尔排序的结果为:
04 13 27 38 49 49 55 65 76 97
观察发现,两个49在排序前后位置颠倒了,所以希尔排序是不稳定的。

3、算法性能分析

(1)时间复杂度分析
        ~~~~~~~        希尔排序的时间复杂度和增量选取有关,希尔排序的增量选取规则有很多,而考研数据结构中常见 的增量选取规则有以下两个。
1)希尔 (Shell) 自己提出的:
        ~~~~~~~         [n/2]、[n/4小]、 …、[n/2k]、 …、2、1
即每次将增量除以2并向下取整,其中n 为序列长度,此时时间复杂度为O(n²)。
2)帕佩尔诺夫和斯塔舍维奇 (Papernov&Stasevich) 提出的:
        ~~~~~~~         2k+1、 …、65、33、17、9、5、3、1
其中, k 为大于等于1的整数,2k+1 小于待排序列长度,增量序列末尾的1是额外添加的。此时时 间复杂度为O(n¹5)。
说明:希尔排序的时间复杂度分析过程十分复杂,因此考研数据结构中不会涉及分析过程的题目, 只需记住上述两个常见的结果即可。
关于希尔排序的一个考点:
请回答希尔排序增量选取时需要注意的地方。
答:
        ~~~~~~~         1)增量序列的最后一个值一定取1。
        ~~~~~~~         2)增量序列中的值应尽量没有除1之外的公因子。
(2)空间复杂度分析
        ~~~~~~~        希尔排序的空间复杂度同直接插入排序一样,为 O(1)。

二、交换类的算法

交换 类排序的核心是“交换”,即每一趟排序,都通过一系列的“交换”动作,让一个关键字排到它最终的位置上。还是军训排队的例子,设想军训刚开始,一群学生要排队,教官说:“你比你旁边的高,你俩换一下。怎么换完还比下一个高?继续换……”最后这个同学将被换到最终位置。这就是“交换”类的排序。属于这类排序的有起泡排序(刚才排队的例子)、快速排序。

1、起泡排序

1、算法介绍

        ~~~~~~~        起泡排序又称冒泡排序。它是通过一系列的“交换”动作完成的。首先第一个关键字和第二个关键 字比较,如果第一个大,则二者交换,否则不交换;然后第二个关键字和第三个关键字比较,如果第二 个大,则二者交换,否则不交换…… 。一直按这种方式进行下去,最终最大的那个关键字被交换到了最 后, 一趟起泡排序完成。经过多趟这样的排序,最终使整个序列有序。这个过程中,大的关键字像石头 一样“沉底”,小的关键字像气泡一样逐渐向上“浮动”,起泡排序的名字由此而来。

2、算法流程

原始序列:49 38 65 97 76 13 27 49
下面进行第一趟起泡排序。
1)1号和2号比较,49>38,交换。
结果:38 49 65 97 76 13 27 49
2)2号和3号比较,49<65,不交换。
结果:38 49 65 97 76 13 27 49
3)3号和4号比较,65<97,不交换。
结果:38 49 65 97 76 13 27 49
4)4号和5号比较,97>76,交换。
结果:38 49 65 76 97 13 27 49
5)5号和6号比较,97>13,交换。
结果:38 49 65 76 13 97 27 49
6)6号和7号比较,97>27,交换。
结果:38 49 65 76 13 27 97 49
7)7号和8号比较,97>49,交换。
结果:38 49 65 76 13 27 49 97
至此一趟起泡排序结束,最大的97被交换到了最后,97到达了它最后的位置。接下来对序列38 49 65 76 13 27 49 按照同样的方法进行第二趟起泡排序。经过若干趟起泡排序后,最终序列有序。要 注意的是:

起泡排序算法结束的条件是在一趟排序过程中没有发生关键字交换。

起泡排序算法代码如下:

void BubbleSort(int R[],int n)   //默认待排序关键字为整形
{
	int i, j, flag;
	int temp;
	for(i = n-1; i >= 1; --i)
	{
		flag = 0;               //变量flag用来标记本趟排序是否发生了交换
		for(j = 1; j <=i; ++i)
		{
			if(R[j-1] > R[j])
			{
				temp = R[i];
				R[j] = R[j-1];
				R[j-1] = temp;
			}
			flag = 1;          //如果没发生交换,则flag的值为0;如果发生交换,则flag值为1
		}
	}
	if(flag = 0)               //一趟排序过程中如果没有发生关键字交换,则证明序列有序,排序结束
	{
		return;
	}
}

3、算法性能分析

(1)时间复杂度分析
由起泡排序算法代码可知,可选取最内层循环中的关键字交换操作作为基本操作。
1)最坏情况,待排序列逆序,此时对于外层循环的每次执行,内层循环中 if语句的条件R[j]<R[j-1] 始终成立,即基本操作执行的次数为 n-i。i 的 取 值 为 1 ~n-1。 因此,基本操作总的执行次数为 (n-1+1)(n-1)/2=n(n-1)/2, 由此可知时间复杂度为O(n²)。
2)最好情况,待排序列有序,此时内层循环中 if 语句的条件始终不成立,交换不发生,且内层循 环执行n-1 次后整个算法结束,可见时间复杂度为O(n)。
综合以上两种情况,平均情况下的时间复杂度为O(n²)。
(2)空间复杂度分析
由算法代码可以看出,额外辅助空间只有一个 temp, 因此空间复杂度为O(1)。

2、快速排序

1、算法介绍

        ~~~~~~~        快速排序也是“交换”类的排序,它通过多次划分操作实现排序。以升序为例,其执行流程可以概 括为:每一趟选择当前所有子序列中的一个关键字(通常是第一个)作为枢轴,将子序列中比枢轴小的 移到枢轴前边,比枢轴大的移到枢轴后边;当本趟所有子序列都被枢轴以上述规则划分完毕后会得到新 的一组更短的子序列,它们成为下一趟划分的初始序列集。

2、执行流程

原始序列:49 38 65 97 76 13 27 49
                   ~~~~~~~~~~~~~~~~~~                   i                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                 j (i 和 j 开始时分别指向头、尾关键字)
进行第一趟快速排序,以第一个数49作为枢轴,整个过程是一个交替扫描和交换的过程。
1 ) 使 用j, 从序列最右端开始向前扫描,直到遇到比枢轴49小的数27,j 停在这里。
49 38 65 97 76 13 27 49
   ~~   i                           ~~~~~~~~~~~~~~~~~~~~~~~~~                           j
2)将27交换到序列前端i 的位置。
27 38 65 97 76 13           ~~~~~~~~~           49
   ~~   i                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~                              j
3 ) 使 用i, 变换扫描方向,从前向后扫描,直到遇到比枢轴49大的数65, i 停在这里。
27 38 65 97 76 13           ~~~~~~~~~           49
            ~~~~~~~~~~~            i                    ~~~~~~~~~~~~~~~~~~                    j
4)将65交换到序列后端j 的位置。
27 38        ~~~~~~        97 76 13 65 49
             ~~~~~~~~~~~~             i                    ~~~~~~~~~~~~~~~~~~                   j
5)使用j变换扫描方向,从后向前扫描,直到遇到比枢轴49小的数13,j 停在这里。
27 38        ~~~~~~        97 76 13 65 49
             ~~~~~~~~~~~~             i                ~~~~~~~~~~~~~~               j
6)将13交换到i 的位置。
27 38 13 97 76         ~~~~~~~         65 49
            ~~~~~~~~~~~            i                ~~~~~~~~~~~~~~                j
7)使用i, 变换扫描方向,从前向后扫描,直到遇到比枢轴49大的数97, i停在这里。
27 38 13 97 76         ~~~~~~~         65 49
                ~~~~~~~~~~~~~~~                i            ~~~~~~~~~~            j
8)将97交换到j 的位置。
27 38 13        ~~~~~~        76 97 65 49
                  ~~~~~~~~~~~~~~~~~                  i           ~~~~~~~~~          j
9)使用j, 变换扫描方向,从后向前扫描,直到遇到比枢轴49小的数,当扫描到i 与j 相遇时,说 明扫描过程结束了。
27 38 13           ~~~~~~~~~           76 97 65 49
                   ~~~~~~~~~~~~~~~~~~                   ij
10)此时 i等于j 的这个位置就是枢轴49的最终位置,将49放入这个位置,第一趟快速排序结束。
27 38 13 49 76 97 65 49
                ~~~~~~~~~~~~~~~                ij
可以看出第一趟划分后,将原来的序列以49为枢轴,划分为两部分,49左边的数都小于或等于它, 右边的数都大于或等于它。接下来按照同样的方法对序列{27 38 13}和序列{76 97 65 49}分别进 行排序。经过几趟这样的划分,最终会得到一个有序的序列。

注意:快速排序中对每一个子序列的一次划分算作一趟排序,每一趟结束之后有一个关键字到达最 终位置。

快速排序算法代码如下:

void QuickSort(int R[], int low, int high)  //对从R[low]到R[high]的关键字进行排序
{
	int  temp;
	int   i=low,j=high;
	if(low < high)
	{
		temp=R[low];
	/*下面这个循环完成了一趟排序,即将数组中小于temp的关键字放在左边,大于 temp 的关键字放在右边*/
		while(i < j)
		{
			while(j > i && R[j] >= temp) --j;//从右往左扫描,找到一个小于temp 的关键字
		    if(i<j)
			{
				R[i] = R[j];//放在temp 左边
				++i;        //i 右移一位
			}
			while(i < j && R[i] < temp) ++i;//从左往右扫描,找到一个大于temp 的关键字
			if(i<j)
			{
				R[j] = R[i];//放在 temp 右边
                --j;        //j 左移一位
			}
		}
	    R[i]=temp;               //将 temp 放在最终位置
		QuickSort(R, low, i-1);  //递归地对temp 左边的关键字进行排序
		QuickSort(R, i+1, high); //递归地对temp 右边的关键字进行排序

}

3、算法性能分析

(1)时间复杂度分析
         ~~~~~~~~         快速排序最好情况下的时间复杂度为O(nlog₂n), 待排序列越接近无序,本算法效率越高。最坏情况 下的时间复杂度为O(n²),待排序列越接近有序,本算法效率越低。平均情况下时间复杂度为 O(nlog₂n)。 快速排序的排序趟数和初始序列有关。
         ~~~~~~~~         说明:后边还会出现多个时间复杂度同为0(nlog₂n)的排序算法,但仅有本节的算法称为快速排序, 原因是这些算法的基本操作执行次数的多项式最高次项为Xxnlog₂n(X 为系数),快速排序的X 最小。 可见它在同级别的算法中是最好的,因此叫快速排序。
(2)空间复杂度分析
        ~~~~~~~        本算法的空间复杂度为0(log₂n)。 快速排序是递归进行的,递归需要栈的辅助,因此它需要的辅助 空间比前面几类排序算法大。

三、选择类的算法

选择类排序的核心是“选择”,即每一趟排序都选出一个最小(或最大)的关键字,把它和序列中的 第一个(或最后一个)关键字交换,这样最小(或最大)的关键字到位。继续军训排队,教官说:“你们 都站着别动,我看谁个子最小。”然后教官选出个子最小的同学,说“第一个位置是你的了,你和第一个 同学换一下,剩下的同学我继续选。”这就是“选择”类的排序。属于这类排序的有简单选择排序、堆排 序。

1、简单选择排序

1、算法介绍

        ~~~~~~~        选择类排序的主要动作是“选择”,简单选择排序采用最简单的选择方式,从头至尾顺序扫描序列, 找出最小的一个关键字,和第一个关键字交换,接着从剩下的关键字中继续这种选择和交换,最终使序 列有序。

2、执行流程

        ~~~~~~~        原始序列:49 38 65 97 76 13 27 49
        ~~~~~~~        在进行选择排序的过程中,把整个序列分成有序部分和无序部分。开始时,整个序列为无序序列,
如下所示:

无序
4938659776132749
$~~~~~~~$进行第一趟排序,从无序序列中选取一个最小的关键字13,使其与无序序列中的第一个关键字交换, 则此时产生了仅含有一个关键字的有序序列,而无序序列中的关键字减少1个,如下所示:
有序无序
1338659776492749
重复上述步骤,直到无序序列中的关键字变为0个为止。

本算法代码如下:

void SelectSort(int R[], int n)
{
	int i, j, k;
    int temp;
	for(i = 0;i < n; ++i)
	{
		k = i;
		/*这个循环是算法的关键,它从无序序列中挑出一个最小的关键字*/ 
		for(j = i + 1; j < n; ++j)
			if(R[k] > R[j])
				k=j;
		/*下面3句完成最小关键字与无序序列第一个关键字的交换*/
		temp = R[i];
		R[i] = R[k];
		R[k] = temp;
}

3、算法性能分析

(1)时间复杂度分析
        ~~~~~~~        通过本算法代码可以看出,两层循环的执行次数和初始序列没有关系,外层循环执行 n 次,内层循 环执行 n-1 次,将最内层循环中的比较操作视为关键操作,其执行次数为(n-1+1)(n-1)/2=n(n-1)/2, 即时
间复杂度为O(n²)。
(2)空间复杂度分析
         ~~~~~~~~         算法所需的辅助存储空间不随待排序列规模的变化而变化,是个常量,因此空间复杂度为O(1)。

2、堆排序

1、算法介绍

        ~~~~~~~        堆是一种数据结构,可以把堆看成一棵完全二叉树,这棵完全二叉树满足:任何一个非叶结点的值 都不大于(或不小于)其左右孩子结点的值。若父亲大孩子小,则这样的堆叫作大顶堆;若父亲小孩子 大,则这样的堆叫作小顶堆。
        ~~~~~~~        根据堆的定义知道,代表堆的这棵完全二叉树的根结点的值是最大(或最小)的,因此将一个无序 序列调整为一个堆,就可以找出这个序列的最大(或最小)值,然后将找出的这个值交换到序列的最后 (或最前),这样,有序序列关键字增加1个,无序序列中关键字减少1个,对新的无序序列重复这样的 操作,就实现了排序。这就是堆排序的思想。
        ~~~~~~~        堆排序中最关键的操作是将序列调整为堆。 整个排序的过程就是通过不断调整,使得不符合堆定义 的完全二叉树变为符合堆定义的完全二叉树。

2、执行流程

        ~~~~~~~        原始序列:49 38 65 97 76 13 27 49
(1)建堆
        ~~~~~~~        先将这个序列调整为一个大顶堆。原始序列对应的完全二叉树如图所示。
请添加图片描述

        ~~~~~~~        在这个完全二叉树中,结点76、13、27、49是叶子结点,它们没有左右孩子,所以它们满足堆的定义。从97开始,按97、65、38、49的顺序依次调整。
        ~~~~~~~         1)调整97。97>49,所以97和它的孩子 49 满足堆的定义,不需要调整。
        ~~~~~~~         2)调整65。65>13,65>27,所以65和它的孩子13、27满足堆的定义,不需要调整。
        ~~~~~~~         3)调整38。38<97,38<76,不满足堆定义了,需要调整。在这里,38 的两个孩子结点值都比38 大,应该和哪个交换呢?显然应该和两者中较大的交换,即和97交换。如果和76交换,则76<97仍然 不满足堆的定义。因此,将38和97交换。交换后38成了49的根结点,38<49,仍然不满足堆定义, 需要继续调整,将38和49交换,结果如图所示。
请添加图片描述
        ~~~~~~~         4)调整49。49<97,49<65不满足堆定义,需要调整,找到较大的孩子97,将49和97交换。交换 后49<76仍不满足堆定义,继续调整,将49与76交换,结果如图所示。
请添加图片描述
(2)插入结点
        ~~~~~~~        需要在插入结点后保持堆的性质,即完全二叉树形态与父大子小性质(以大顶堆为例),因此需要先将要插入的结点X 放在最底层的最右边,插入后满足完全二叉树的特点; 然后把 x 依次向上调整到合适 位置以满足父大子小的性质。
(3)删除结点
        ~~~~~~~        当删除堆中的一个结点时,原来的位置就会出现一个孔,填充这个孔的方法就是,把最底层最右边 的叶子的值赋给该孔并下调到合适位置,最后把该叶子删除。
(4)排序
        ~~~~~~~        可以看到,此时已经建立好了一个大顶堆。对应的序列为:97 76 65 49 49 13 27 38。将 堆顶关键字97和序列最后一个关键字38交换。第一趟堆排序完成。97到达其最终位置。将除97外的 序列38 76 65 49 49 13 27重新调整为大顶堆。现在这个堆只有38是不满足堆定义的,其他的关键字都满足,所以只需要调整一个38就够了。
        ~~~~~~~        调整38,结果如图所示。
请添加图片描述

        ~~~~~~~         现在的序列为:76 49 65 38 49 13 27 97。将堆顶关键字76和最后一个关键字27交换,第二趟堆排序完成。76 到达其最终位置,此时序列为:27 49 65 38 49 13 76 97。然后对除76和97的序列依照上面的方法继续处理,直到树中只剩1个结点时排序完成。
        ~~~~~~~         堆排序执行过程描述(以大顶堆为例)如下:
        ~~~~~~~         1)从无序序列所确定的完全二叉树的最后一个非叶子结点开始,从右至左,从下至上,对每个结点 进行调整,最终将得到一个大顶堆。
        ~~~~~~~         对结点的调整方法: 将当前结点(假设为 a) 的值与其孩子结点进行比较,如果存在大于 a 值的孩 子结点,则从中选出最大的一个与 a 交换。当a 来到下一层的时候重复上述过程,直到a 的孩子结点值 都小于a 的值为止。
        ~~~~~~~         2)将当前无序序列中的第一个关键字,反映在树中是根结点(假设为a) 与无序序列中最后一个关 键字交换(假设为 b)。a进入有序序列,到达最终位置。无序序列中关键字减少1个,有序序列中关键 字增加1个。此时只有结点b 可能不满足堆的定义,对其进行调整。
        ~~~~~~~         3)重复第2)步,直到无序序列中的关键字剩下1个时排序结束。
本算法代码如下:

/*本函数完成在数组R [1ow] 到 R[high]   的范围内对在位置1ow 上的结点进行调整*/
void Sift(int R[], int low, int high)//这里关键字的存储设定为从数组下标 1 开始
{
	int i = low, j = 2 * i;//R[j] 是R[i] 的左孩子结点
	int temp = R[i];
	while(j <= high)
	{
		if(j < high && R[j] < R[j+1])//若右孩子较大,则把j 指向右孩子
			++j;                     //j变为2*i+1
		if(temp<R[j])
		{
			R[i]=R[j];      		 //将R[j]调整到双亲结点的位置上
			i=j;					 //修改i 和 j 的值,以便继续向下调整
			j=2*i;
		}
        else
			break;					 //调整结束
	}
	R[i]=temp;						 //被调整结点的值放入最终位置
}
/*堆排序函数*/
void  heapSort(int R[], int  n)
{
	int i;
	int temp;
	for(i = n/2; i >= 1; --i)     //建立初始堆
		Sift(R,i,n);
	for(i = n; i >= 2; --i)       //进行n-1 次循环,完成堆排序
	{
		/*以下3句换出了根结点中的关键字,将其放入最终位置*/
    	temp = R[1];
		R[1] = R[i];
		R[i] = temp;
		Sift(R,1,i- 1);           //在减少了 1 个关键字的无序序列中进行调整
	}
}

3、算法性能分析

        ~~~~~~~        (1)时间复杂度分析
        ~~~~~~~        对于函数 Sif(),显然j 走了一条从当前结点到叶子结点的路径,完全二叉树的高度为[log₂(n+1)], 即对每个结点调整的时间复杂度为0(log₂n)。对于函数 heapSort(),基本操作总次数应该是两个并列的 for 循环中的基本操作次数之和,第一个循环的基本操作次数为 O(log₂n)×n/2, 第二个循环的基本操作次数 为 0(log₂n)×(n-1),因此整个算法的基本操作次数为O(log₂n)×n/2+0(log₂n)×(n-1),化简后得其时间复杂 度为O(nlog₂n)。
        ~~~~~~~        (2)空间复杂度分析
        ~~~~~~~        算法所需的辅助存储空间不随待排序列规模的变化而变化,是个常量,因此空间复杂度为O(1)。
        ~~~~~~~        说明:上面对时间复杂度的分析不太严格,但是通过上面的方法去大体计算一个算法的时间复杂度, 这在考研中运用得比较多,也符合考研对算法时间复杂度分析的要求。例如, 一个算法中主要进行了在 一棵完全二叉树中从某个结点走向叶子结点的操作,完全二叉树的高度为[log₂ (n+1)], 其时间复杂度就 可以认为是0(log₂n), 而不必十分严格地去考虑是从根结点出发到达叶子结点,还是从中间某个结点出 发到达叶子结点。
        ~~~~~~~        堆排序在最坏情况下的时间复杂度也是 O(nlog₂n), 这是它相对于快速排序的最大优点。堆排序的 空间复杂度为0(1),在所有时间复杂度为O(nlog₂n)的排序中是最小的,这也是其一大优点。堆排序适 合的场景是关键字数很多的情况,典型的例子是从10000个关键字中选出前10个最小的,这种情况用 堆排序最好。如果关键字数较少,则不提倡使用堆排序。

四、归并类的算法

所谓归并就是将两个或两个以上的有序序列合并成一个新的有序序列,归并类排序就是基于这种思 想。我们继续排队,这次教官想了个特别的方法,他说:“你们每个人,先和旁边的人组成一个二人组, 二人组内部先排好。”看到大家排好了,继续说:“二人组和旁边的二人组继续组合成一个四人组,每个 四人组内部排好,动作快!"这样不停排下去,最后全部学生都归并到了一个组中,同时也就排好序了。 这就是“归并”类的排序。这个例子正是二路归并排序,特点是每次都把两个有序序列归并成一个新的有序序列。

1、二路归并排序

1、执行流程

        ~~~~~~~        原始序列:49 38 65 97 76 13 27
        ~~~~~~~         1)将原始序列看成7个只含有一个关键字的子序列,显然这些子序列都是有序的。
        ~~~~~~~        子序列1:49
        ~~~~~~~        子序列2:38
        ~~~~~~~        子序列3:65
        ~~~~~~~        子序列4:97
        ~~~~~~~        子序列5:76
        ~~~~~~~        子序列6:13
        ~~~~~~~        子序列7:27
        ~~~~~~~         2)两两归并,形成若干有序二元组,即49和38归并成{3849},65和97归并成{65 97},76和13 归并成{13 76},27没有归并对象,保持原样。第一趟二路归并排序结束,结果如下:
        ~~~~~~~        {38 49},{65 97},{13 76},{27}
        ~~~~~~~         3)再将这个序列看成若干二元组子序列。
        ~~~~~~~        子序列1:38 49
        ~~~~~~~        子序列2:65 97
        ~~~~~~~        子序列3:13 76
        ~~~~~~~        子序列4:27
        ~~~~~~~        最后一个子序列长度可能是1,也可能是2。
        ~~~~~~~         4)继续两两归并,形成若干有序四元组(同样,最后的子序列中不一定有4个关键字),即{38 49} 和{65 97}归并形成{38 49 65 97},{13 76}和{27}归并形成{13 27 76}。第二趟二路归并排序
结束,结果如下:
        ~~~~~~~        {38 49 65 97},{13 27 76}
        ~~~~~~~         5)最后只有两个子序列了,再进行一次归并,就可完成整个二路归并排序,结果如下:
        ~~~~~~~         13 27 38 49 65 76 97
        ~~~~~~~        由以上分析可知,归并排序可以看作一个分而治之的过程(关于分治法,可以看本书最后一章的讲 解):先将整个序列分为两半,对每一半分别进行归并排序,将得到两个有序序列,然后将这两个序列归 并成一个序列即可。假设待排序列存在数组 A 中,用low 和 high 两个整型变量代表需要进行归并排序的 关键字范围,由此可以写出如下代码:

void mergeSort(int A[], int low, int high)
{
	if(low < high)
	{
		int mid = (low + high)/2;
	1	mergeSort(A, low, mid);     //归并排序前半段
		mergeSort(A, mid + 1, high);//归并排序后半段
		merge(A, low, mid, high);  //这里直接修改调用了线性表那一章的函数merge(),它的功能是把A 数组中1ow 到 mid 和mid+1 到 high 范围内的两段有序序列归并成一段有序序列
	}
}

2、算法性能分析

        ~~~~~~~        (1)时间复杂度分析
        ~~~~~~~        归并排序中可选取函数 merge()内的 “归并操作” 作为基本操作。函数 merge(的作用是将两个有序 序列归并成一个整体有序的序列。 “归并操作” 即为将待归并表中的关键字复制到一个存储归并结果的表 中的过程。在顺序表中,函数 merge()的 “归并操作” 执行次数为要归并的两个子序列中关键字个数之和, 这在线性表一章有所体现。由归并排序的过程可知:
        ~~~~~~~        第1趟归并需要执行2×(n/2)=n次基本操作(其中,2为两子序列关键字个数之和, n/2为要归并的 子序列对的个数;每个子序列对执行一次函数 merge(), 也就是两次基本操作)。
        ~~~~~~~        第2趟归并需要执行4×(n/4)=n次基本操作。
        ~~~~~~~        第3趟归并需要执行8×(n/8)=n 次基本操作。
        ~~~~~~~         ···
        ~~~~~~~        第k 趟归并需要执行2×n/2=n 次基本操作。
        ~~~~~~~         ···
        ~~~~~~~        当 n/2⁵=1时,即需要归并的两个子序列长度均为原序列的一半,只需执行一次函数 merge()归并排 序即可结束。此时k=log₂n, 即总共需要进行 log₂n趟排序,每趟排序执行n 次基本操作,因此整个归并 排序中总的基本操作执行次数为n log₂n, 时间复杂度为O(nlog₂n)。
        ~~~~~~~        可见归并排序时间复杂度和初始序列无关,即平均情况下、最好情况下、最坏情况下均为 O( nlog₂n)。
        ~~~~~~~        (2)空间复杂度分析
        ~~~~~~~        因归并排序需要转存整个待排序列,因此空间复杂度为 O(n)

五、基数类的算法

基数类的排序是最特别的一类,跟前面的思想完全不同(前面都是要进行“比较”和“移动”这两 个操作)。基数类的排序基于多关键字排序的思想,把一个逻辑关键字拆分成多个关键字。例如,对一副 去掉大小王的52张扑克牌进行基数排序,可以先按花色排序(如按红桃、黑桃、方片和梅花的顺序), 这样就分成了4堆,然后每一堆再按照从A 到 K 的顺序,排序使这副牌最终有序。具体排序过程后边会 细致讲解。

1、基数排序

1、算法介绍

       ~~~~~~        基数排序的思想是“多关键字排序”,前面已经讲过了。基数排序有两种实现方式:第一种叫作最高 位优先,即先按最高位排成若干子序列,再对每个子序列按次高位排序。举扑克牌的例子,就是先按花 色排成4个子序列,再对每种花色的13张牌进行排序,最终使所有扑克牌整体有序。第二种叫作最低位 优先,这种方式不必分成子序列,每次排序全体关键字都参与。最低位可以优先这样进行,不通过比较, 而是通过“分配”和“收集”。还是扑克牌的例子,可以先按数字将牌分配到13个桶中,然后从第一个 桶开始依次收集;再将收集好的牌按花色分配到4个桶中,然后还是从第一个桶开始依次收集。经过两 次“分配”和“收集”操作,最终使牌有序。

2、执行流程

       ~~~~~~        下面通过一个例子来体会基数排序过程,初始桶如图所示。
请添加图片描述

       ~~~~~~        原始序列:278 109 063 930 589 184 505 269 008 083
       ~~~~~~        每个关键字的每一位都是由“数字”组成的,数字的范围是0~9,所以准备10个桶用来放关键字。 要注意的是,组成关键字的每一位不一定是数字。例如,如果关键字的某一位是扑克牌的花色,因为花 色有4种,所以在按花色那一位排序时,要准备4个桶。同理,如果关键字有一位是英文字母,那么按 这一位排序时,就要准备26个桶(假设不区分大小写)。这里所说的“桶”,其实是一个先进先出的队列 (从桶的上面进,下面出)。
       ~~~~~~        1)进行第一趟分配和收集,要按照最后一位。
       ~~~~~~        ① 分配过程如下(注意,关键字从桶的上面进入):
       ~~~~~~        278的最低位是8,放到桶8中,如图所示。

请添加图片描述
109的最低位是9,放到桶9中,如图所示。
请添加图片描述
         ~~~~~~~~         按照这样的方法,依次(按原始序列顺序)将原始序列的每个数放到对应的桶中。第一趟分配过程 完成,结果如图所示。
请添加图片描述
         ~~~~~~~~         ② 收集过程是这样的:按桶0到桶9的顺序收集,注意关键字从桶的下面出。
         ~~~~~~~~         桶0:930
         ~~~~~~~~         桶1:没关键字,不收集
         ~~~~~~~~         桶2:没关键字,不收集
         ~~~~~~~~         桶3:063,083
         ~~~~~~~~         ···
         ~~~~~~~~         桶8:278,008
         ~~~~~~~~         桶9:109,589,269
         ~~~~~~~~         将每桶收集的关键字依次排开,所以第一趟收集后的结果为:
         ~~~~~~~~          930 063 083 184 505 278 008 109 589 269
         ~~~~~~~~         注意观察,最低位有序了,这就是第一趟基数排序后的结果。
         ~~~~~~~~          2)在第一趟排序结果的基础上,进行第二趟分配和收集,这次按照中间位。
         ~~~~~~~~         ① 第二趟分配过程如下:
         ~~~~~~~~          930的中间位是3,放到桶3中,如图所示。

请添加图片描述
         ~~~~~~~~          063的中间位是6,放到桶6中,如图所示。
请添加图片描述
         ~~~~~~~~          按照同样的方法,将其余关键字依次入桶,结果如图所示。
请添加图片描述
         ~~~~~~~~         ② 进行第二趟收集。
         ~~~~~~~~         桶0:505,008,109
         ~~~~~~~~         桶1:没关键字,不收集 桶2:没关键字,不收集 桶3:930
         ~~~~~~~~         ···
         ~~~~~~~~         桶8:083,184,589
         ~~~~~~~~         桶9:没关键字,不收集
         ~~~~~~~~         第二趟收集结果为:
         ~~~~~~~~          505 008 109 930 063 269 278 083 184 589
         ~~~~~~~~         此时中间位有序了,并且中间位相同的那些关键字,其最低位也是有序的,第二趟基数排序结束。 3)在第二趟排序结果的基础上,进行第三趟分配和收集,这次按照最高位。
         ~~~~~~~~         ① 第三趟分配过程如下:
         ~~~~~~~~          505的最高位是5,放到桶5中,如图所示。
在这里插入图片描述

         ~~~~~~~~          008的最高位是0,放到桶0中,如图8所示。
请添加图片描述

         ~~~~~~~~         按照同样的方法,将其余关键字依次入桶,结果如图所示。
请添加图片描述
         ~~~~~~~~         ② 进行第三趟收集。 桶0:008,063,083 桶1:109,184
         ~~~~~~~~         桶2:269,278
         ~~~~~~~~         桶3:没关键字,不收集
         ~~~~~~~~         桶8:没关键字,不收集
         ~~~~~~~~         桶9:930
         ~~~~~~~~         第三趟收集结果为:
         ~~~~~~~~          008 063 083 109 184 269 278 505 589 930
         ~~~~~~~~         现在最高位有序,最高位相同的关键字按中间位有序,中间位相同的关键字按最低位有序(这里没 体现出来),于是整个序列有序,基数排序过程结束。

3、算法性能分析

         ~~~~~~~~         时间复杂度:平均和最坏情况下都是 O(d(n+rd))。
         ~~~~~~~~         空间复杂度: O(ra)。
         ~~~~~~~~         其中, n 为序列中的关键字数; d 为关键字的关键字位数,如930,由3位组成, d=3;ra 为关键字 基的个数,这里的基指的是构成关键字的符号,如关键字为数值时,构成关键字的符号就是0~9这些数 字, 一共有十个,即ra=10。
         ~~~~~~~~         这里简单讲解基数排序时间复杂度的记忆方法。基数排序每一趟都要进行“分配”和“收集”。“分 配”需要依次对序列中的每个关键字进行,即需要顺序扫描整个序列,所以有n 这一项;“收集”需要依 次对每个桶进行,而桶的数量取决于关键字的取值范围,如放数字的桶有10个,放花色的桶有4个等, 刚好是ra的值,所以有ra这一项,因此一趟“分配”和“收集”需要的时间为n+ra。 整个排序需要多少 趟的“分配”和“收集”呢?需要d 趟,即关键字的关键字位数有几位,就需要几趟。例如上面的例子,关键字由3位组成,所以要进行3趟。
         ~~~~~~~~         因此,时间复杂度为O(d(n+ra))。
         ~~~~~~~~         至于空间复杂度,因为每个桶实际上是一个队列,需要头尾指针,共有ra个桶,所以需要2ra个存
放指针的空间,因此是O(ra)。
         ~~~~~~~~         说明:基数排序适合的场景是序列中的关键字很多,但组成关键字的关键字的取值范围较小,如数 字0~9是可以接受的。如果关键字的取值范围也很大,如26个字母,并且序列中大多数关键字的最高 位关键字都不相同,那么这时可以考虑使用“最高位优先法”,先根据最高位排成若干子序列,然后分 别对这些子序列进行直接插入排序。

排序知识点总环

         ~~~~~~~~          1. 复杂度总结
         ~~~~~~~~         (1)时间复杂度
         ~~~~~~~~         平均情况下,快速排序、希尔排序(复杂度了解即可)、归并排序和堆排序的时间复杂度均为 O(nlog2n), 其他都是 O(n²)。 一个特殊的是基数排序,其时间复杂度为O(d(n+ra))。
         ~~~~~~~~         最坏情况下,快速排序的时间复杂度为O(n²), 其他都和平均情况下相同。
         ~~~~~~~~         故事助记:如军训时,教官说:“快些以nlog₂n 的速度归队。”其中,“快”指快速排序,“些”指 希尔排序(发音近似),“归”指归并排序,“队”指堆排序(谐音),这4 种排序的平均复杂度都是 O(nlog2n)。
         ~~~~~~~~         (2)空间复杂度
         ~~~~~~~~         记住几个特殊的就好,快速排序为0(log₂n), 归并排序为O(n), 基数排序为0(ra),其他都是O(1)。
         ~~~~~~~~         (3)其他
         ~~~~~~~~         直接插容易插变成O(n), 起泡起得好变成O(n), 所谓“容易插”或“起得好”都是指初始序列已经 有序。
         ~~~~~~~~          2. 算法稳定性总结
         ~~~~~~~~         一句话记忆:“考研复习痛苦啊,情绪不稳定,快些选一堆好友来聊天吧”。
         ~~~~~~~~         这里,“快”指快速排序,“些”指希尔排序,“选”指简单选择排序,“堆”指堆排序,这4种是不 稳定的,其他自然都是稳定的。
         ~~~~~~~~         注意:关于简单选择排序,按照本书的算法(同时也是绝大多数学校在考研数据结构中所使用的算 法 ) 来实现, 一定是不稳定的。例如,对于序列“4(1)、3、4(2)、1、5”进行简单选择排序,括号中标 出了相同关键字的前后顺序,第一趟选出1为当前最小值,与4(1)交换,得到的序列为1、3、4(2)、4(1)、 5, 显然是不稳定的。如果把交换操作换成插入操作,即每次选出的最小值都插入到已排好的有序序列 尾部,则此算法变成了稳定的。
         ~~~~~~~~         由此可见,简单选择排序算法有两个版本 交换版和插入版。在以数组为关键字载体的情况下,
显然用交换版比较合适,因为在数组上执行插入操作需要移动大量关键字;而对于以链表为关键字载体 的情况,用插入版就比较合适,且能得到稳定的结果。
         ~~~~~~~~         对于简单选择排序,在考研数据结构中出现最多的在于对其算法稳定性的考查。在题目没有明确说 明以链表为关键字载体且没有说明算法具体执行流程的情况下,答案都应该是不稳定的,
         ~~~~~~~~          3. 其他细节(与排序原理有关)
         ~~~~~~~~          1)经过一趟排序,能够保证一个关键字到达最终位置,这样的排序是交换类的那两种(起泡、快速) 和选择类的那两种(简单选择、堆)。
         ~~~~~~~~          2)排序算法的关键字比较次数和原始序列无关——简单选择排序和折半插入排序。 3)排序算法的排序趟数和原始序列有关——交换类的排序。
         ~~~~~~~~          4. 再次比较一下直接插入排序和折半插入排序
二者最大的区别在于查找插入位置的方式不同。直接插入按顺序查找的方式,而折半插入按折半查 找的方式。这里将排序和查找两章的内容结合,大家在复习时也应该融会贯通,计算机的知识都是相通的。
         ~~~~~~~~          5. 一个有用的结论
         ~~~~~~~~          借助于“比较”进行排序的算法,在最坏情况下的时间复杂度至少为O(nlog₂n)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值