您现在的位置是:首页 >技术杂谈 >数据结构与算法——内排序篇详解网站首页技术杂谈

数据结构与算法——内排序篇详解

♚焕蓝·未来 2024-06-17 10:43:07
简介数据结构与算法——内排序篇详解

目 录

1. 内排序

1.1 概述

1.1.1 定义

  • 排序:将一组无序的记录序列调整为有序的记录序列的一种操作。

  • 排序的目的:提高查询的效率。

  • 相关名词:

    • 关键字:数据元素(或记录)中某个数据项,用于标识该数据元素(或记录)。作为排序依据的数据项。
    • 主关键字:能够唯一标识一条记录的数据项。
    • 次关键字:能表示多条记录的数据项。
  • 排序稳定性:

    • 相同的关键字(52、52),通过排序算法排序后,顺序是否相同
      • 如果相同(52、52),算法是稳定算法
      • 如果不同(52、52),算法是不稳定算法。
  • 约定:

    • 排序:按关键字非递增(从小到大)排序
    • 存储:以顺序表作为排序表的存储结构
    • 类型:关键字类型为整形

1.1.2 排序分类&稳定性

  • 按照存储器不同分为:外部排序、内部排序

    • 内部排序:所有数据都在内存中进行的排序。适合数据量小的数据元素的排序。
    • 外部排序:排序过程中,需要访问外部存储器的排序。待排序的数据元素非常多必须存储在外部存储器上。
  • 按照相同关键字排序前后的位置分为:稳定排序、不稳定排序。例如:{3,4,2,3,1}

    • 稳定排序:相同关键字间的前后位置关系在排序前和排序后保持一致。例如:{1,2,3,3,4}
    • 不稳定排序:保持不一致。例如:{1,2,3,3,4}

1.1.3 内排序的方法

  • 内部排序:一个逐渐扩大记录的有序序列长度的过程。
类型描述
插入类将无序子序列中的一个或几个记录插入到有序序列中,从而增加记录的有序子序列的长度。
交换类通过交换无序序列中的记录,从而得到其中关键字最小或最大的记录,并将它加入到有序
子序列中,以此方法增加记录的有序子序列的长度。
选择类从记录的无序子序列中选择关键字最小或最大的记录,并将它加入到有序子序列中,
以此方法增加记录的有序子序列的长度。
归并类通过归并两个或两个以上的记录有序子序列,逐渐增加记录有序序列的长度。

1.1.4 排序算法的性能评价

  • 排序算法好坏的标准:算法的时间复杂度、算法的空间复杂度
  • 排序是一个经常使用的运算,所需时间是衡量排序算法好坏的重要标志。
  • 排序的时间开销,可以通过算法执行过程中的记录比较次数移动次数来衡量。

1.1.5 待排序记录的类描述

  • 实现内部排序的基本操作有两个:

    • “比较”序列中两个关键字的大小
    • “移动”记录
  • 顺序表记录结点类

    package book.ch07;
    
    /**
     * 顺序表记录结点类
     */
    public class RecordNode {
    
    	public Comparable key; // 关键字
    	public Object element; // 数据元素
    
    	public RecordNode() {
    	}
    
    	public RecordNode(Comparable key) { // 构造方法1
    		this.key = key;
    
    	}
    
    	public RecordNode(Comparable key, Object element) { // 构造方法2
    		this.key = key;
    		this.element = element;
    	}
    
    	public String toString() { // 覆盖toString()方法
    		return "[" + key + "," + element + "]";
    	}
    }
    
    
  • 带排序的顺序表类描述

    public class SeqList {
    
        public RecordNode[] r;    //顺序表记录结点数组
        public int curlen;        //顺序表长度,即记录个数
        
        // 顺序表的构造方法,构造一个存储空间容量为maxSize的顺序表
        public SeqList(int maxSize) {
            this.r = new RecordNode[maxSize];  // 为顺序表分配maxSize个存储单元
            this.curlen = 0;                   // 置顺序表的当前长度为0
        }
        
        public void display() {    //输出数组元素
            for (int i = 0; i < this.curlen; i++) {
                // 如果是个位数,多输出一个空格
                String str = r[i].key.toString().length() == 1 ? "  " : " ";
                System.out.print(str + r[i].key.toString());
            }
            System.out.println();
        }
        
       public void display(int sortMode) {    //输出数组元素
            int i;
            if(sortMode==9)   //带监视哨的直接插入排序方法,0号单元用于存放监视哨
                i=1;
            else
                i=0;
            for (; i < this.curlen; i++) {
                String str = r[i].key.toString().length() == 1 ? "  " : " ";
                System.out.print(str + r[i].key.toString());
            }
            System.out.println();
        }
        
    }
    

1.1.6 算法:顺序表插入

  • 步骤:
    1. 在当前顺序表的第i个结点之前插入一个RecordNode类型的结点x
    2. 其中i取值范围为:0≤i≤length()。
    3. 如果i值不在此范围则抛出异常,当i=0时表示在表头插入一个数据元素x,
    4. 当i=length()时表示在表尾插入一个数据元素x
  • 代码

    public void insert(int i, RecordNode x) throws Exception {
        if (curlen == r.length) {    // 判断顺序表是否已满
            throw new Exception("顺序表已满");
        }
        if (i < 0 || i > curlen) {  // i小于0或者大于表长
            throw new Exception("插入位置不合理");
        }
        for (int j = curlen; j > i; j--) {
            r[j] = r[j - 1];   // 插入位置及之后的元素后移
        }
        r[i] = x;   // 插入x
        this.curlen++;  // 表长度增1
    }

1.1.7 练习

  • 练习1:使用xx排序,写出每一趟的排序结果

    16, 15, 50, 53, 64, 7

  • 练习2:使用xx排序,写出每一趟的排序结果

    2,5,8,3,6,9,1,4,7

  • 练习3:使用xx排序,写出每一趟的排序结果

    9 , 20 , 13 , 20 , 12

1.2 插入排序

1.2.1 概述

  • 插入排序:每次将一个待排序的记录,按其关键字的大小插入到前面已排序好的记录序列中的适当位置,直到全部记录插入完成为止。

  • 确定插入位置的查找方法不同导致不同的算法描述:

    • 直接插入排序:基于顺序查找
    • 希尔排序:基于逐趟缩小增量

1.2.2 直接插入排序

1)概述:

  • 直接插入排序:Straight Insertion Sort

  • 基本条件:待排序记录依次存放在数据r[0…n-1]中

  • 思想:

    • 先将第0个记录,组成一个有序子表
    • 然后依次将后面的记录插入到这个子表中,且一直保持它的有序性。

2)步骤

  1. 在r[0…i-1]中查找r[i]的插入位置,r[0…j].key ≤r[i].key < r[j+1…i-1].key
  2. 将r[j+1 … i-1] 中的所有记录均后移一个位置
  3. 将r[i]插入到r[j+1]的位置上

3)示意图

