7. 内排序
7.1 排序的基本概念
1. 排序的定义
所谓排序,简单地说就是将一组“无序”的记录序列调整为“有序”的举例序列的一种操作。通常待排序的记录有多个数据项,把用于作为排序依据的数据项称为关键字。例如,学生成绩表由学号、姓名和各科成绩等数据项组成,这些数据项都可作为关键字来进行排序。
2. 排序的分类
1)内部排序和外部排序
按照排序过程中所涉及的存储器的不同可分为内部排序与外部排序。内部排序是指待排序序列完全存放在内存中进行的排序过程,这种方法适合数量不太大的数据元素的排序。外部排序是指待排序的数据元素非常多,以至于它们必须存储在外部存储器上,这种排序过程中需要访问外存储器,这样的排序称为外排序。
2)稳定排序与不稳定排序
若对任意一组数据元素序列,使用某种排序算法对它进行按照关键字的排序,若相同关键字间的前后位置关系在排序前与排序后保持一致,则称此排序方法是稳定的,而不能保持一致的排序方法则称为不稳定的。例如,一组关键字序列{3,4,2,3,1},若经过排序后变为{1,2,3,3,4},则此排序方法是稳定的;若经过排序后变为{1,2,3,3,4},则此排序方法是不稳定的。
3. 内排序的方法
内部排序的过程是一个逐步扩大记录的优先序列长度的过程。基于不同的“扩大”有序序列长度的方法,内部排序方法大致可分为以下几种类型:插入类、交换类、选择类、归并类和其他类。
1)插入类排序方法是指将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序子序列的长度。
2)交换类排序方法是指通过“交换”无序序列中的记录,从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
3)选择类排序方法是指从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
4)归并类排序方法是指通过“归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。
4. 排序算法的性能评价
排序算法有很多种,在众多的排序算法中,简单地评价哪一种算法是好的很困难。通常认为某种算法是用于某些情况,而这种算法在另外情况下性能也就不如其他算法。评价排序算法好换的标准主要有两条:算法的时间复杂度和空间复杂度。
5. 待排序记录的类描述
内部排序方法可在不同的存储结构上实现,但待排序的数据元素集合通常以线性表为主,因此存储结构多选用顺序表和链表。此外,顺序表又具有随机存取的特性,因此这儿介绍的排序算法都是针对顺序表进行操作的。
待排序的顺序表记录类描述如下:
package chapter7;
public class RecordNode {
public KeyType key;// 关键字
public ElementType element;// 数据类型
public RecordNode(KeyType key) {
this.key = key;
}
public RecordNode(KeyType key, ElementType element) {
this.key = key;
this.element = element;
}
}
顺序表记录关键字类
package chapter7;
/**
* 顺序表记录关键字类
*
* @author Oner.wv
*
*/
public class KeyType implements Comparable<KeyType> {
public int key;// 关键字
public KeyType() {
}
public KeyType(int key) {
this.key = key;
}
@Override
public String toString() {// 覆盖toString方法
return key + " ";
}
@Override
public int compareTo(KeyType another) {// 覆盖Comparable接口中比较关键字大小的方法
int thisVal = this.key;
int anotherVal = another.key;
return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1));
}
}
顺序表记录节点类
package chapter7;
/**
* 顺序表记录节点类
*
* @author Oner.wv
*
*/
public class ElementType {
public String data;// 用户可自定义其他数据类型
public ElementType() {
}
public ElementType(String data) {
this.data = data;
}
public String toString() {
return data;
}
}
待排序的顺序表类描述如下:
package chapter7;
public class SeqList {
public RecordNode[] r;// 顺序表记录节点数组
public int curlen;// 顺序表长度
// 顺序表的构造方法,构造一个存储空间为maxSize个存储单元
public SeqList(int maxSize) {
this.r = new RecordNode[maxSize];// 为顺序表分配maxSize个存储单元
int curlen = 0;// 置顺序表的当前长度为0
}
// 在当前顺序表的第i个结点之前插入一个RecordNode类型的节点x
public void insert(int i, RecordNode x) throws Exception {
if (curlen == r.length) {// 判断顺序表是否已满
throw new Exception("顺序表已满");
}
if (i < 0 || i > curlen) {
throw new Exception("插入位置不合理");
}
for (int j = curlen; j > i; j--) {
r[i] = r[i - 1];// 插入位置及以后的数据元素后移
}
r[i] = x;// 插入x
this.curlen++;// 表长度加1
}
}
7.2 插入排序
这儿介绍两种插入排序方法:直接插入排序和希尔排序
7.2.1 直接插入排序
直接插入排序(Straight Insertion Sort)是一种简单地排序方法,其基本思想先将原序列分为有序区和无序区,然后再经过比较和后移操作将无序区元素插入到有序区中。
看下面的一个例子:
下标: 0 1 2 3 4
初始关键字:[52] 39 67 70 52
i=1:[39 52] 67 70 52
i=2:[39 52 67] 70 52
i=3:[39 52 67 70] 52
i=4:[39 52 52 67 70]
直接插入排序算法的主要步骤归纳如下:
1)将r[i]暂存在临时变量temp中。
2)将temp与r[j](j=i-1,i-2,...,0)依次比较,若temp.key<r[j].key,则将r[j]后移一个位置,知道temp.key>=r[j].key为止(此时j+1即为r[i]插入位置)。
3)将temp插入到第j+1个位置上。
4)令i=1,2,3,...,n-1,重复步骤1)~3)。
// 直接插入排序
public void insertSort() {
RecordNode temp;
int i, j;
for (i = 1; i < this.curlen; i++) {// n-1趟扫描
temp = r[i];// 将待插入的第i条记录暂存于temp中
for (j = i - 1; j >= 0 && temp.key.compareTo(r[j].key) < 0; j--) {
r[j + 1] = r[j];// 将前面比r[i]大的记录向后移动
}
r[j + 1] = temp;// r[i]插入到第j+1个位置
}
}
上面算法中的“j >= 0 && temp.key.compareTo(r[i].key) < 0”语句中的“j>=0”是用来控制下标越界。为了提高算法 效率,可对该算法进行如下改进:首先将待排序的n条记录从下标为1的存储单元开始一次存放在数组r中,再将顺序表中的第0个存储单元设置为一个“监视哨”,即在查找之前把r[i]赋值给r[0],这样每循环一次只需要进行记录的比较,不需要比较下表是否越界,当比较到第0个位置时,由于“r[0].key==r[i].key”必然成立,将自动退出循环,所以只需设置一个循环条件:“ temp.key.compareTo(r[i].key) < 0”。
改进直接插入排序算法:带监视哨的直接插入排序算法
// 带监视哨的直接插入排序
public void insertSortWithGuard() {
int i, j;
for (i = 1; i < this.curlen; i++) {// n-1趟扫描
r[0] = r[i];// 将待插入的第i条记录暂存在r[0]中,同时r[0]为监视哨
for (j = i - 1; r[0].key.compareTo(r[j].key) < 0; j--) {// 将前面较大的数据元素向后移动
r[j + 1] = r[j];
}
r[j + 1] = r[0];// r[i]插入到第j+1个位置
}
}
算法性能分析:
1)空间复杂度:
从实现原理可知,直接插入排序是在原输入数组上进行后移赋值操作的(称“就地排序”),所需开辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)。
2)时间复杂度:直接插入排序耗时的操作有:比较+后移赋值。时间复杂度如下:最好情况:序列是升序排列,在这种情况下,需要进行的比较操作需(n-1)次。后移赋值操作为0次,即O(n);最坏情况:序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。后移赋值操作是比较操作的次数加上 (n-1)次,即O(n^2);渐进时间复杂度(平均时间复杂度):O(n^2)。
3)算法稳定性:直接插入排序是一种稳定的排序算法。
7.2.2 希尔排序
希尔排序(Shell Sort),又称缩小增量排序,其原理是:先将整个序列分割成若干小的子序列,再分别对子序列进行直接插入排序,使得原来序列成为基本有序。这样通过对较小的序列进行插入排序,然后对基本有序的数列进行插入排序,能够提高插入排序算法的效率。
看下面一个例子:
希尔排序的主要步骤归纳如下:
1)选择一个增量序列{d0,d1,d2,...,d(k-1)}。
2)根据当前增量di将n条记录分成di个子表,每个子表中记录的下标相隔为di。
3)对各个子表中的记录进行直接插入排序
4)令i=0,1,...,k-1,重复上述步骤2)~4)。
// 希尔排序
public void shellSort(int[] d) {// d[]为增量数组
RecordNode temp;
int i, j;
// 控制增量,增量减半,若干趟扫描
for (int k = 0; k < d.length; k++) {
// 一趟中若干子表,每个记录在自己所属子表内进行直接插入排序
int dk = d[k];
for (i = dk; i < this.curlen; i++) {
temp = r[i];
for (j = i - dk; j >= 0 && temp.key.compareTo(r[j].key) < 0; j -= dk) {
r[j + dk] = r[j];
}
r[j + dk] = temp;
}
}
}
算法性能分析:
1)空间复杂度:
从实现原理可知,希尔排序是在原输入数组上进行后移赋值操作的(称“就地排序”),所需开辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)
2)时间复杂度:
希尔排序耗时的操作有:比较 + 后移赋值。时间复杂度如下:最好情况:序列是升序排列,在这种情况下,需要进行的比较操作需(n-1)次。后移赋值操作为0次,即O(n);最坏情况:O(nlog2n);渐进时间复杂度(平均时间复杂度):O(nlog2n)。
3)增量选取:希尔排序的时间复杂度与增量的选取有关,但是现今仍然没有人能找出希尔排序的精确下界。一般的选择原则是:取上一个增量的一半作为此次序列的划分增量。首次选择序列长度的一半为增量。
4)算法稳定性:
希尔排序是不稳定的。因为在进行分组时,相同元素可能分到不同组中,改变相同元素的相对顺序。
7.3 交换排序
交换排序的基本思想是两两比较排序记录的关键字,若两个记录的次序相反则交换这两个记录,知道没有范旭的记录为止。应用交换排序基本思想的主要排序方法有冒泡排序和快速排序。
7.3.1 冒泡排序
冒泡排序(Bubble Sort)的基本思想是将待排序的数组看成从上到下排序,把关键字值较小的记录看成“较轻的”,关键字较大的记录看成是“较重的”,较小关键字值的记录好像水中的气泡一样,向上浮;较大关键字值的记录如水中的石块向下沉,当所有气泡都浮到了相应的位置,并且所有的石头都沉到了水中,排序就结束了。
冒泡排序算法的主要步骤如下:
1)置初值i为1。
2)在无序序列中{r[0],r[1],...,r[n-i]}中,从头至尾依次比较相邻的两个记录r[j]与r[j+1](0<=j<n-i-1),若r[j].key>r[j+1].key,则交换位置。
3)i=i+1。
4)重复步骤2)~3),直到在步骤2)中未发生记录交换或i=n-1为止。
// 冒泡排序
public void bubbleSort() {
RecordNode temp;// 辅助结点
for (int i = 1; i < this.curlen; i++) {
for (int j = 0; j < this.curlen - i; j++) {
if (r[j].key.compareTo(r[j + 1].key) > 0) {// 逆序时,交换
temp = r[j];
r[j] = r[j + 1];
r[j + 1] = temp;
}
}
}
}
如果上面代码中,里面一层循环在某次扫描中没有执行交换,则说明此时序列已经全部有序列,无需再扫描了。因此,增加一个标记,每次发生交换,就标记,如果某次循环完没有标记,则说明已经完成排序。
// 优化后的冒泡排序
public void bubbleSort2() {
RecordNode temp;// 辅助接点
boolean flag = true;// 是否交换的标记
for (int i = 1; i < this.curlen && flag; i++) {// 有标记时再进行下一趟,最多n-1趟
flag = false;// 记录未交换
for (int j = 0; j < this.curlen - i; j++) {// 一次比较、交换
if (r[j].key.compareTo(r[j + 1].key) > 0) {//逆序时,交换
temp = r[j];
r[j] = r[j + 1];
r[j + 1] = temp;
flag = true;
}
}
}
}
算法性能分析:
1)空间复杂度。冒泡排序禁用了一个辅助单元,空间复杂度为O(1)。
2)时间复杂度。最好情况是排序表已有序时,在第一趟比较过程中,一次交换都未发生,所以在执行一趟排序之后就结束,这时只需要比较n-1次,不需移动记录,时间复杂度为O(n);最坏情况为逆序状态,总共要进行n-1趟冒泡排序,在第i趟排序中,比较次数为n-i,移动次数为3(n-i),则总的比较次数为0.5*n(n-1),总的移动此树为1.5n(n-1),时间复杂度为O(n^2)。
3)算法稳定性。冒泡排序是一种稳定的排序算法。
7.3.2 快速排序
快速排序(Quick Sort)是冒泡排序的一种改进算法。快速排序采用了分支策略,即将原问题划分成若干个规模更小但与原问题相似的子问题,然后用递归方法解决这些子问题,最后再将它们组合成原问题的解。
快速排序的基本思想是一趟排序将要排序的记录分割成独立的两个部分,其中一部分的所有记录的关键字值都比另一部分的所有关键字值小,然后再按此方法对这两部分记录分别进行快速排序,整个排序过程可以递归进行,以此达到整个记录序列变成有序。
一趟快速排序算法的主要步骤归纳如下:
1)设置两个变量i、j,初值分别为low和high,分别表示待排序序列的起始下标和终止下标。
2)将第i个记录暂存在变量pivot中,即pivot=r[i]。
3)从下标为j的位置开始由后向前依次搜索,当找到第1个比pivot的关键字值小的记录时,则将该记录向前移动到下标为i的位置上,然后i=i+1。
4)从下标为i的位置开始由前向后依次搜索,当找到第1个比pivot的关键字值大的记录时,则将该记录向后移动到下标为j的位置上,然后j=j-1。
5)重复第3)、4)步骤,知道i==为止。
6)r[i]=pivot。
// 快速排序
public void quickSort() {
qSort(0, this.curlen - 1);
}
// 对子表r[low..high]快速排序
private void qSort(int low, int high) {
if (low < high) {
int pivotloc = Partition(low, high);// 一趟排序,
qSort(low, pivotloc - 1);// 低子表递归排序
qSort(pivotloc + 1, high);// 高子表递归排序
}
}
// 一趟快速排序算法
private int Partition(int i, int j) {
RecordNode pivot = r[i];// 从第一个记录作为支点记录
while (i < j) {
while (i < j && pivot.key.compareTo(r[j].key) <= 0) {
j--; // 右侧扫描
}
if (i < j) {
r[i] = r[j];// 将比支点记录关键字值小的记录向前移动
i++;
}
while (i < j && pivot.key.compareTo(r[i].key) > 0) {
i++;// 左侧扫描
}
if (i < j) {
r[j] = r[i];// 将比支点记录关键字值大的记录向后移动
j--;
}
}
r[i] = pivot;// 支点记录到位
return i;// 返回支点位置
}
算法性能分析:
1)空间复杂度。快速排序在系统内部需要用一个栈来实现递归,最好情况,递归树的深度为log2n,其空间复杂度也就为O(logn),最坏情况,需要进行n‐1递归调用,其空间复杂度为O(n),平均情况,空间复杂度也为O(log2n)。
2)时间复杂度。在最优的情况下,快速排序算法的时间复杂度为O(nlog2n);最坏情况下,快速排序算法的时间复杂度为O(n^2);平均下来,快速排序算法的时间复杂度为O(nlog2n)。虽然快速排序的最坏时间复杂度为O(n^2),但是就平均性能而言,它是基于关键字比较的内部排序算法中速度最快的。
3)算法稳定性。快速排序是一种不稳定的排序算法。
7.4 选择排序
选择排序的主要思想是每一趟从待拍序列中选取一个关键字值最小的记录,也即第1趟从n个记录中选取关键字值最小的记录,在第二趟中,从剩下的n-1个记录中选取关键数值最小的记录,直到每个序列中的记录都选完位置。这样,由选取记录的顺序便可得到按关键字值有序的序列。
7.4.1 直接选择排序
直接选择排序(Straight Selection Sort)的基本思想是:每一趟从待排序序列中选择一个关键字值最小的记录,也即第1趟从n个记录中选取关键字值最小的记录,在第2趟中,从剩下的n-1个记录中选取关键字值最小的记录,直到整个序列中的记录都选完位置。这样,由选取记录的顺序便可得到按关键字值有序的序列。
// 直接选择排序
public void selectSort() {
RecordNode temp;// 辅助结点
for (int i = 0; i < this.curlen - 1; i++) {// n-1趟比较
for (int j = i + 1; j < this.curlen; j++) {// 第i个数据和它后面的记录比较
if (r[i].key.compareTo(r[j].key) > 0) {// 第i位置上关键字值大于j位置上关键字值大小,交换它们
temp = r[j];
r[j] = r[i];
r[i] = temp;
}
}
}
}
// 改进上面的算法
// 每次找到最小的数据的索引,减少交换的次数
public void selectSort2() {
RecordNode temp;// 辅助结点
for (int i = 0; i < this.curlen - 1; i++) {// n-1趟比较
int min = i;// 设第i条记录的关键字值最小
for (int j = i + 1; j < this.curlen; j++) {// 第i个数据和它后面的记录比较
if (r[min].key.compareTo(r[j].key) > 0) {
min = j;
}
}
if (min != i) {// 每趟比较最多交换一次 ,将本趟关键字值最小的记录与第i条记录交换
temp = r[i];
r[i] = r[min];
r[min] = temp;
}
}
}
算法性能分析:
1)空间复杂度。直接选择排序仅用了一个辅助单元,空间复杂度为O(1)。
2)时间复杂度。直接选择排序的时间复杂度为O(n^2)。
3)算法稳定性。直接选择排序是一种不稳定的排序算法。
7.4.2 树形选择排序
树形选择排序(Tree Selection Sort)又称锦标赛排序(Tournament Sort),它的原理:首先对n个记录的关键字进行两两比较,比较的结果是把关键字值较小者作为优胜者上升到父结点,得到[n/2](向上取整)个比较的优胜者,然后对这[n/2](向上取整)个记录再进行关键字的两两比较,如此重复,直到选出一个关键字值最小的记录为止。
这个过程可以用一棵有n个叶子结点的完全二叉树表示。如图中的二叉树表示从8个关键字中选出最小关键字的过程:
8个叶子结点中依次存放排序之前的8个关键字,每个非终端结点中的关键字均等于其左、右孩子结点中较小的那个关键字,则根结点中的关键字为叶子结点中的最小关键字。
在输出最小关键字之后,根据关系的可传递性,欲选出次小关键字,仅需将叶子结点中的最小关键字(13)改为“∞”,然后从该叶子结点开始,和其左右兄弟的关键字进行比较,修改从叶子结点到根结点的路径上各结点的关键字,则根结点的关键字即为次小值。
同理,可依次选出从小到大的所有关键字。
由于含有n个叶子结点的完全二叉树的深度为[log2n]+1,则在树形选择排序中,除了最小关键字以外,每选择一个次小关键字仅需进行[log2n]次比较,因此,它的时间复杂度为O(nlog2n)。
但是,这种排序方法也有一些缺点,比如辅助存储空间较多,并且需要和“最大值”进行多余的比较。 为了弥补,另一种选择排序被提出——堆排序。
7.4.3 堆排序
堆排序(Heap Sort)是一种重要的选择排序方法,它只需要一个记录大小的辅助存储空间,每个待排序的记录仅占用一个记录大小的存储空间,因此弥补了树形选择排序的弱点。
1. 堆的定义
n个记录关键字的序列{k0,k1,..kn-1},当且仅当满足下面的关系式①或②时称为堆(Heap)。
ki<=k2i+1且ki<=k2i+2,其中2i+1<=n-1,2i+2<=n-1 ①
ki>=k2i+1且ki>=k2i+2,其中2i+1<=n-1,2i+2<=n-1 ②
前者称为小顶堆,后者称为大顶堆。例如:关键字序列{12,36,24,85,47,30,53,91}是一个小顶堆;关键字序列{91,47,85,24,36,53,30,16}是一个大顶堆。
采用一个数组存储序列{k0,k1,...,kn-1},则该序列可以看作是一棵顺序存储的完全二叉树,那么ki和k2i+1、k2i+2的关系就是双亲与其左、右孩子之间的关系。因此,通常用完全二叉树的形式来直观地描述一个堆。i结点的父结点下标为(i-1)/2,i结点的左右子结点下标分别为2*i+1和2*i+2。
如最大化堆存储如下:
实现堆排序需要解决两个问题:
1)如何把一个无序序列建成一个堆?
2)如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
先考虑第2)个问题。一般在输出堆顶元素之后,视为将这个元素排除,然后用表中最后一个元素填补它的位置,自上向下进行调整:首先将堆顶元素和它的左右子树的根结点进行比较,把最小的元素交换到堆顶;然后顺着被破坏的路径一路调整下去,直至叶子结点,就得到新的堆。这种从堆顶到叶子结点的调整过程也称为“筛选”。
2. 建初始堆
从无序序列建立堆的过程就是一个反复“筛选”的过程。因为完全二叉树的最后一个非叶子结点的编号是[n/2-1]向下取整,所以“筛选”只需要从编号是n/2-1]向下取整所得到的数的结点开始。如下图,就是把无序序列{16,7,3,20,17,8}建成一个大顶堆的过程。
1)首先根据该序列元素构建一个完全二叉树
2)然后需要构造初始堆,则从最后一个非叶节点开始调整,调整过程如下:
3)20和16交换后导致16不满足大顶堆的性质,因此需重新调整
3. 筛选法调整堆
当将堆顶最大关键字值的结点与堆最后一个结点(第n-1个结点)交换后,交换后的根结点与其左右子树可能不符合堆的定义。此时,需要调整根结点与其左右两个堆顶结点之间的大小关系,使之符合堆的定义。调整方法如下:
1)将根结点r[0]与左、右子树中关键字值较大的结点进行交换。
2)当与左孩子交换,则左子树的堆被破坏,且仅左子树的根结点不满足堆的性质;若与右孩子交换,则右子树的堆被破坏,且仅右子树的根结点不满足堆的性质。对不满足堆性质的子树继续进行上述交换操作,直到叶子结点或者堆被建成为止。如下图,就是一次筛选法调整堆的过程。
1)上面已经建好了一个大顶堆。
2)将堆顶元素20和最后一个结点3(第n-1个结点)交换。
3)此时3位于堆顶不满堆的性质,则需调整继续调整
4. 堆排序
堆排序是一种选择排序。建立的初始堆为初始的无序区。进行堆排序时,首先输出堆顶结点(因为它的关键字值是最大的),然后将堆顶结点和最后一个结点(第n-1个结点)交换,这样,第n-1个位置作为有序区,前n-2个位置仍是无序区,对无序区进行筛选法调整堆,调整后得到新的大顶堆后,再将堆顶结点(因为它的关键字值是次大的)和最后第2个结点(第n-2个结点)进行交换,这样有序区长度变为2,然后再对无序区进行调整构成新的大顶堆,每次交换都导致无序区的长度减1,有序区的长度加1。如此反复,直到整个序列有序,则堆排序完成。下面演示了一个堆排序的过程。
1)首先先将堆顶结点和最后一个结点进行交换,然后重新调成一个新的大顶堆,即上面筛选法调整堆中所演示的案例,调整后的结果如下:
2)将调整后生成的堆顶结点17和最后第2个结点3(第n-2个结点)进行交换
3)调整后发现3不满足堆的定义,然后进行调整
4)继续将堆顶结点和无序区中的最后一个结点进行交换,并进行调整,直到整个序列都有序为止。
实现代码如下:
// 堆排序
public void heapSort() {
RecordNode temp;
for (int i = curlen / 2 - 1; i >= 0; i--) {// 创建堆
heapAdjust(i, curlen);
}
for (int i = curlen - 1; i > 0; i--) {// 每趟将最大关键字值交换到后面,再调整成堆
temp = r[0];
r[0] = r[i];
r[i] = temp;
heapAdjust(0, i);
}
}
// 筛选调整堆算法
private void heapAdjust(int low, int high) {
int i = low;// 子树的根结点
int j = 2 * i + 1;// j为i结点的左孩子
RecordNode temp = r[i];
while (j < high) {
if (j < high - 1 && r[j].key.compareTo(r[j + 1].key) < 0) {//
j++;// 记录比较,j为左右孩子的较大者
}
if (temp.key.compareTo(r[j].key) < 0) {// 若父母结点值较小
r[i] = r[j];// 孩子结点中的较大值上移
i = j;
j = 2 * i + 1;
} else {
j = high + 1;// 退出循环
}
}
r[i] = temp;// 当前子树的原根植调整后的位置
}
算法性能分析:
1)空间复杂度。堆排序需要一个记录的辅助存储空间,空间复杂度为O(1)。
2)时间复杂度。堆排序的时间复杂度为O(nlog2n)。
3)算法稳定性。堆排序是一种不稳定的排序算法。
7.5 归并排序
归并排序(Merging Sort)是与插入排序、交换排序、选择排序不同的另一类排序方法。归并的含义是将两个或两个以上的有序表合并成一个新的有序表。其中,将两个有序表合并成一个有序表的归并排序称为二路归并排序,否则称为多路归并排序。归并排序既可用于内部排序,也可以用于外部排序,这里仅对内部排序的二路归并方法进行讨论。
示例:
// 归并排序
public void mergeSort() {
mergepass(r, 0, this.curlen - 1);
}
/**
* 将数组r中的r[low..high]进行归并排序
*
* @param r
* 待排序的数组
* @param low
* 待排序数组的起始角标
* @param high
* 待排序数组的结束角标
*/
private void mergepass(RecordNode[] r, int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
mergepass(r, low, mid);// 递归左半部分处理
mergepass(r, mid + 1, high);// 递归右半部分处理
merge(r, low, mid, high);// 左右半边归并
}
}
// 两个相邻的有序序列的归并算法
/**
* 将数组r中的r[low..mid]和r[mid+1..high]进行归并
*
* @param r
* 待排序的数组
* @param low
* 待排序数组中第一段的起始角标
* @param mid
* 待排序数组中第一段的结束角标
* @param high
* 待排序数组中第二段的结束角标
*/
private void merge(RecordNode[] r, int low, int mid, int high) {
int i = low, j = mid + 1, k = 0; // mid+1为第二段有序子序列第一个元素角标
RecordNode[] temp = new RecordNode[high - low + 1];// 辅助数组
while (i <= mid && j <= high)// 顺序选取两个有序子序列中的较小元素,存储到辅助数组temp中
{
if (r[i].key.compareTo(r[j].key) <= 0)// 将较小的存入辅助数组temp中
temp[k++] = r[i++];
else
temp[k++] = r[j++];
}
while (i <= mid) {// 若比较完之后,第一个有序子序列仍有剩余,则直接复制到temp数组中
temp[k++] = r[i++];
}
while (j <= high) {// 若比较完之后,第二个有序子序列仍有剩余,则直接复制到temp数组中
temp[k++] = r[j++];
}
for (i = low, k = 0; i <= high; i++, k++) {// 将归并后排好序的数据复制到r中的low~high区间
r[i] = temp[k];
}
}
算法性能分析:
1)空间复杂度。二路归并排序需要与一个待排序记录序列等长的辅助数组来存放排序过程中的中间结果,所以空间复杂度为O(n)。
2)时间复杂度。二路归并排序时间复杂度为O(nlog2n)。
3)算法稳定性。归并排序是一种稳定的排序算法。
7.6 基数排序
基数排序(Radix Sort)是一种借助于多关键字进行排序,也是一种将但关键字按基数分成“多关键字”进行排序的方法。
7.7 各种内部排序算法的性能比较
各种内部排序算法的性能比较如下表:
下面给出几种选择排序算法的建议:
1)若n较小(例如n<=50),则可采用直接插入排序或直接选择排序。
2)若记录序列初始状态基本有序(指正序),则应采用直接插入排序、冒泡排序。
3)若n较大时,则应采用时间复杂度为O(nlog2n)的排序算法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序中被认为最好的一种排序算法,当待排序的关键字是随机分布时,快速排序的平均时间最短;堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况(当待排记录序列按关键字排序基本有序时),这两种排序都是不稳定的;当内存空间允许,且要求排序时稳定时,则可选用归并排序。