插入排序算法
插入排序是最简单的排序算法。插入排序有N-1趟排序组成。对于P=1趟到P=N-1趟,插入排序保证从位置0到位置P上的元素已为排序状态。插入排序利用了这样的事实:位置0到位置P-1上的元素是排过序的。图1显示一个简单的数组在每一趟排序后的情况。
图1:每趟后的插入排序
如下代码实现了上述想法:
void InsertionSort(ElementType A[], int N) { int j, p; ElementType Tmp; for (p = 1; p < N; ++p){ Tmp = A[p]; for (j = p; A[j - 1] > Tmp && j > 0; --j){ A[j] = A[j - 1]; } A[j] = Tmp; } }
在第2行到第5行实现数据移动而没有明显使用交换。位置P上的元素存于Tmp,而在位置P之前所有更大的元素都被向右移动一个位置。然后Tmp被置于正确的位置上。
从代码中可以看出,插入排序复杂度是O(N^2),但如果输入数据已预先排序,那么运行时间为O(N)。
希尔排序
通过比较相距一定间隔的元素来工作,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。所以希尔排序也叫缩小增量排序。希尔排序使用一个序列h1,h2,....,hn,叫做增量序列,只要h1=1,任何增量序列都是可以的,不过有些增量序列比另外一些增量序列更好。在使用增量hk的一趟排序后,对于每一个i,我们有A[i] <= A[i+hk],即所有相隔hk的元素都被排序,此时和排序文件是hk-排序的。希尔排序的一个重要性质是:一个hk-排序的文件保持它的hk-排序性。如果情况不是这样的话,希尔排序也就没有意义了。
hk-排序的一般做法是:对于hk,hk+1,...,N-1中的每一个位置i,把其上的元素放到i,i-hk,i-2*hk....中间的正确位置上。一趟hk排序的作用就是对hk个独立的子数组执行一次插入排序。
希尔排序的运行时间依赖于增量序列,但最坏情形下的运行时间为O(N^2),但对于有些增量序列,其时间可减少到O(N^1.2)。以下是以增量序列:1,2,4,N/2的一种实现:
void Shellsort(ElementType A[], int N) { int i, j, Increment; ElementType 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; } } }
堆排序
堆排序主要是从二叉堆中通过删除最小元(树根)而得。回忆一下,建立N个元素的堆,需要线性O(N)的时间,然后执行N次DeleteMin操作。按照顺序,最小的元素先离开该堆。通过将这些元素离开的顺序即可得到N个元素的排序,因此总的时间复杂度是O(NlogN)。经验指出,堆排序是一个非常稳定的算法,它平均使用的比较只比最坏情形界指出的略少。执行堆排序的代码如下:
#define LeftChild(i) (2 * (i) + 1) void PercDown(ElementType A[], int i, int N) { int child; ElementType 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(ElementType A[], int N) { int i; for (i = N / 2; i >= 0; --i) PercDown(A, i, N); //构造堆 for (i = N -1; i > 0; --i){ Swap(&A[0], &A[i]); //将最大元素(根)与数组末尾元素交换,从而删除最大元素,重新构造堆 PercDown(A, 0, i); } }