4)分析:不带监视哨的算法

  1. 查找r[i]的插入位置,将r[i]暂存在临时变量temp中,将temp与r[j] (j=i-1,i-2, …, 0)依次进行比较
  2. 若temp.key < r[j].key,则将r[j]后移一个位置,直到temp.key ≥ r[j].key 为止
  3. 将temp插入到第j+1个位置上
  4. 令 i = 1, 2, 3, …, n-1 ,重置上面3步。

5)算法【7.1】:不带监视哨

  • 算法
//【算法7.1】 不带监视哨的直接插入排序算法
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个位置
        //System.out.print("第" + i + "趟: ");
        //display();
    }
}
  • 测试
package book.ch07;

/**
 * @author txt
 * @email tanxintong99@163.com
 */
public class TestSeqList1_insert {
    public static void main(String[] args) throws Exception {
        int[] arr = {52,39,67,95,79,8,25,52};
        SeqList seqList = new SeqList(arr.length);
        for (int i = 0; i < arr.length; i++) {
            seqList.insert(i, new RecordNode(arr[i]));
        }
        // 不带监视哨的直接插入排序
        seqList.insertSort();
    }
}
//
//第1趟:  39 52 67 95 79  8 25 52
//第2趟:  39 52 67 95 79  8 25 52
//第3趟:  39 52 67 95 79  8 25 52
//第4趟:  39 52 67 79 95  8 25 52
//第5趟:   8 39 52 67 79 95 25 52
//第6趟:   8 25 39 52 67 79 95 52
//第7趟:   8 25 39 52 52 67 79 95

6)分析:带监视哨的算法

  • 算法7.1中第6行的循环条件中的j≥0用来控制下标越界。为了提高算法效率,可对该算法改进如下:
    • 首先将待排序的n条记录从下标为1的存储单元开始依次存放在数组r中,
    • 在将顺序表的第0个存储单元设置为一个监视哨,即在查找之前把r[i]赋给r[0],
    • 这样每循环一次,只需要进行记录的比较,不需要比较下标是否越界

7)算法【7.2】:带监视哨【重点】

  • 算法
//【算法7.2】带监视哨的直接插入排序算法
public void insertSortWithGuard() {

    int i, j;
//        System.out.println("带监视哨的直接插入排序");
    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个位置
        System.out.print("第" + i + "趟: ");
        display(9);
    }
}
  • 测试
package book.ch07;

/**
 * @author txt
 * @email tanxintong99@163.com
 */
public class TestSeqList2_insertGuard {
    public static void main(String[] args) throws Exception {
        int[] arr = {0,52,39,67,95,79,8,25,52};
        SeqList seqList = new SeqList(arr.length);
        for (int i = 0; i < arr.length; i++) {
            seqList.insert(i, new RecordNode(arr[i]));
        }
        // 带监视哨的直接插入排序
        seqList.insertSortWithGuard();
    }
}
//
//第1趟:  39 52 67 95 79 8 25 52
//第2趟:  39 52 67 95 79 8 25 52
//第3趟:  39 52 67 95 79 8 25 52
//第4趟:  39 52 67 79 95 8 25 52
//第5趟:  8 39 52 67 79 95 25 52
//第6趟:  8 25 39 52 67 79 95 52
//第7趟:  8 25 39 52 52 67 79 95

8)性能分析

  • 平均值:约n2/4,直接插入排序的时间复杂度为O(n2)
  • 需要一个辅助单元r[0],空间复杂度为O(1)
  • 直接插入排序是一种稳定的排序算法。

9)练习

  • 练习1:使用直接插入排序,写出每一趟的排序结果

    16, 15, 50, 53, 64, 7

  • 练习2:使用直接插入排序,监视哨版,写出每一趟的排序结果

    2, 5, 8, 3, 6, 9, 1, 4, 7

  • 练习3:使用直接插入排序,写出每一趟的排序结果

    9 , 20 , 13 , 20 , 12

1.2.3 希尔排序

1)概述

  • 希尔排序:Shell Sort
  • 希尔排序,又称为缩小增量排序。
  • 基本思路:
    • 先选取一个小于n的整数di(称为增量),并将记录序列分成若干子表
    • 从下标为0的记录开始,间隔为di的记录组成一个子表,在各个子表内进行直接插入排序。
    • 一趟完成后,逐步减小增量di,并重复上面的步骤,直到di=1,使得间隔为1的记录有序,也就是整个序列都达到有序。

2)示意图

3)分析

  • 序列:52、39、67、95、70、8、25、52、56、5

4)算法【7.3】:实现

  • 代码

    //【算法7.3】希尔排序算法
    public void shellSort(int[] d) { //d[]为增量数组
        RecordNode temp;
        int i, j;
        System.out.println("希尔排序");
        //控制增量,增量减半,若干趟扫描
        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;
            }
            System.out.print("增量dk=" + dk + "  ");
            display();
        }
    }
    
  • 测试

    package book.ch07;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestSeqList3_shell {
        public static void main(String[] args) throws Exception {
            int[] arr = {52,39,67,95,70,8,25,52,56,5};
            int[] d = {5,3,1};
            SeqList seqList = new SeqList(arr.length);
            for (int i = 0; i < arr.length; i++) {
                seqList.insert(i, new RecordNode(arr[i]));
            }
            // 希尔排序算法
            seqList.shellSort(d);
        }
    }
    //希尔排序
    //增量dk=5    8 25 52 56  5 52 39 67 95 70
    //增量dk=3    8  5 52 39 25 52 56 67 95 70
    //增量dk=1    5  8 25 39 52 52 56 67 70 95
    
    

5)性能分析

  • 希尔排序的时间复杂度:不确定,但在插入排序中,希尔排序的效能最高,最好情况可达O(nlog2n)
  • 只需要一个记录的辅助空间,O(1)
  • 不稳定的排序方式,也就是说 52和 52 顺序不确定。

6)练习

  • 统一要求,增量数组为[5,3,1]

  • 练习1:使用希尔排序,写出每一趟的排序结果

    16, 15, 50, 53, 64, 7

  • 练习2:使用希尔排序,写出每一趟的排序结果

    2,5,8,3,6,9,1,4,7

  • 练习3:使用希尔排序,写出每一趟的排序结果

    9 , 20 , 13 , 20 , 12

1.3 交换排序

1.3.1 概述

  • 交换排序的基本思想是两两比较待排序记录的关键字,若两个记录的次序相反则交换这两个记录,直到没有反序的记录为止。
  • 交换排序主要方法:
    • 冒泡排序
    • 快速排序

1.3.2 冒泡排序

1)概述

  • 冒泡排序:Bubble Sort
  • 将待排序的数组看成从上到下的存放,把关键字较小的记录看成较轻的,关键字较大的记录看成较重的
  • 较小关键字值的记录,好像水中的气泡一样,向上浮;
  • 较大关键字值的记录,好像水中的石块一样,向下沉;
  • 当所有的气泡都浮到了相应的位置,并且所有的石块都沉到了水中,排序就结束了。

2)示意图

