首先我们要讲排序是否稳定,所谓排序稳定就是指:如果两个数相同,对他们进行的排序结果为他们的相对顺序不变。例如A={1,2,1,2,1}这里排序之后是A = {1,1,1,2,2} 稳定就是排序后第一个1就是排序前的第一个1,第二个1就是排序前第二个1,第三个1就是排序前的第三个1。同理2也是一样。不稳定呢就是他们的顺序不应和开始顺序一致。也就是可能会是A={1,1,1,2,2}这样的结果。
原地排序:原地排序就是指不申请多余的空间来进行的排序,就是在原来的排序数据中比较和交换的排序。例如快速排序,堆排序等都是原地排序,合并排序,计数排序等不是原地排序。
一、选择排序
1. 基本思想:
每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
选择排序是属于稳定排序,原地排序,其算法时间复杂度为O(N2)。它的好处就是每次只选择确定的元素,不会对很多数据进行交换。所以在数据交换量上应该比冒泡小。
2. 排序过程:
【示例】:
初始关键字 [49 38 65 97 76 13 27 49]
第一趟排序后 13 [38 65 97 76 49 27 49]
第二趟排序后 13 27 [65 97 76 49 38 49]
第三趟排序后 13 27 38 [97 76 49 65 49]
第四趟排序后 13 27 38 49 [49 97 65 76]
第五趟排序后 13 27 38 49 49 [97 97 76]
第六趟排序后 13 27 38 49 49 76 [76 97]
第七趟排序后 13 27 38 49 49 76 76 [97]
最后排序结果 13 27 38 49 49 76 76 9
void swap (int *a, int *b) {
int tmp; tmp = *a; *a = *b; *b = tmp;
}
void SelectionSort (int* arr, int length) {
int i, j, MaxPos;
for (i = 0; i < length-1; i++) {
MaxPos = i; //记录最大值的下标
for (j = i+1; j < length; j++)
if (arr[MaxPos] > arr[j])
MaxPos = j;
if(MaxPos != i)
swap (&arr[MaxPos], &arr[i]);
}
} /*SelectionSort */
二、插入排序
插入排序(Insertion Sort)的基本思想是:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子文件中的适当位置,直到全部记录插入完成为止。
其时间复杂度为O(N2),稳定排序,原地排序。插入排序的速度还是很快的,特别是在数组已排好了之后,用它的思想来插入一个数据,效率是很高的。因为不用全部排。他的数据交换也很少,只是数据后移,然后放入要插入的数据。(这里不是指调用插入排序,而是用它的思想)。在数据大部分都排好的情况下,用插入排序会非常方便,数据的移动和交换都很少。
void InsertSort (int *array, int length)
{ /* 对记录数组array做直接插入排序,length为数组中待排序记录的数目*/
int i, j;
for (i = 2; i <= length; i++){ /*array数组从1开始*/
if (array[i] < array[i-1]){
array[0] = array[i]; /*将待插入记录存放到监视哨array[0]中*/
j = i-1;
while (array[0] < array[j]){ /* 寻找插入位置 */
array[j+1] = array[j];
j--;
}
array[j+1] =array[0]; /*将待插入记录插入到已排序的序列中*/
}
}
} /* InsertSort */
哨兵(监视哨)array[0]有两个作用:一是作为临变量存放array[i](当前要进行比较的关键字)的副本;二是在查找循环中用来监视下标变量j是否越界。
当文件的初始状态不同时,直接插入排序所耗费的时间是有很大差异的。最好情况是文件初态为正序,此时算法的时间复杂度为O(N),最坏情况是文件初态为反序,相应的时间复杂度为O(N2),算法的平均时间复杂度是O(N2)。算法的辅助空间复杂度是O(1),是一个就地排序。
下面是一个折半插入排序:
void BinInsertSort (int array[], int length)
{/*对记录数组r进行折半插入排序,length为数组的长度*/
int i, j, temp, low, high, mid;
for (i = 2; i <= length; i++) {
temp = array[i];
low = 1;
high = i-1;
while (low <= high ) { /* 确定插入位置*/
mid = (low+high)/2;
if (temp < array[mid]) high = mid-1;
else low = mid+1;
}
for (j = i-1; j >= low; j--)
array[j+1]= array[j]; /* 记录依次向后移动 */
array[low] = temp; /* 插入记录 */
}
}/*BinInsertSort*/
三、冒泡排序
[算法思想]:将被排序的记录数组R[1..n]垂直排列,每个记录R[i]看作是重量为R[i].key的气泡。根据轻气泡不能在重气泡之下的原则,从下往上扫描数组R:凡扫描到违反本原则的轻气泡,就使其向上"飘浮"。如此反复进行,直到最后任何两个气泡都是轻者在上,重者在下为止。
其时间复杂度为O(N2),稳定排序,原地排序。冒泡排序的思想很不错,一个一个比较,把小的上移,依次确定当前最小元素。因为他简单,稳定排序,而且好实现,所以用处也是比较多的。还有一点就是加上哨兵之后他可以提前退出。
void BubbleSort(int array[], int length )
{/*对记录数组r做冒泡排序,length为数组的长度*/
int i, j, temp;
bool change;
change = true;
for (i = 1; i < length && change; i++) { /*数组是从1开始*/
change = false;
for (j = 1; j <= length-i ; j++)
if (array[j] > array[j+1]) {
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
change = true;
}
}
} /* BubbleSort */
四、希尔排序
算法思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插人排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。该方法实质上属于插入类排序,是将整个无序列分割成若干小的子序列分别进行插入排序。
其算法时间复杂度为O(n*log(n)),非稳定排序,原地排序。
给定实例的shell排序的排序过程,假设待排序文件有10个记录,其关键字分别是:
49,38,65,97,76,13,27,49,55,04。
增量序列的取值依次为:
5,3,1
Shell排序的算法实现
排序过程:先取一个正整数d1<n,把所有序号相隔d1的数组元素放一组,组内进行直接插入排序;然后取d2<d1,重复上述分组和排序操作;直至di=1,即所有记录放进一个组中排序为止
初始:(1):d=5
49 38 65 97 76 13 27 49* 55 04
|-----------------| 49 13
|-----------------| 38 27
|-----------------| 65 49*
|------------------| 97 55
|------------------| 76 04
一趟结果
13 27 49* 55 04 49 38 65 97 76
(2):d=3
13 27 49* 55 04 49 38 65 97 76
|-----------|----------|----------| 13 55 38 76
|------------|---------| 27 04 65
|-----------|----------| 49* 49 97
二趟结果
13 04 49* 38 27 49 55 65 97 76
(3):d=1
13 04 49* 38 27 49 55 65 97 76
|---|---|---|---|--|---|--|---|---|
三趟结果
04 13 27 38 49* 49 55 65 76 97
算法描述:
void ShellInsert (int *array, int length, int delta)
{/*对记录数组r做一趟希尔插入排序,length为数组的长度,delta 为增量*/
int i, j;
for (i = 1+delta; i <= length; i++) /* 1+delta为第一个子序列的第二个元素的下标 */
if (array[i] < array[i-delta])
{
array[0] = array[i]; /*备份array[i] (不做监视哨) */
for (j = i-delta; j > 0 && array[0]<array[j]; j -= delta)
array[j+delta] = array[j];
array[j+delta] = array[0];
}
}/*ShellInsert*/
void ShellSort(int array[], int length, int delta[], int n)
{/*对记录数组r做希尔排序,length为数组array的长度,delta 为增量数组,n为delta[]的长度 */
int i;
for (i = 0; i <= n-1; i++)
ShellInsert (array, length, delta[i]);
}/*ShellSort*/
注意:
当增量d=1时,ShellSort和ShellInsert 基本一致,只是由于没有哨兵而在内循环中增加了一个循环判定条件"j>0",以防下标越界。
算法分析
1.增量序列的选择
Shell排序的执行时间依赖于增量序列。好的增量序列的共同特征:
① 最后一个增量必须为1;
② 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
2.Shell排序的时间性能优于直接插入排序,希尔排序的时间性能优于直接插入排序的原因:
①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
②当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。
③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
因此,希尔排序在效率上较直接插人排序有较大的改进。
3.稳定性
希尔排序是不稳定的。参见上述实例,该例中两个相同关键字49在排序前后的相对次序发生了变化。
五、堆排序
1、 堆排序定义
n个关键字序列Kl,K2,…,Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质): (1)Ki≤K2i且Ki≤K2i+1
(2)Ki≥K2i且Ki≥K2i+1 (1≤i≤ )
若将此序列所存储的向量R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。
【例】关键字序列(10,15,56,25,30,70)和(70,56,30,25,15,10)分别满足堆性质(1)和(2),故它们均是堆,其对应的完全二叉树分别如小根堆示例和大根堆示例所示。
2、大根堆和小根堆
根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最小者的堆称为小根堆。
根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆。
注意:
①堆中任一子树亦是堆。
②以上讨论的堆实际上是二叉堆(Binary Heap),类似地可定义k叉堆。
3、堆排序特点
堆排序(HeapSort)是一树形选择排序。在排序过程中,将R[l..n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系【参见二叉树的顺序存储结构】,在当前无序区中选择关键字最大(或最小)的记录。
4、堆排序与直接插入排序的区别
直接选择排序中,为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n]中选出关键字最小的记录,又需要做n-2次比较。事实上,后面的n-2次比较中,有许多比较可能在前面的n-1次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执行了这些比较操作。堆排序可通过树形结构保存部分比较结果,可减少比较次数。
5、堆排序
堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。
(1)用大根堆排序的基本思想
① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区
② 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key
③ 由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。
……
直到无序区只有一个元素为止。
(2)大根堆排序算法的基本操作:
① 初始化操作:将R[1..n]构造为初始堆;
② 每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
注意:
①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。
②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻,堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。
(3)堆排序的算法:
void Heapify (int *array, int k, int length)
{ /* 假设array[k..length]是以array[k]为根的完全二叉树,且分别以array[2k]和array[2k+1]
为根的左、右子树为大根堆,调整array[k],使整个序列array[k..m]满足堆的性质 */
int temp, i, j, flag = 0;
temp = array[k]; /* 暂存"根"记录array[k] */
i = k;
j = 2*i;
while (j <= length && !flag){
if (j < length && array[j]<array[j+1])
j++; /* 若存在右子树,且右子树根的关键字大,则沿右分支"筛选" */
if (temp >= array[j]) /* 筛选完毕 */
flag = 1;
else {
array[i] = array[j];
i = j;
j = 2*i;
} /* 继续筛选 */
}
array[i] = temp; /* array[k]填入到恰当的位置 */
}
void BuildHeap (int *array, int length)
{ /*对记录数组array建堆,length为数组的长度*/
int i;
for (i = length/2; i > 0; i--) /* 自第[length/2]个记录开始进行筛选建堆 */
Heapify(array, i, length);
}
void HeapSort(int array[], int length )
{/*对array[1..n]进行堆排序,执行本算法后,array中记录按关键字由大到小有序排列*/
int i, temp;
BuildHeap (array, length);
for (i = length; i >= 1; i--){
temp = array[1];
array[1] = array[i]; /* 将堆顶记录和堆中的最后一个记录互换 */
array[i] = temp;
Heapify (array, 1, i - 1); /* 进行调整,使array1..i-1]变成堆 */
}
} /* HeapSort */
(4) BuildHeap和Heapify函数的实现
因为构造初始堆必须使用到调整堆的操作,先讨论Heapify的实现。
① Heapify函数思想方法
每趟排序开始前R[l..i]是以R[1]为根的堆,在R[1]与R[i]交换后,新的无序区R[1..i-1]中只有R[1]的值发生了变化,故除R[1]可能违反堆性质外,其余任何结点为根的子树均是堆。因此,当被调整区间是R[low..high]时,只须调整以R[low]为根的树即可。
"筛选法"调整堆
R[low]的左、右子树(若存在)均已是堆,这两棵子树的根R[2low]和R[2low+1]分别是各自子树中关键字最大的结点。若R[low].key不小于这两个孩子结点的关键字,则R[low]未违反堆性质,以R[low]为根的树已是堆,无须调整;否则必须将R[low]和它的两个孩子结点中关键字较大者进行交换,即R[low]与R[large](R[large].key=max(R[2low].key,R[2low+1].key))交换。交换后又可能使结点R[large]违反堆性质,同样由于该结点的两棵子树(若存在)仍然是堆,故可重复上述的调整过程,对以R[large]为根的树进行调整。此过程直至当前被调整的结点已满足堆性质,或者该结点已是叶子为止。上述过程就象过筛子一样,把较小的关键字逐层筛下去,而将较大的关键字逐层选上来。因此,有人将此方法称为"筛选法"。
②BuildHeap的实现
要将初始文件R[l..n]调整为一个大根堆,就必须将它所对应的完全二叉树中以每一结点为根的子树都调整为堆。
显然只有一个结点的树是堆,而在完全二叉树中,所有序号 的结点都是叶子,因此以这些结点为根的子树均已是堆。这样,我们只需依次将以序号为 , -1,…,1的结点作为根的子树都调整为堆即可。
5、 算法分析
堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。
堆排序的最坏时间复杂度为O(nlogn)。堆排序的平均性能较接近于最坏性能。
由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
堆排序是就地排序,辅助空间为O(1),它是不稳定的排序方法。
六、归并排序
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
归并操作的工作原理如下:
1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
2、设定两个指针,最初位置分别为两个已经排序序列的起始位置;
3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
4、重复步骤3直到某一指针达到序列尾;
5、将另一序列剩下的所有元素直接复制到合并序列尾。
算法分析:时间复杂度O(nlog(n)),稳定排序,非原地排序,空间复杂度O(n)。他的思想是分治,先分成小的部分,排好部分之后合并,因为我们另外申请的空间,在合并的时候效率是0(n)的。速度很快。貌似他的上限是n*log(n),所以如果说是比较的次数的话,他比快速排序要少一些。对任意的数组都能有效地在n*log(n)排好序。但是因为他是非原地排序,所以虽然他很快,但是貌似他的人气没有快速排序高。
void Merge (int array[], int first, int mid, int last)
{/* 已知array[first..mid]和array[mid+1..last]分别按关键字有序排列,将它们合并成一个有序序列 */
int i, begin1, begin2;
int *temp = (int *) malloc ((last-first+1)*sizeof (int));
begin1 = first;
begin2 = mid+1;
i = 0;
while (begin1 <= mid && begin2 <= last){
if (array[begin1] < array[begin2]){
temp[i] = array[begin1];
begin1++;
}
else {
temp[i] = array[begin2];
begin2++;
}
i++;
}
while (begin1 <= mid){
temp[i] = array[begin1];
i++;
begin1++;
}
while (begin2 <= last){
temp[i] = array[begin2];
i++;
begin2++;
}
for (i = 0; i < (last-first+1); i++)
array[first+i] = temp[i];
free (temp);
}/* Merge */
void MergeSort (int array[], int first, int last)
{
int mid = 0;
if (first < last){
mid = (first + last)/2;
MergeSort (array, first, mid);
MergeSort (array, mid+1, last);
Merge (array, first, mid, last);
}
} /* MergeSort */
七、快速排序
1、算法思想
(1)分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。
(2)快速排序的基本思想
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用第一个数据)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。一趟快速排序的算法是:
1)设置两个变量I、J,排序开始的时候:I=0,J=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即 key=A[0];
3)从J开始向前搜索,即由后开始向前搜索(J=J-1),找到第一个小于key的值A[J],并与A[I]交换;
4)从I开始向后搜索,即由前开始向后搜索(I=I+1),找到第一个大于key的A[I],与A[J]交换;
5)重复第3、4、5步,直到 I=J;
(3,4步是在程序中没找到时候j=j-1,i=i+1,直至找到为止。找到并交换的时候i, j指针位置不变。另外当i=j这过程一定正好是i+或j-完成的最后另循环结束)
(3)算法分析: 时间复杂度:n*log(n),不稳定排序,原地排序。
2、参考代码:
void QuickSort (int array[], int left, int right)
{
int middle, i, j, temp;
middle= Middle (array[left], array[(left+right)/2], array[right]);//三数中值法
i = left;
j = right;
while (i <= j){ //如果两边扫描的下标交错,就完成一次排序
while (array[i] < middle && i < right) //从左扫描大于中值的数
i++;
while (array[j] > middle && j > left) //从右扫描小于中值的数
j--;
if (i <= j){ //找到了可交换一对值
temp = array[i];
array[i] = array[j];
array[j] = temp;
i++;
j--;
}
}
if (left < j) //当左边部分有值(left<j),递归左半边
QuickSort (array, left, j);
if (right > i) //当右边部分有值(right>i),递归右半边
QuickSort (array, i, right);
} /* QuickSort */
八、桶排序
九、基数排序