回顾基础的排序算法
在数据结构这门课程里,学校的老师们是较侧重于教学生如何利用诸如C语言来实现线性表,队列,栈,树这些简单的数据结构,理解这些数据结构的用法,而往往轻视了后半块关于图论,排序,查找等偏算法分析方面的内容的教学。
虽然课程的名字叫做数据结构,但很多CS强校和国外的著作都习惯地把它称作“数据结构与算法分析”。说到底,数据结构只能算是对编程语言使用的一个延伸,很多程序设计入门书诸如《C Primer Plus》和《C和指针》的后半段都会谈一些链表,树等内容,所以现在回过头来看,前面数据结构的学习还是为了在后面实现一些复杂的算法的时候,能够更好地利用这些结构来处理数据和实现算法中的思想。
在期末考试中,我们最后的大题目往往都是解答题,要求学生用编程语言实现诸如快速排序这样的算法,那么这样来看的话,独立实现这些复杂的算法,便就是真正考验这门课程是否学好的重要标准之一。
这一篇就先回顾排序算法 。
冒泡排序(Bubble Sort) 冒泡排序是一种比较简单的排序算法,通常被用来对于刚入门学习程序设计的学生介绍算法的概念。
算法原理:
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
算法分析:
综上,因此冒泡排序总的平均时间复杂度为O(n²)。
算法稳定性: 由于冒泡算法的核心思想就是把小的元素往前调,大的元素往后调,相同的元素不做处理,所以即使相同的元素中间还隔着其他元素,这两个元素也不会发生交换,所以冒泡排序是一种稳定的排序算法。
算法实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef int ElemType;void BubbleSort ( ElemType A[], int N ) { int i, j; ElemType Tmp; for ( i = 0 ; i < N - 1 ; i++) for ( j = 0 ; j < N - 1 - i; j++) { if ( A[ j ] > A[ j + 1 ] ) { Tmp = A[ j ]; A[ j ] = A[ j + 1 ]; A[ j + 1 ] = Tmp; } } }
插入排序(Insertion Sort) 插入排序由N-1趟排序组成,并且利用了这样的事实:位置0到位置P-1上的元素是排过序的。
它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法原理:
从第一个元素开始,该元素可以认为已经被排序。
取出下一个元素,在已经排序的元素序列中从后向前扫描。
如果该元素(已排序)大于新元素,将该元素移到下一位置。
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
将新元素插入到该位置后
重复步骤2~5
算法分析:
由于嵌套循环的每一个都花费N次迭代,因此插入排序为O(n²),而且这个界是精确的,因为以反序输入同样可以达到该界。
如果输入数据是有序的,那么运行时间为O(n),因为内层的for循环的检测总是立即判定不成立而终止。
综上,插入排序的平均情形也是O(n²)。
算法稳定性: 插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
算法实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 typedef int ElemType;void InsertionSort ( ElemType A[], int N ) { int j, P; ElemType Tmp; for ( P = 1 ; P < N; P++ ) { Tmp = A[ P ]; for ( j = P; j > 0 && A[ j - 1 ] > Tmp; j-- ) A[ j ] = A[ j - 1 ]; A[ j ] = Tmp; } } ``` --- # 希尔排序:(Shell Sort) 希尔排序是插入排序的一种更高效的改进版本,通过比较相距一定距离的元素来工作,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止,所以希尔排序有时也叫缩小增量排序。 增量是希尔排序的重要部分,只要最终增量为1 任何增量序列(*increment sequence*)都可以工作。算法最开始以一定的增量进行排序。然后会继续以一定增量进行排序,最终算法以增量为1 进行排序。当增量为1 时,算法变为插入排序,这就保证了数据一定会被排序。 ## 算法原理: 先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量`d2<d1`重复上述的分组和排序,直至所取的增量`dt=1 (dt<dt-l<…<d2<d1)`,即所有记录放在同一组中进行直接插入排序为止。 增量的选择有很多,并且关乎算法的效率。增量序列的一种流行选择是使用Shell建议的序列,`d1 = [ N / 2 ]`和`d(t) = [ d(t-1 )/2 ]`。 已知的最好步长串行是由Sedgewick提出的 `(1 , 5 , 19 , 41 , 109 ,...)`,该串行的项来自 `9 * 4 ^i - 9 * 2 ^i + 1 ` 和 `4 ^i - 3 * 2 ^i + 1 ` 这两个算式,这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长串行的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。 ## 算法分析: #### 优劣 希尔排序是基于插入排序的一种算法,在此算法基础之上增加了一个新的特性,提高了效率。希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n²),而Hibbard增量的希尔排序的时间复杂度为O(n^3 /2 ),但是现今仍然没有人能找出希尔排序的精确下界。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(n²)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 #### 时间性能 1. 增量序列的选择Shell排序的执行时间依赖于增量序列。 好的增量序列的共同特征: - 最后一个增量必须为1 ; - 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。 有人通过大量的实验,给出了较好的结果:当n较大时,比较和移动的次数约在nl.25 到1.6 n1.25 之间。 2 . Shell排序的时间性能优于直接插入排序希尔排序的时间性能优于直接插入排序的原因: ①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。 ②当n值较小时,n和的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0 ()差别不大。 ③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1 作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。 因此,希尔排序在效率上较直接插入排序有较大的改进。 ## 算法稳定性: 由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。 ## 算法实现: ```C typedef int ElemType;void Shellsort ( ElemType A[], int N ) { int i, j, Increment; ElemType Tmp; for ( Increment = N / 2 ; Increment > 0 ; Increment /= 2 ) for ( i = Increment; i < N; i++ ) { Tmp = A[ i ]; for ( j = i; j >= Increment; j -= Increment ) if ( Tmp < A[ j - Increment ] ) A[ j ] = A[ j - Increment]; else break ; A[ j ] = Tmp; } }
选择排序(Selection sort) 选择排序(Selection sort)是一种简单直观的排序算法。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
算法原理:
从未排序的数组中选择最小的元素,组成有序序列。
从无序序列中继续挑选最小的元素,插入有序序列。
以此类推,直到全部转化为有序序列。
算法分析: 选择排序的交换操作介于0和(n-1)次之间。选择排序的比较操作为n(n-1)/2次之间。选择排序的赋值操作介于0和3(n-1)次之间。
比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2
。 交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换n-1次。 交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。
算法稳定性: 选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
算法实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 typedef int ElemType;void Selectsort ( ElemType A[], int N ) { int i, j, loc; ElemType min, Tmp; for ( i = 0 ; i < N; i++) { min = A[ i ]; for ( j = i; j < N; j++) { if ( A[ j ] <= min ) { min = A[ j ]; loc = j; } } Tmp = A[ i ]; A[ i ] = A[ loc ]; A[ loc ] = Tmp; } }
堆排序(Heapsort) 堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆节点的访问 通常堆是通过一维数组来实现的。在起始数组为 0 的情形中:
父节点i的左子节点在位置 (2*i+1);
父节点i的右子节点在位置 (2*i+2);
子节点i的父节点在位置 floor((i-1)/2);
堆的操作 在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义以下几种操作:
最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
创建最大堆(Build_Max_Heap):将堆所有数据重新排序
堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
算法原理:
先将无序的数组建立成一个最大堆(即最大值位于根结点的完全二叉树)。
依次从最大堆中删除最大值,放入数组尾部,并将最大堆重新调整。直到全部删除为止。
算法分析: 堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用PercDown(下滤算法)实现的。
平均性能 O(N*logN)。
其他性能
由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
堆排序是就地排序,辅助空间为O(1).
在最坏的情况下,堆排序最多使用2NlogN - O(N)
次比较。
算法稳定性: 它是不稳定的排序方法。
算法实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #define LeftChild(i) (2*(i)+1) typedef int ElemType;void PercDown ( ElemType A[], int i, int N ) { int Child; ElemType Tmp; for ( Tmp = A[ i ]; LeftChild( i ) < N; i = Child ) { Child = LeftChild( i ); if ( Child != N - 1 && A[ Child + 1 ] > A[ Child ]) Child++; if ( Tmp < A[ Child ]) A[ i ] = A[ Child ]; else break ; } A[ i ] = Tmp; } void Heapsort ( ElemType A[], int N ) { int i; ElemType Tmp; for ( i = N / 2 ; i >= 0 ; i-- ) PercDown( A, i, N); for ( i = N - 1 ; i > 0 ; i-- ) { Tmp = A[ 0 ]; A[ 0 ] = A[ i ]; A[ i ] = Tmp; PercDown( A, 0 , i ); } }