3)算法分析

  • 从第一个记录开始,依次对无序区中相邻记录进行关键字比较,如果大在上,小在下,则交换,第一趟扫描下来表中最大的沉在最下面
  • 然后再对前n-1个记录进行冒泡排序,直到排序成功为止。
  • 一般,第i趟扫描时,r[0]到r[n-i]和r[i+1]到r[n-1]分别为当前的无序区和有序区。

待排序的序列:

{52, 39, 67, 95, 70, 8, 25, 52}

4)算法:标准版

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) {	//交换相邻的2个元素
                temp = r[j];
                r[j] = r[j+1];
                r[j + 1] = temp;
            } 
        }
    }
}

5)算法【7.4】:优化版【重点】

  • 代码
//【算法7.4】 冒泡排序算法
public void bubbleSort() {
//    System.out.println("冒泡排序");
    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;							//如果发生交换,记录
            }
        }
        System.out.print("第" + i + "趟: ");
        display();
    }
}
  • 测试
package book.ch07;

/**
 * @author txt
 * @email tanxintong99@163.com
 */
public class TestSeqList4_bubble {
    public static void main(String[] args) throws Exception {
        int[] arr = {52,39,67,95,70,8,25,52};
        SeqList seqList = new SeqList(arr.length);
        for (int i = 0; i < arr.length; i++) {
            seqList.insert(i, new RecordNode(arr[i]));
        }
        // 希尔排序算法
        seqList.bubbleSort();
    }
}
//冒泡排序
//第1趟:  39 52 67 70  8 25 52 95
//第2趟:  39 52 67  8 25 52 70 95
//第3趟:  39 52  8 25 52 67 70 95
//第4趟:  39  8 25 52 52 67 70 95
//第5趟:   8 25 39 52 52 67 70 95
//第6趟:   8 25 39 52 52 67 70 95

6)性能分析

  1. 时间复杂度:O(n2)
  2. 空间复杂度:仅需要一个记录的辅助空间,O(1)
  3. 是稳定的排序方法

7)练习

  • 练习1:使用冒泡排序,写出每一趟的排序结果

    16, 15, 50, 53, 64, 7

  • 练习2:使用冒泡排序,写出每一趟的排序结果

    2,5,8,3,6,9,1,4,7

  • 练习3:使用冒泡排序,写出每一趟的排序结果

    9 , 20 , 13 , 20 , 12

1.3.3 快速排序

1)概述

  • 快速排序:Quick Sort
  • 通过一趟排序将要排序的记录分割成独立的两个部分,其中一部分的所有记录的关键字值都比另外一部分的所有记录关键字值小
  • 然后再按此方法对这两部分记录分别进行快速排序
  • 整个排序过程可以递归进行,以此达到整个记录序列变成有序。

2)示意图

3)算法分析

  • 假设待排序的记录序列为:{ r[row] , r[row+1] , … , r[high] }
  • 首先在该序列中任意选取一条记录(该记录称为支点,通常选r[row]作为支点)
  • 然后将所有关键字值比支点小的记录都放到它的前面
  • 所有关键字值比支点大的记录都放到它的后面
  • 由此可以将该支点记录最后所落的位置i作为分界线,
  • 将记录序列{ r[row] , r[row+1] , … , r[i-1], r[i], r[i+1], r[i+2], … , r[high] }分隔成两个子序列 { r[row] , r[row+1] , … , r[i-1]} 和 {r[i+1], r[i+2], … , r[high] }。
  • 这个过程称为一趟快速排序。通过一趟排序,支点记录就落在了最终排序结果的位置上。

  • 核心思路:

    • 变量i作用:从左往右寻找比支点大的值,从而交换到后面的位置(上一次j的位置)。
    • 变量j作用:从右往左寻找比支点小的值,从而交换到前面的位置(上一次i的位置)。

4)算法【7.5】:一趟快速排序

  • 代码

    //【算法7.5】一趟快速排序
    //交换排序表r[i..j]的记录,使支点记录到位,并返回其所在位置
    //此时,在支点之前(后)的记录关键字均不大于(小于)它
    public int Partition(int i, int j) {
        RecordNode pivot = r[i];          //第一个记录作为支点记录
        System.out.println(i + ".." + j + ",  pivot=" + pivot.key + "  ");
        System.out.print("初始值:     ");
        int c = 0;
        display();
        while (i < j) {    //从表的两端交替地向中间扫描
            // 1.1 从后到前,寻找第一个比支点小的元素
            while (i < j && pivot.key.compareTo(r[j].key) <= 0) {
                j--;
            }
            // 1.2 将后面较小的元素,移到前面去
            if (i < j) {
                r[i] = r[j];   //将比支点记录关键字小的记录向前移动
                System.out.print("第"+(++c)+"次交换后:");
                display();
                i++;
            }
            // 2.1 从前到后,选择第一恶比支点大的元素
            while (i < j && pivot.key.compareTo(r[i].key) > 0) {
                i++;
            }
            // 2.2 将前面较大的元素,移到后面去
            if (i < j) {
                r[j] = r[i];   //将比支点记录关键字大的记录向后移动
                System.out.print("第"+(++c)+"次交换后:");
                display();
                j--;
            }
    
        }
        r[i] = pivot;         //支点记录到位
        System.out.print("一趟完成:   ");
        display();
        return i;             //返回支点位置
    }
    
  • 测试

    package book.ch07;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestSeqList5_part {
        public static void main(String[] args) throws Exception {
            int[] arr = {52,39,67,95,70,8,25,52};
            SeqList seqList = new SeqList(arr.length);
            for (int i = 0; i < arr.length; i++) {
                seqList.insert(i, new RecordNode(arr[i]));
            }
            // 一趟快速排序
            seqList.Partition(0, arr.length - 1);
        }
    }
    //一趟快速排序
    //0..7,  pivot=52
    //初始值:      52 39 67 95 70  8 25 52
    //第1次交换后: 25 39 67 95 70  8 25 52
    //第2次交换后: 25 39 67 95 70  8 67 52
    //第3次交换后: 25 39  8 95 70  8 67 52
    //第4次交换后: 25 39  8 95 70 95 67 52
    //一趟完成:    25 39  8 52 70 95 67 52
    
    

