第九节:排序(上)

9.1 概述

对于之后应用到的一些说明:

(1)void X_Sort(ElementType A[], int N) X为排序名称。

①大多数情况下,为了简单起见,讨论从小到大的整数排序。

②默认N为正整数。

③只讨论基于比较的排序(>=<都是有定义的)。

④只讨论内部排序(一次性可以写入内存,然后只在内存里面的数据排序)。

⑤稳定性:任意两个相等的数据,排序前后的相对位置不发生改变。

没有一种排序是任何情况下都表现最好的!!!

9.2 简单排序算法

9.2.1 冒泡排序

在一次排序完成后,最后面的一定是最大的,第二次排序的时候只需要对前N-1个排序即可,然后N-2……一直到最后完成。

冒泡排序的伪码描述如下:

最好情况:顺序T=O(N);最差情况:逆序T=O(N2)

这个算法具有稳定性

9.2.2 插入排序

类似于打扑克的时候拿到牌之后进行排序 。

源代码:

最好情况:顺序T=O(N)

最坏情况:逆序T=O (N2)

此方法也具有稳定性

举例,问序列{34,8,64,51,32,21}中用插入排序和冒泡排序分别需要交换多少次?

答:冒泡法:9次;插入法,9次。

    它们的次数相等,是巧合还是必然?见下一节。

9.2.3 时间复杂度下界

1.逆序对

对于下标i<j,如果A[i]>A[j],则称(i,j)是一对逆序对(inversion)。

我们来看上一节中最后的序列{34,8,64,51,32,21},它里面有多少逆序对呢?

(34,8)(34,42)(34,21)(64,51)(64,21)(64,21)(51,32)(51,21)(32,21)

一共9个逆序对。这说明,前面两种算法,交换2个相邻元素正好消去1个逆序对

插入排序时间复杂度:T(N,I)=O(N+I)。这里面N是元素个数,I是逆序对个数。这个可以这样理解,时间复杂度最低为元素个数N,即逆序对为0,也就是顺序排列情况下;然后随着多一个逆序对,复杂度加1。

如果序列基本有序,那么插入排序简单且高效。

2.定理1:任意N个不同元素组成的序列平均具有N(N-1)/4个逆序对。

3.定理2:任何仅以交换相邻两元素来排序的算法,其平均时间复杂度为Ω(N2。(注意:Ω是下界,O是上界)

    这意味着,要提高算法效率,我们必须:每次消去不止1个逆序对!每次交换相隔较远的2个元素!

9.3 希尔排序

1.简单举例

它利用了插入排序的简单,同时克服了交换每次只交换相邻两个元素的缺点。

用下面这个序列来进行希尔排序的举例:

81

94

11

96

12

35

17

95

28

58

41

75

15

(1)首先以每5个元素取一个的规律进行排序:这里取了81、35、41。用插入排序对其进行排序:

35

94

11

96

12

41

17

95

28

58

81

75

15

然后取94、17、75进行插入排序,得到下面的结果:

35

17

11

96

12

41

75

95

28

58

81

94

15

然后考虑11、95、15,得到下面结果:

35

17

11

96

12

41

75

15

28

58

81

94

95

然后考虑96、28,得到下面结果:

35

17

11

28

12

41

75

15

96

58

81

94

95

然后考虑12和58:

35

17

11

28

12

41

75

15

96

58

81

94

95

(2)再用每3个元素取一个的规律进行排序:这里取35、28、75、58、95,结果如下:

28

12

11

35

15

41

58

17

94

75

81

96

95

(3)再做1间隔排序(即普通插入排序)。我们可以发现,此时整个序列已经基本有序,所以此时插入排序简单高效。

11

12

15

17

28

35

41

58

75

81

94

95

96

该算法的步骤:

①.定义增量序列DM>DM-1>…>D1=1

②对每个Dk进行“Dk-间隔”排序。

注意:“Dk-间隔”有序的序列,在执行“Dk-1-间隔”排序后,仍然是“Dk-间隔”有序的。

2.希尔增量序列

(1)原始希尔排序:DM=[N/2],Dk=[Dk+1/2]。其中,[]代表取整。

伪代码如下:

最坏情况:T=θ(N2)θ代表即使上界也是下界,即可以达到的值

这种情况即,增量元素不互质,则小增量可能根本不起作用。

3.其它增量序列

(1)Hibbard增量序列:Dk=2k-1(相邻元素互质)。最坏情况:T=θ(N3/2),猜想Tavg=O(N5/4)

(2)Sedgewick增量序列:{1,5,19,41,109}(9×4i-9×2i+1或4i-3×2i+1),猜想Tavg=O(N7/6)Tworst=O(N4/3)

4.稳定性:不稳定。

9.4 堆排序

9.4.1 选择排序

无论如何,T=θ(N2)

如何快速寻找到最小元?见下一节。

9.4.2 堆排序(选择排序的改进)

1.算法1:

时间复杂度:T(N)=O(NlogN)

缺点:需要额外O(N)空间,并且复制元素需要时间。

2.算法2:

注意,在堆排序里面的这个堆,元素是从0的位置开始的;原来学堆的时候,0是用来放哨兵的。

    因此,在堆排序中,元素下标从0开始。则对于下标为i的元素,其左、右孩子的下标分别为:2i+1, 2i+2

    伪码描述如下:

定理:堆排序处理N个不同元素的随机排列的平均比较次数是2NlogN-O(Nlog logN)。

注意:虽然堆排序给出最佳平均时间复杂度,但实际效果不如用Sedgewick增量序列的希尔排序。

整体C语言源代码:

9.5 归并排序

9.5 1有序子列的归并

1.举例:假设我们有两个有序序列如下:

1

13

24

26

 

2

15

27

38

我们要将它们合并成一个序列并按照顺序排序。我们需要设置三个指针,如图1所示。Aptr指向A序列的第一个元素(1),Bptr指向B序列的第一个元素(2),Cptr指向合并后的第一个元素。这里的指针其实是三个整数,分别存储的三个元素的下标。首先比较Aptr和Bptr指向的元素那个比较小,选择比较小的放入Cptr所代表的下标的那个位置。然后将Aptr加1,Cptr加1,用Bptr所代表的的下标的元素和Aptr所代表的的下标的元素比较,发现Bptr下标的元素(2)小,将2存入Cptr代表下标的元素,即C[Cptr]。然后依次类推。

 1.jpg

图1

时间复杂度T(N)=O(N)

伪代码描述如下:

9.5.2递归算法

1.分而治之

如图2所示,该算法是稳定的

2.jpg 

图2

伪码描述如下:

    时间复杂度为:T(N)=T(N/2)+T(N/2)+O(N),即T(N)=O(NlogN)。

2.统一函数接口

为了与前面的函数接口统一,因此我们需要再写一个函数来统一函数接口。其伪码描述如下:

如果只在Merge中声明临时数组,那么两个子函数声明如下:

void Merge( ElementType A[], int L, int R, int RightEnd )

void MSort( ElementType A[], int L, int RightEnd )

    这样也不是不行,但是这样的话,每一次在子函数里面用一次释放一次,增加了时间复杂度,还不如直接在最外层的代码里面定义好。

整体的源代码实现如下:

9.5.3 非递归算法

非递归算法的图示如图3所示。

1526113355712562.jpg 

图3

额外空间复杂度是O(N)。我们只需要开一个临时数组就可以了,没必要每次都开一个。

其伪码描述如下:

统一接口如下:

具有稳定性!

缺点是需要额外空间之类的~此方法主要用在外排序。

C语言实现源代码: