参考书籍:数据结构(C语言版)严蔚敏吴伟民编著清华大学出版社
本文中的代码可从这里下载:https://github.com/qingyujean/data-structure
1.简单选择排序
1.1基本思想
通过 n-i 次关键字间的比较,从无序序列[i..n]的 n-i+1 记录中选出关键字最小的记录加入有序序列(即作为有序序列中的第i个记录)。
1.2代码实现
package sort.selectionSort;
public class SimpleSelectSort {
/**
* @param args
*/
public static int selectMin(int[] L, int i){
int min = L[i], min_k = i;
for(int k = i+1; k <= L.length-1; k++)
if(L[k] < min){
min = L[k];
min_k = k;
}
return min_k;
}
//对顺序表L做简单选择排序
public static void selectSort(int[] L){
//从i后面的子序列中(即从i->n中)选出一个min插入到i的位置上
for(int i = 1; i < L.length-1; i++){//i的取值是1...n-1
int j = selectMin(L, i);//从L[i...n]里选出一个min,并返回其下标
if(j!=i){//交换L[i]与L[j]
L[0] = L[i];
L[i] = L[j];
L[j] = L[0];
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] test = {0, 8, 3, 9, 1, 6}; //0号单元未使用
selectSort(test);
for(int i = 1; i <= test.length-1; i++)
System.out.print(test[i]+" ");
}
}
运行结果:
1.3性能分析
由于存在着不相邻元素之间的互换,因此,简单选择排序是“不稳定的” 。
算法实现共需要进行 n-1 次选择,每次选择需要进行n-i次比较(1≤i≤n-1),而每次交换最多需3次移动,因此,总的比较次数 C = n(n-1)/2,总的移动次数 M = 3(n-1)。故其时间复杂度为O(n^2)。
选择排序的主要操作是进行关键字间的比较,因此改进选择排序应从如何减少 “比较” 考虑。显然,在n个关键字中选出最小值,至少进行n-1次比较,然而,继续在剩余的n-1个关键字中选择次小值,是否一定要进行n-2次比较呢?如果能利用前n-1次比较所得信息,就可减少以后各趟选择排序中所用的比较次数。如树形选择排序,又称锦标赛排序,其过程: 首先对n个记录的关键字两两比较,然后在 n/2 个较小者之间再进行两两比较,如此重复,直至选出最小关键字的记录为止。此对应的过程可以用一棵有n个叶子结点的完全二叉树表示。实际上,体育比赛中的锦标赛便是一种选择排序。
再例如后面提到的堆排序。
2.堆排序
堆的定义:若有n个元素的排序码k1,k2,k3,…,kn,当满足如下条件:
其中i=1,2,…,[n/2]向下取整,则称此n个元素的排序码k1,k2,k3,…,kn为一个堆。若将此排序码按顺序组成一棵完全二叉树,则(1)称为小根堆(二叉树的所有根结点值小于或等于左右孩子的值),(2)称为大根堆(二叉树的所有根结点值大于或等于左右孩子的值)。
例如:判定序列{96,83,27,38,11,09}、{12,36,24,85,47,30,53,91 }是否堆
将排序码按顺序组成一棵完全二叉树,则易判别。
小根堆:二叉树的所有根结点值小于或等于左右孩子的值;
大根堆:二叉树的所有根结点值大于或等于左右孩子的值;
2.1基本思想
将堆中第一个结点(二叉树根结点)和最后一个结点的数据进行交换(k1与kn),再将k1~kn-1重新建堆,然后k1和kn-1交换,再将 k1~kn-2重新建堆,然后k1和kn-2交换,如此重复下去,每次重新建堆的元素个数不断减1,直到重新建堆的元素个数仅剩一个为止。这时堆排序已经完成,则排序码k1,k2,k3,…,kn已排成一个有序序列。
实现堆排序需要解决两个问题:
(1)如何由一个无序序列建成一个堆?
(2)如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
例:图(a)是个堆,假设输出堆顶元素之后,以堆中最后一个元素替代之,如图(b)所示。此时根结点的左、右子树均为堆,则仅需自上至下进行调整即可。
这个自堆顶至叶子的调整过程称为“筛选”。
概念:“筛选”
假若完全二叉树的某一个结点 i,它的左、右子树已是堆。需要将R[2i].key与R[2i+1].key之中的最小者与R[i].key比较,若R[i].key较大则交换,这有可能破坏下一级堆。于是继续采用上述方法构造下一级堆,直到完全二叉树中结点i构成堆为止。这个自堆顶到叶子的调整过程为“筛选”。
建堆
将排序码{ k1,k2,k3 … ,kn }表示成一棵完全二叉树,然后从第 [n/2]向下取整 个排序码(即最后一个非终端结点)开始筛选,使由该结点作为根结点组成的子二叉树符合堆的定义,然后从第 [n/2]向下取整- 1个排序码重复刚才操作,直到第一个排序码(即根)止。这时候,该二叉树符合堆的定义,初始堆已经建立。
2.2代码实现
例:关键字序列T= (21,25,49,25,16,08),请建大根堆。然后对建好的大根堆进行排序
package sort.selectionSort;
public class HeapSort {
/**
* @param args
*/
//已知H[s...m]中记录的关键字除H[s]之外其他关键字均满足大顶堆的定义,本函数则
//实现调整H[s]以使得H[s...m]成为一个大顶堆
public static void heapAdjust(int[] H, int s, int m){
//s指向暂时的堆顶位置,但H[s]使得不满足大顶堆的定义,所以调整H[s],使得H[s...m]成为一个大顶堆
//这个过程叫做一次“筛选”:从堆顶到叶子节点的调整
for(int j = 2*s; j <= m; j *= 2){//j = j * 2,即从当前层往下一层考察
if(j<m && H[j+1] >H[j])//j<m是为了防止H[j+1]的j+1越界
j++;//j总指向s的2个孩子里大的那个
if(H[j] > H[s]){
H[0] = H[s];
H[s] = H[j];
H[j] = H[0];
}else
break;//如果新换上来的堆顶本身就并不破坏堆得定义,则无需继续调整,因为他下面的元素是一定满足堆的
s = j;//s调整好后,往下一层考察,直至到叶子节点
}
}
public static void heapSort(int[] H){
int n = H.length-1;
//建堆
for(int i = n/2; i > 0; i--){//将H[1...n]建成大顶堆,从第n/2个元素开始调整,即“筛选”过程
//即调整元素的顺序是第n/2个元素,第n/2 - 1个元素,...,第1个元素
//每一次筛选都是一次从当前元素(局部的堆顶到叶子节点的调整)
heapAdjust(H, i, n);//每一次调整,都使得H[i...n]满足大顶堆的定义,直至整个H[1...n]都满足大顶堆定义
}
//堆排序
//总是将最后一个元素与堆顶互换,即越大的数慢慢往后排,互换以后要做调整使剩余的元素仍然满足大顶堆的定义
for(int i = n; i > 0; i--){//i指向当前还未排好序的子序列的“最后一个元素”,而i后面的是已经排好序的,
//即每次从堆顶输出的那个最大值,排到i指向的这个最后位置
H[0] = H[i];
H[i] = H[1];//H[1]是当前的堆顶元素,是当前最大值,排到最后面去
H[1] = H[0];//由于H[i]跑到了堆顶位置可能破坏堆,需要调整
heapAdjust(H, 1, i-1);//从堆顶调整到i-1对应的节点
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] testH = {0, 21, 25, 49, 25, 16, 8}; //0号单元未使用
//堆采用顺序表存储
heapSort(testH);
for(int i = 1; i <= testH.length-1; i++)
System.out.print(testH[i]+" ");
}
}
运行结果:
2.3性能分析
在整个堆排序中,共需要进行 n+[n/2]向下取整 -1 次筛选运算,每次筛选运算进行双亲和孩子或兄弟结点的排序码的比较和移动次数都不会超过完全二叉树的深度。故每次筛选运算的时间复杂度为O(log2n),则整个堆排序过程的时间复杂度为O(nlog2n) 。
堆排序在最坏情况下,时间复杂度也为O(nlog2n)。相对于快速排序,这是堆排序的最大优点。此外,堆排序仅需一个记录大小辅助存储空间供交换使用。
由于存在着不相邻元素之间的互换,因此,堆排序是一种不稳定的排序方法。