5)算法【7.6/7.7】:完整快排

  • 代码

    //【算法7.6】 递归形式的快速排序算法
    //对子表r[low..high]快速排序
    public void qSort(int low, int high) {
        if (low < high) {
            int pivotloc = Partition(low, high);   // 一趟排序,将排序表分为两部分
            qSort(low, pivotloc - 1);       // 低子表递归排序
            qSort(pivotloc + 1, high);      // 高子表递归排序
        }
    }
    
    //【算法7.7】顺序表快速排序算法
    public void quickSort() {
        qSort(0, this.curlen - 1);
    }
    
  • 测试

    package book.ch07;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestSeqList6_quick {
        public static void main(String[] args) throws Exception {
            int[] arr = {52,39,67,95,70,8,25,52};
            SeqList seqList = new SeqList(arr.length);
            System.out.print("数组序列号:");
            for (int i = 0; i < arr.length; i++) {
                System.out.print("  " + i);
                seqList.insert(i, new RecordNode(arr[i]));
            }
            System.out.println();
            // 快速排序
            seqList.quickSort();
        }
    }
    //快速排序
    //数组序列号:  0  1  2  3  4  5  6  7
    //0..7,  pivot=52
    //初始值:      52 39 67 95 70  8 25 52
    //第1次交换后: 25 39 67 95 70  8 25 52
    //第2次交换后: 25 39 67 95 70  8 67 52
    //第3次交换后: 25 39  8 95 70  8 67 52
    //第4次交换后: 25 39  8 95 70 95 67 52
    //一趟完成:    25 39  8 52 70 95 67 52
    //0..2,  pivot=25
    //初始值:      25 39  8 52 70 95 67 52
    //第1次交换后:  8 39  8 52 70 95 67 52
    //第2次交换后:  8 39 39 52 70 95 67 52
    //一趟完成:     8 25 39 52 70 95 67 52
    //4..7,  pivot=70
    //初始值:       8 25 39 52 70 95 67 52
    //第1次交换后:  8 25 39 52 52 95 67 52
    //第2次交换后:  8 25 39 52 52 95 67 95
    //第3次交换后:  8 25 39 52 52 67 67 95
    //一趟完成:     8 25 39 52 52 67 70 95
    //4..5,  pivot=52
    //初始值:       8 25 39 52 52 67 70 95
    //一趟完成:     8 25 39 52 52 67 70 95
    
    

6)性能分析

  1. 时间复杂度:O( nlog2n )
  2. 空间复杂度:O( log2n )
  3. 不稳定排序

7)练习

  • 练习1:使用快速排序,写出每一趟的排序结果

    16, 15, 50, 53, 64, 7

  • 练习2:使用快速排序,写出每一趟的排序结果

    2,5,8,3,6,9,1,4,7

  • 练习3:使用快速排序,写出每一趟的排序结果

    9 , 20 , 13 , 20 , 12

1.4 选择排序

1.4.1 概述

  • 选择排序的主要思想是每一趟从待排序列中选取一个关键字值最小的记录,也即第1趟从n个记录中选取关键字最小的记录,在第2趟中,从剩下的n-1个记录中选取关键字值最小的记录,直到整个序列中的记录都选完位置,这样,由选取记录的顺序便可得到按关键字有序的排序。
  • 常见的三种选择排序方式:
    • 直接选择排序
    • 树形选择排序
    • 堆排序

1.4.2 直接选择排序

1)概述

  • 首先在所有记录中选出关键字值最小的记录,把它与第一个记录进行位置交换,
  • 然后在其余的记录中再选出关键字值次小的记录,与第二个记录进行位置交换
  • 依次类推,直到所有记录排好序。

2)示意图

3)算法分析

  • 主要步骤:
    • 置i的初值为0
    • 当i<n-1时,重复下列步骤:
      1. 在无序子序列{r[i+1], … , r[n-1]}中选出一个关键字值最小的记录r[min]
      2. 若r[min]不是r[i](即min != i),则交换r[i]和r[min]的位置,否则不进行交换
      3. 将i的值加1

4)算法【7.8】

  • 代码

    //【算法7.8】直接选择排序
    public void selectSort() {
     //   System.out.println("直接选择排序");
        RecordNode temp; //辅助结点
        for (int i = 0; i < this.curlen - 1; i++) {//n-1趟排序
            //每趟在从r[i]开始的子序列中寻找最小元素
            int min = i;               //设第i条记录的关键字最小
            for (int j = i + 1; j < this.curlen; j++) {//在子序列中选择关键字最小的记录
                if (r[j].key.compareTo(r[min].key) < 0) {
                    min = j;             //记住关键字最小记录的下标
                }
            }
            if (min != i) {    //将本趟关键字最小的记录与第i条记录交换
                temp = r[i];
                r[i] = r[min];
                r[min] = temp;
            }
            System.out.print("第" + (i + 1) + "趟: ");
            display();
        }
    }
    
  • 测试

    package book.ch07;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestSeqList7_select {
        public static void main(String[] args) throws Exception {
            int[] arr = {52,39,67,95,70,8,95,25};
            SeqList seqList = new SeqList(arr.length);
            System.out.print("序列号:");
            for (int i = 0; i < arr.length; i++) {
                System.out.print("  " + i);
                seqList.insert(i, new RecordNode(arr[i]));
            }
            System.out.println();
            // 直接选择排序
            seqList.selectSort();
        }
    }
    //直接选择排序
    //序列号:  0  1  2  3  4  5  6  7
    //第1趟:   8 39 67 95 70 52 95 25
    //第2趟:   8 25 67 95 70 52 95 39
    //第3趟:   8 25 39 95 70 52 95 67
    //第4趟:   8 25 39 52 70 95 95 67
    //第5趟:   8 25 39 52 67 95 95 70
    //第6趟:   8 25 39 52 67 70 95 95
    //第7趟:   8 25 39 52 67 70 95 95
    

5)性能分析

  • 对n个记录进行简单选择排序,所需进行的关键字间的比较次数总计为:
    ∑ i = 1 n − 1 ( n − i ) = n ( n − 1 ) 2 ( ) sum_{i=1}^{n-1}(n-i) = frac{n(n-1)}{2} ag{} i=1n1(ni)=2n(n1)()

  • 移动记录的次数,最小值为0,最大值为3(n-1)

  • 时间复杂度:O(n2)

  • 空间复杂度:O(1) ,仅用一个辅助单元

  • 直接选择排序是不稳定排序算法。

6)练习

  • 练习1:使用直接选择排序,写出每一趟的排序结果

    16, 15, 50, 53, 64, 7

  • 练习2:使用直接选择排序,写出每一趟的排序结果

    2,5,8,3,6,9,1,4,7

  • 练习3:使用直接选择排序,写出每一趟的排序结果

    9 , 20 , 13 , 20 , 12

1.4.3 树形选择排序

1)概述

  • 在直接选择排序中,关键字的总比较次数是n(n-1)/2 。
  • 实际上,该方法中,有许多关键字之间进行了不止一次的比较,也就是说,两个关键字之间可能进行了两次以上的比较。能否在选择最小关键字值记录的过程中,把关键字比较的结果保存下来,以便在以后需要的时候直接查看这个比较结果,而不需要再进行比较呢。 答案是肯定的。
  • 树形选择排序(Tree Selection Sort)的原理与此类似。
  • 树形选择排序又称为锦标赛排序。
  • 也就是,体育比赛中的淘汰制,每两个对手经过比赛留下 获胜者继续参加下一轮的淘汰赛,直到最后剩下两个对手进行决赛争夺冠军。

2)算法分析

  • 待排序序列为:52, 39, 67, 95, 70, 8, 25, 52

  • 首先针对n个记录进行两两比较,比较的结果是把关键字值较小者作为优胜者上升到父节点,得到n/2个优胜者,作为第一步比较的结果保留下来。

  • 然后对n/2个记录再进行关键字的两两比较,如此重复,直到选出一个关键字值最小的记录为止。

  • 这个过程可用一个含有n个叶子节点的完全二叉树来表示,这种树称为胜者树。

  • 接着,再求出次小关键字记录,只需将叶子节点中最小的关键字值改为∞,然后从该叶子节点开始与其左(或右)兄弟的关键字进行比较,用较小关键字修改父节点关键字,用此方法,从下到上修改从该叶子结点到根节点路径上的所有结点的关键字,则可得到次小关键字记录。

  • 以此类推,可依次选出从小到大的所有关键字。

3)树结点类

class TreeNode {      //胜者树的结点类

    public RecordNode data;  //排序记录结点数据值
    public int index;        //结点在满二叉树中的序号
    public int active;       //参加选择标志,1表示参选,0表示不参选
}

4)算法【7.9】:树形选择排序

  • 【算法7.9】 树形选择排序(锦标赛排序)

    //【算法7.9】 树形选择排序(锦标赛排序)
    //建立树的顺序存储数组tree,并对其排序,并将结果返回到r中
    void tournamentSort() {
        TreeNode[] tree;                    // 胜者树结点数组
        int leafSize = 1;                   // 胜者树的叶子结点数
        // 得到胜者树叶子结点(外结点)的个数,该个数必须是2的幂,例如:7个结点,需要8个叶子
        while (leafSize < this.curlen) {
            leafSize *= 2;
        }
        int TreeSize = 2 * leafSize - 1;   // 胜者树的所有节点数 P178
        int loadindex = leafSize - 1;      // 叶子结点(外结点)存放的起始位置
        tree = new TreeNode[TreeSize];
        int j = 0;					//用于处理每层中的所有结点,每次处理2个,j和j+1
        //给叶子结点填充数据
        //把待排序结点复制到胜者树的叶子结点中,如果有数据active为1,如果没有数据active为0
        for (int i = loadindex; i < TreeSize; i++) {
            tree[i] = new TreeNode();
            tree[i].index=i;
            if (j < this.curlen) {          // 复制结点
                tree[i].active=1;
                tree[i].data=r[j++];
            } else {
                tree[i].active=0;          // 空的外结点,叶子结点没有填满,相当于∞
            }
        }
        // 给非叶子结点赋值,值的来源是两个叶子节点的获胜者
        // 使用i记录每一层第一个结点的索引值
        int i = loadindex;                 // 进行初始比较查找关键码最小结点
        while (i > 0) {                    // 产生胜者树,先下层后上层,每层从头开始
            j = i;
            // 处理某一层中的所有结点,获胜者将赋值给父节点,处理数据2个2个处理
            while (j < 2 * i) {           // 处理各对比赛者,处理当前层,每次处理2个结点
                if (tree[j + 1].active == 0 || ((tree[j].data).key.compareTo((tree[j + 1].data).key)) <= 0) {
                    tree[(j - 1) / 2] = tree[j];      // 左孩子(胜者)赋值给父结点
                } else {
                    tree[(j - 1) / 2] = tree[j + 1];  // 右孩子(胜者)赋值给父结点
                }
                j += 2;                   // 下【一对】比赛者(2个结点)
            }
            i = (i - 1) / 2;             // 处理上层结点(获得上层第一个结点的下标)
        }
        // 依次更新数据,将不参赛的结点剔除,tree[0]存放每次比较的获胜者
        for (i = 0; i < this.curlen - 1; i++) {  // 处理剩余的n-1个记录
            r[i] = tree[0].data;                // 将胜者树的根(最小者)存入数组r
            tree[tree[0].index].active=0;       // 该元素相应外结点不再比赛
            updateTree(tree, tree[0].index);   // 调整胜者树,将最小元素移至到根元素
            display();
        }
        // 整理n-1完成后,树中存放最后一个结点,也就是最大值
        r[this.curlen - 1] = tree[0].data;
    }
    
  • 【算法7.10】树形选择排序的调整算法

    //【算法7.10】树形选择排序的调整算法,i是当前最小关键字记录的下标
    void updateTree(TreeNode[] tree, int i) {
        int j;
        // 当前记录i已经参选,将对手存放到父节点
        if (i % 2 == 0) {       // i为偶数,对手为左结点
            tree[(i - 1) / 2] = tree[i - 1];
        } else {                // i为奇数,对手为右结点
            tree[(i - 1) / 2] = tree[i + 1];
        }
        i = (i - 1) / 2;        // 最小元素输出后,其对手上升到父结点
        while (i > 0) {         // 直到i==0
            // 获得对手的索引,左孩子(i)的对手右孩子(i+1);右孩子(i)的对手左孩子(i-1)
            if (i % 2 == 0) {   // i为偶数,对手为左结点
                j = i - 1;
            } else {            // i为奇数,对手为右结点
                j = i + 1;
            }
            // 比赛对手中有一个为空
            if (tree[i].active == 0 || tree[j].active == 0) {
                if (tree[i].active == 1) {
                    tree[(i - 1) / 2] = tree[i];  // i可参选,i上升到父结点
                } else {
                    tree[(i - 1) / 2] = tree[j];  // 否则,j上升到父结点(j可参选或不可参选)
                }
            } else {    // 双方都可参选
                // 关键码小者上升到父结点
                if ((tree[i].data).key.compareTo((tree[j].data).key) <= 0) {
                    tree[(i - 1) / 2] = tree[i];
                } else {
                    tree[(i - 1) / 2] = tree[j];
                }
            }
            i = (i - 1) / 2;     // i上升到父结点
        }
    }
    
  • 测试

    package book.ch07;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestSeqList8_tree {
        public static void main(String[] args) throws Exception {
            int[] arr = {52,39,67,95,70,8,25,52};
            SeqList seqList = new SeqList(arr.length);
            System.out.print("序列号:");
            for (int i = 0; i < arr.length; i++) {
                System.out.print("  " + i);
                seqList.insert(i, new RecordNode(arr[i]));
            }
            System.out.println();
            // 树形选择排序(锦标赛排序)
            seqList.tournamentSort();
            seqList.display();
        }
    }
    //树形选择排序(锦标赛排序)
    
    
    
    

5)性能分析

  • 空间复杂度:树形选择排序虽然减少了排序时间,但使用了较多的附加存储空间(多了2n-1个)。
  • 时间复杂度:O(nlog2n)
  • 树形选择排序是一种稳定的排序算法。

6)练习

  • 练习1:使用树形选择排序,写出每一趟的排序结果

    16, 15, 50, 53, 64, 7

  • 练习2:使用树形选择排序,写出每一趟的排序结果

    2,5,8,3,6,9,1,4,7

  • 练习3:使用树形选择排序,写出每一趟的排序结果

    9 , 20 , 13 , 20 , 12

1.4.4 堆排序

1)概述

  • 堆排序是一种重要的选择排序方法,它只需要一个记录大小的辅助存储空间,每个待排序的记录仅占用一个记录大小的存储空间,因此弥补了树形选择排序的弱点。

  • 大顶堆:每个节点的值都大于或者等于它的左右子节点的值。
    { k i ≥ k 2 i + 1 ( 2 i + 1 ≤ n − 1 ) k i ≥ k 2 i + 2 ( 2 i + 2 ≤ n − 1 ) (大顶堆) egin{cases} k_i ge k_{2i+1} & (2i+1 le n-1) \ k_i ge k_{2i+2} & (2i+2 le n-1) \ end{cases} ag{大顶堆} {kik2i+1kik2i+2(2i+1n1)(2i+2n1)(大顶堆)

  • 小顶堆:每个节点的值都小于或者等于它的左右子节点的值。
    { k i ≤ k 2 i + 1 ( 2 i + 1 ≤ n − 1 ) k i ≤ k 2 i + 2 ( 2 i + 2 ≤ n − 1 ) (小顶堆) egin{cases} k_i le k_{2i+1} & (2i+1 le n-1) \ k_i le k_{2i+2} & (2i+2 le n-1) \ end{cases} ag{小顶堆} {kik2i+1kik2i+2(2i+1n1)(2i+2n1)(小顶堆)

  • 一般升序采用大顶堆,降序采用小顶堆

2)示意图

  • 大顶堆:

  • 小顶堆:

3)算法分析

  • 基本思想:
    • 首先将这n条记录按关键字值的大小建立堆(称为初始堆),将堆顶元素r[0]与r[n-1]交换
    • 然后,将剩下的{r[0]..r[n-2]}序列调整成堆
    • 再将 r[0]与r[n-2]交换,再将剩下的{r[0]..r[n-3]}序列调整成堆
    • 如此反复,直到整个序列有序。
    • 这个过程称为堆排序
  • 要实现堆排序需解决以下两个主要问题:
    1. 将n条记录的序列按关键字值的大小建成初始堆。
    2. 将堆顶记录r[0]与r[i]交换后,如何将序列{r[0]..r[i-1]}按其关键字值的大小调整成一个新堆。

4)筛选法调整堆:分析

  • 将给定无序序列构造成一个小顶堆,调整过程如下:
  1. 假设给定无序序列结构如下,且该序列已经是小顶堆:

  2. 然后交换堆顶元素和最后一个元素,从而筛选出整个序列中的最小值(13)

  3. 然后将剩余的无序序列调整成小堆顶,先从根的左右孩子[25,18]中选择最小值18,然后根与最小孩子比较,并保证根为最小值[18]

  4. 然后继续调整下一层,选择出[63,95,46]中的最小值46

  5. 此时,我们就将一个无需序列构造成了一个小顶堆。

6)筛选法调整堆:算法【7.11】

  • 代码

    //【算法7.11】将以筛选法调整堆算法
    //将以low为根的子树调整成小顶堆,low、high是序列下界和上界
    public void sift(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;                   //当前子树的原根值调整后的位置
    //  System.out.print("sift  " + low + ".." + high + "  ");
    //  display();
    }
    
  • 测试

    package book.ch07;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestSeqList11_sift {
        public static void main(String[] args) throws Exception {
            int[] arr = {13,25,18,33,58,95,46,63};
            SeqList seqList = new SeqList(arr.length);
            System.out.print("序列号:");
            for (int i = 0; i < arr.length; i++) {
                System.out.print("  " + i);
                seqList.insert(i, new RecordNode(arr[i]));
            }
            System.out.println();
            System.out.print("原数据:");
            seqList.display();
            // 树形选择排序(锦标赛排序)
            RecordNode temp = seqList.r[0];
            seqList.r[0] = seqList.r[arr.length - 1];
            seqList.r[arr.length-1] = temp;
            System.out.print("交换值:");
            seqList.display();
            seqList.sift(0, arr.length-1);
            System.out.print("调整后:");
            seqList.display();
        }
    }
    //树形选择排序(锦标赛排序)
    //序列号:  0  1  2  3  4  5  6  7
    //原数据: 13 25 18 33 58 95 46 63
    //交换值: 63 25 18 33 58 95 46 13
    //调整后: 18 25 46 33 58 95 63 13
    

7)建初始堆:分析

  • 为一个无序序列构建堆的过程,就是对完全二叉树从下往上反复筛选的过程。也就是说每一个结点与自己的孩子结点进行比较,从而选择出最小元素。
  • 有孩子的结点,称为非叶子结点,在完全二叉树中,最后一个非叶子结点的编号为 ⌊n/2⌋ -1
  • 因此我们从最后一个非叶子结点开始调整,最后调整到根节点

//序列号:	      0  1  2  3  4  5  6  7
//sift  3..8   33 25 46 13 58 95 18 63
//sift  2..8   33 25 18 13 58 95 46 63
//sift  1..8   33 13 18 25 58 95 46 63
//sift  0..8   13 25 18 33 58 95 46 63

8)堆排序:分析

  • 步骤:
  1. 首先通过对 ⌊n/2⌋ -1记录进行调整,构建堆。
  2. 然后将堆顶的元素,与最后一个元素交换,获得最小元素。
  3. 接着将剩余无序序列,调整成小顶堆,重复步骤2,获得每一次调整后的最小值
  4. 直到调整到根,从而获得排序序列

9)堆排序:算法【7.12】

  • 代码

    //【算法7.12】 堆排序算法
    public void heapSort() {
       // System.out.println("堆排序");
        int n = this.curlen;
        RecordNode temp;
        // 从最后一个非叶子节点开始调整堆,直到根结点
        for (int i = n / 2 - 1; i >= 0; i--) {//创建堆
            sift(i, n);
        }
        System.out.println("堆构建完成");
        for (int i = n - 1; i > 0; i--) {//每趟将最小值交换到后面,再调整成堆
            // 第一个元素和无序中的最后一个交换
            temp = r[0];
            r[0] = r[i];
            r[i] = temp;
            sift(0, i);
        }
    }
    
  • 测试

    package book.ch07;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestSeqList12_heap {
        public static void main(String[] args) throws Exception {
            int[] arr = {33,25,46,13,58,95,18,63};
            SeqList seqList = new SeqList(arr.length);
            System.out.print("序列号:		");
            for (int i = 0; i < arr.length; i++) {
                System.out.print("  " + i);
                seqList.insert(i, new RecordNode(arr[i]));
            }
            System.out.println();
            // 树形选择排序(锦标赛排序)
            seqList.heapSort();
        }
    }
    //树形选择排序(锦标赛排序)
    //序列号:	    0  1  2  3  4  5  6  7
    //sift  3..8   33 25 46 13 58 95 18 63
    //sift  2..8   33 25 18 13 58 95 46 63
    //sift  1..8   33 13 18 25 58 95 46 63
    //sift  0..8   13 25 18 33 58 95 46 63
    //堆构建完成
    //sift  0..7   18 25 46 33 58 95 63 13
    //sift  0..6   25 33 46 63 58 95 18 13
    //sift  0..5   33 58 46 63 95 25 18 13
    //sift  0..4   46 58 95 63 33 25 18 13
    //sift  0..3   58 63 95 46 33 25 18 13
    //sift  0..2   63 95 58 46 33 25 18 13
    //sift  0..1   95 63 58 46 33 25 18 13
    

10)性能分析

  • 空间复杂度:需要一个记录辅助存储空间,O(1)
  • 最坏时间复杂度:O(nlog2n)
  • 堆排序是不稳定的排序算法。

11)练习

  • 练习1:使用xx排序,写出每一趟的排序结果

    16, 15, 50, 53, 64, 7

  • 练习2:使用xx排序,写出每一趟的排序结果

    2,5,8,3,6,9,1,4,7

  • 练习3:使用xx排序,写出每一趟的排序结果

    9 , 20 , 13 , 20 , 12

1.5 归并排序

1.5.1 概述

  • 归并排序,Merging Sort
  • 归并排序:将两个或两个以上的有序表合并成一个新的有序表。
  • 归并排序分类:
    • 二路归并排序
    • 多路归并排序
  • 二路归并排序:将两个有序表合并成一个有序表的归并排序,称为二路归并排序。也就是将两个相邻的记录有序子序列归并为一个记录的有序序列。
  • 二路归并排序基本思想:
    • 将待排序记录r[0]到r[n-1]看成是n个长度为1的有序子表
    • 把这些子表依次两两归并,便得到[n/2]个有序子表
    • 然后,再把这[n/2]个有序的子表进行两两归并
    • 如此重复,直到最后得到一个长度为n的有序表为止

1.5.2 两个相邻有序序列归并:分析

  • 假设前后两个有序序列分别存放在一维数组人的r[h…m] 和 r[m+1…t]中,同时,提供另一个有序数组order,用于存放归并后的数据
  • 首先在两个有序序列中,分别从第1个记录开始进行对应关键字的比较,将关键字值较小的记录放入有序数据order中
  • 然后,依次对两个有序序列中剩余记录进行相同操作,直到两个有序序列中的所有记录都加入到有序数组order中为止
  • 最后,这个有序数组order中存放的记录序列就是归并排序后的结果。

1.5.3 两个相邻有序序列归并:算法【7.13】

  • 代码

    /**
     *
     * @param r 待排序的数组(多个有序子序列)
     * @param order 已经排序号的数组
     * @param h 第一个子序列开始的位置
     * @param m 第一个子序列结束的位置,第二个子序列开始的位置为m+1
     * @param t 第二个子序列结束的位置
     */
    //【算法7.13】两个有序序列的归并算法
    //把r数组中两个相邻的有序表r[h]~r[m]和r[m+1]~r[t]归并为一个有序表order[h]~order[t]
    public void merge(RecordNode[] r, RecordNode[] order, int h, int m, int t) {
        int i = h, j = m + 1, k = h;
        while (i <= m && j <= t) {                  // 将r中两个相邻子序列归并到order中
            if (r[i].key.compareTo(r[j].key) <= 0) {// 较小值复制到order中
                order[k++] = r[i++];
            } else {
                order[k++] = r[j++];
            }
        }
        while (i <= m) {                // 将前一个子序列剩余元素复制到order中
            order[k++] = r[i++];
        }
        while (j <= t) {                // 将后一个子序列剩余元素复制到order中
            order[k++] = r[j++];
        }
    }
    
  • 测试

    package book.ch07;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestSeqList13_merge {
        public static void main(String[] args) throws Exception {
            int[] arr = {39,52,67,95,8,25,56,70};
            SeqList seqList = new SeqList(arr.length);
            System.out.print("序列号:");
            for (int i = 0; i < arr.length; i++) {
                System.out.print("  " + i);
                seqList.insert(i, new RecordNode(arr[i]));
            }
            System.out.println();
            System.out.print("归并前:");
            seqList.display();
            // 归并操作
            System.out.print("归并后:");
            RecordNode[] temp = new RecordNode[arr.length];
            // 待归并的数据 {39,52,67,95}和 {8,25,56,70}
            seqList.merge(seqList.r,temp,0,3, 7);
            // 输出归并后的数据
            for (int i = 0; i < temp.length; i++) {
                String str = temp[i].key.toString().length() == 1 ? "  " : " ";
                System.out.print(str + temp[i].key.toString());
            }
            System.out.println();
        }
    }
    //归并操作
    //序列号:  0  1  2  3  4  5  6  7
    //归并前: 39 52 67 95  8 25 56 70
    //归并后:  8 25 39 52 56 67 70 95
    
    

1.5.4 一趟归并:分析

  • 假设r为待排序的数组,n为待排序列的长度,s为待归并的有序子序列的长度,一趟归并排序的结果存放在数组order中。
  • 两个相邻的有序序列,第一趟处理的长度为1,第二趟为2,第三趟为4 … ,也就是说s的取值1/2/4/8…
  • 步骤:
    1. 根据长度s,进行两两归并
    2. 归并操作时,如果最后两个序列长度不等,将剩余内容归并到有序表中
    3. 归并操作时,如果有未参加归并操作的内容,直接复制到order中

1.5.5 一趟归并:算法【7.14】

  • 代码

    //【算法7.14】一趟归并算法
    //把数组r[n]中每个长度为s的有序表两两归并到数组order[n]中
    //s 为子序列的长度,n为排序序列的长度
    public void mergepass(RecordNode[] r, RecordNode[] order, int s, int n) {
        System.out.print("子序列长度s=" + s + " ");
        int p = 0;  //p为每一对待合并表的第一个元素的下标,初值为0
        while (p + 2 * s - 1 <= n - 1) {  //两两归并长度均为s的有序表
            merge(r, order, p, p + s - 1, p + 2 * s - 1);
            p += 2 * s;
        }
        if (p + s - 1 < n - 1) {  //归并最后两个长度不等的有序表
            merge(r, order, p, p + s - 1, n - 1);
        } else {
            for (int i = p; i <= n - 1; i++) //将剩余的有序表复制到order中
            {
                order[i] = r[i];
            }
        }
    }
    
  • 测试:s=1

    package book.ch07;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestSeqList14_mergepass {
        public static void main(String[] args) throws Exception {
            int[] arr = {52,39,67,95,70,8,25,52,56};
            SeqList seqList = new SeqList(arr.length);
            System.out.print("序列号:		 ");
            for (int i = 0; i < arr.length; i++) {
                System.out.print("  " + i);
                seqList.insert(i, new RecordNode(arr[i]));
            }
            System.out.println();
            System.out.print("初始值:		 ");
            seqList.display();
            // 一趟归并算法
            RecordNode[] temp = new RecordNode[arr.length];
            int s = 1;
            seqList.mergepass(seqList.r, temp, s, arr.length);
            for (int i = 0; i < temp.length; i++) {
                String str = temp[i].key.toString().length() == 1 ? "  " : " ";
                System.out.print(str + temp[i].key.toString());
            }
            System.out.println();
    
        }
    }
    //一趟归并算法
    //序列号:		 0  1  2  3  4  5  6  7  8
    //初始值:		52 39 67 95 70  8 25 52 56
    //子序列长度s=1  39 52 67 95  8 70 25 52 56
    
    
  • 测试:s=8

    package book.ch07;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestSeqList14_mergepass2 {
        public static void main(String[] args) throws Exception {
            int[] arr = {8,25,39,52,52,67,70,95,56};
            SeqList seqList = new SeqList(arr.length);
            System.out.print("序列号:		 ");
            for (int i = 0; i < arr.length; i++) {
                System.out.print("  " + i);
                seqList.insert(i, new RecordNode(arr[i]));
            }
            System.out.println();
            System.out.print("初始值:		 ");
            seqList.display();
            // 一趟归并算法
            RecordNode[] temp = new RecordNode[arr.length];
            int s = 8;
            seqList.mergepass(seqList.r, temp, s, arr.length);
            for (int i = 0; i < temp.length; i++) {
                String str = temp[i].key.toString().length() == 1 ? "  " : " ";
                System.out.print(str + temp[i].key.toString());
            }
            System.out.println();
    
        }
    }
    //一趟归并算法
    //序列号:		 0  1  2  3  4  5  6  7  8
    //初始值:		 8 25 39 52 52 67 70 95 56
    //子序列长度s=8   8 25 39 52 52 56 67 70 95
    
    

1.5.6 二路归并:分析

  • 设置待排序的n个记录保存在数组r[n]中,归并过程中需要引入辅助数组temp[n],

  • 第1趟由r归并到temp,第2趟由temp归并到r

  • 如此反复,直到n个记录成为一个有序表为止。

  • 在归并过程中,为了将最后的排序结果仍置于数组中,需要进行的归并趟数为偶数,

  • 如果实际上只需奇数趟即可生成,那么最后还要进行一趟,正好此时temp中的n个有序记录,为一个长度不大于s的表,将会背直接复制r。

1.5.7 二路归并: 算法【7.15】

  • 添加打印方法

    public void display(RecordNode[] arr) {    //输出数组元素
        for (int i = 0; i < arr.length; i++) {
            String str = arr[i].key.toString().length() == 1 ? "  " : " ";
            System.out.print(str + arr[i].key.toString());
        }
        System.out.println();
    }
    
  • 算法代码

    //【算法7.15】2-路归并排序算法
    public void mergeSort() {
        System.out.println("归并排序");
        int s = 1;                              // s为已排序的子序列长度,初值为1
        int n = this.curlen;
        RecordNode[] temp = new RecordNode[n];  // 定义长度为n的辅助数组temp
        while (s < n) {
            mergepass(r, temp, s, n);           // 一趟归并,将r数组中各子序列归并到temp中
            display(temp);                      // 打印temp临时数组
            s *= 2;                             // 子序列长度加倍,下一趟归并
            mergepass(temp, r, s, n);           // 将temp数组中各子序列再归并到r中
            display();                          // 打印r数组
            s *= 2;
        }
    }
    
  • 测试

    package book.ch07;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestSeqList15_mergeSort {
        public static void main(String[] args) throws Exception {
            int[] arr = {52,39,67,95,70,8,25,52,56};
            SeqList seqList = new SeqList(arr.length);
            System.out.print("序列号:		 ");
            for (int i = 0; i < arr.length; i++) {
                System.out.print("  " + i);
                seqList.insert(i, new RecordNode(arr[i]));
            }
            System.out.println();
            System.out.print("初始值:		 ");
            seqList.display();
            // 二路归并算法
            seqList.mergeSort();
    
        }
    }
    //二路归并算法
    //序列号:		0  1  2  3  4  5  6  7  8
    //初始值:		52 39 67 95 70  8 25 52 56
    //归并排序
    //子序列长度s=1  39 52 67 95  8 70 25 52 56
    //子序列长度s=2  39 52 67 95  8 25 52 70 56
    //子序列长度s=4   8 25 39 52 52 67 70 95 56
    //子序列长度s=8   8 25 39 52 52 56 67 70 95
    
    

1.5.8 性能分析

  • 时间复杂度:
    • 归并趟数:log2n
    • 每一趟时间复杂度:O(n)
    • 二路归并排序:O(nlog2n)
  • 空间复杂度:O(n)
  • 二路归并是一个稳定的排序算法

1.6 基数排序

1.6.1 概述

  • 基数排序(Radix Sort):是一种借助于多关键字进行排序,也就是一种将单关键字按基数分成“多关键字”进行排序的方法。
  • 基数排序分类:
    • 多关键字排序
    • 链式基数排序

1.6.2 多关键字排序

  • n个记录的序列 {R1, R2, … , Rn}对关键字(k1, k2, … , kd)有序。

  • 就是说,对于序列中任意两个记录Ri和Rj (1≤i<j≤n) 都满足下列有序关系:

    (ki1, ki2, … , kid) < (kj1, kj2, … , kjd)

    • K1称为最主位关键字
    • Kd称为最主位关键字
  • 多关键字排序按照关键字的顺序分为两种方式:

    • 最主位优先MSD法:先对K1进行分组,同一组中记录,若关键字K1相等,再对各组按K2排序分成子组,… , 至最后对最次位关键字排序完成为止。
    • 最次为优先LSD法:先对Kd进行排序,然后对Kd-1进行排序,依次类推,直至对主要位关键字K1排序完成为止。(排序过程中不需要根据“前一个”关键字的排序结果,将记录序列分隔成若干个子序列)
  • 例如:学生记录含三个关键字,系别班号班内的序列号,其中以系别为最主位关键字。

1.6.3 链式基数排序

  • 假如多关键字的记录序列中,每个关键字的取值范围相同,则按LSD法进行排序时,可以采用“分配-收集”的方法,其好处是不需要进行关键字间的比较。

  • 对于数字型或字符型的单关键字,可以看成是由多个数位或多个字符构成的多关键字,此时可以采用这种“分配-收集”的办法进行排序,称为基数排序法。

  • 例如:关键字如下{209, 386, 768, 185, 247, 606, 230, 834, 539}

    • 首先按其个位数取值分别为0,1,…9分配成10组,之后按从0至9的顺序将他们收集在一起
    • 然后按其十位数取值分别为0,1,…9分配成10组,之后再按从0至9的顺序将他们收集在一起
    • 最后按其百位数重复一遍上述操作
  • 在计算机上实现基数排序时,为减少所需辅助存储空间,应采用链表作存储结构,即链式基数排序,具体操作:

    1. 待排序记录以指针相链,构成一个链表
    2. 分配时,按当前关键字位所取值,将记录分配到不同的链队列中,每个队列中记录的关键字位相同
    3. 收集时,按当前关键字位取值从小到大将各队列首尾相链成一个链表
    4. 对每个关键字位均 重复2)和3)两步
  • 例如:

  • 注意:

    1. 分配和收集的实际操作仅为修改链表中的指针和设置队列的头、尾指针
    2. 为查找使用,该链表尚需将它调整为有序表。

1.7 各种排序的比较

=========================================================

后记

好啦,以上就是本期全部内容,能看到这里的人呀,都是能人

十年修得同船渡,大家一起点关注。

我是♚焕蓝·未来,感谢各位【能人】的:点赞收藏评论,我们下期见!

各位能人们的支持就是♚焕蓝·未来前进的巨大动力~

注:如果本篇Blog有任何错误和建议,欢迎能人们留言!

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。