【数据结构】 归并排序、 基数排序

简介: 【数据结构】 归并排序、 基数排序

一、什么是归并排序?


归并排序是建立在归并操作上的一种有效,稳定的排序算法。是将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。


二、归并排序


1 . 概述


归并排序,Merging Sort


归并排序:将两个或两个以上的有序表合并成一个新的有序表。


归并排序分类:


1. 二路归并排序


2. 多路归并排序


二路归并排序:将两个有序表合并成一个有序表的归并排序,称为二路归并排序。也就是将两个相邻的记录有序子序列归并为一个记录的有序序列。


二路归并排序基本思想:


1. 将待排序记录r[0]到r[n-1]看成是n个长度为1的有序子表


2. 把这些子表依次两两归并,便得到[n/2]个有序子表


3. 然后,再把这[n/2]个有序的子表进行两两归并


4. 如此重复,直到最后得到一个长度为n的有序表为止


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


假设前后两个有序序列分别存放在一维数组人的r[h..m] 和 r[m+1..t]中,同时,提供另一个有序数组order,用于存放归并后的数据


首先在两个有序序列中,分别从第1个记录开始进行对应关键字的比较,将关键字值较小的记录放入有序数据order中


然后,依次对两个有序序列中剩余记录进行相同操作,直到两个有序序列中的所有记录都加入到有序数组order中为止


最后,这个有序数组order中存放的记录序列就是归并排序后的结果。


2b3ee094a2fd49989a1d1094572f4975.png

3. 两个相邻有序序列归并:算法


代码

/**
 *
 * @param r 待排序的数组(多个有序子序列)
 * @param order 已经排序号的数组
 * @param h 第一个子序列开始的位置
 * @param m 第一个子序列结束的位置,第二个子序列开始的位置为m+1
 * @param t 第二个子序列结束的位置
 */
//【算法】两个有序序列的归并算法
//把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++];
    }
}
  • 测试
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

4. 一趟归并:分析


假设r为待排序的数组,n为待排序列的长度,s为待归并的有序子序列的长度,一趟归并排序的结果存放在数组order中。


两个相邻的有序序列,第一趟处理的长度为1,第二趟为2,第三趟为4 ... ,也就是说s的取值1/2/4/8...


步骤:


根据长度s,进行两两归并


归并操作时,如果最后两个序列长度不等,将剩余内容归并到有序表中


归并操作时,如果有未参加归并操作的内容,直接复制到order中


5. 一趟归并:算法


代码

//【算法】一趟归并算法
//把数组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

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("序列号:\t\t ");
        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("初始值:\t\t ");
        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
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("序列号:\t\t ");
        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("初始值:\t\t ");
        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

6 二路归并:分析


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


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


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


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


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


7. 二路归并: 算法


添加打印方法

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();
}

算法代码

//【算法】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;
    }
}
  • 测试
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("序列号:\t\t ");
        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("初始值:\t\t ");
        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

8. 性能分析


时间复杂度:


1. 归并趟数:log2n


2. 每一趟时间复杂度:O(n)


3. 二路归并排序:O(nlog2n)


空间复杂度:O(n)


二路归并是一个==稳定==的排序算法


三、什么是基数排序?


基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort。它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。


四、基数排序


1 概述


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


基数排序分类:


多关键字排序


链式基数排序


2 多关键字排序


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


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


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


1. K1称为最主位关键字


2. Kd称为最主位关键字


多关键字排序按照关键字的顺序分为两种方式:


1. 最主位优先MSD法:先对K1进行分组,同一组中记录,若关键字K1相等,再对各组按K2排序分成子组,... , 至最后对最次位关键字排序完成为止。


2. 最次为优先LSD法:先对Kd进行排序,然后对Kd-1进行排序,依次类推,直至对主要位关键字K1排序完成为止。(排序过程中不需要根据“前一个”关键字的排序结果,将记录序列分隔成若干个子序列)


例如:学生记录含三个关键字,系别、班号和班内的序列号,其中以系别为最主位关键字。


ab25495f32044d35beb5770770d246cd.png


3. 链式基数排序


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


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


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


1. 首先按其个位数取值分别为0,1,...9分配成10组,之后按从0至9的顺序将他们收集在一起


2. 然后按其十位数取值分别为0,1,...9分配成10组,之后再按从0至9的顺序将他们收集在一起


3. 最后按其百位数重复一遍上述操作


在计算机上实现基数排序时,为减少所需辅助存储空间,应采用链表作存储结构,即链式基数排序,具体操作:


1. 待排序记录以指针相链,构成一个链表


2. 分配时,按当前关键字位所取值,将记录分配到不同的链队列中,每个队列中记录的关键字位相同


3. 收集时,按当前关键字位取值从小到大将各队列首尾相链成一个链表


4. 对每个关键字位均 重复2)和3)两步


例如:

c2ec91d3741a44149930e8eb708dcedb.png


c2ec91d3741a44149930e8eb708dcedb.png

0ea1d583c42e41b5a8233d6e61479bf8.png


注意:


分配和收集的实际操作仅为修改链表中的指针和设置队列的头、尾指针


为查找使用,该链表尚需将它调整为有序表。


五、各种排序的比较


bd101d916199474db23627af21af19b5.png

相关文章
|
4月前
|
存储 搜索推荐 算法
【初阶数据结构篇】归并排序和计数排序(总结篇)
归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide andConquer)的⼀个⾮常典型的应⽤。
62 0
|
2月前
|
算法 搜索推荐 Shell
数据结构与算法学习十二:希尔排序、快速排序(递归、好理解)、归并排序(递归、难理解)
这篇文章介绍了希尔排序、快速排序和归并排序三种排序算法的基本概念、实现思路、代码实现及其测试结果。
37 1
|
2月前
|
算法 搜索推荐 Java
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
基数排序是一种稳定的排序算法,通过将数字按位数切割并分配到不同的桶中,以空间换时间的方式实现快速排序,但占用内存较大,不适合含有负数的数组。
41 0
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
|
2月前
【初阶数据结构】打破递归束缚:掌握非递归版快速排序与归并排序
【初阶数据结构】打破递归束缚:掌握非递归版快速排序与归并排序
|
2月前
|
算法
蓝桥杯宝藏排序 | 数据结构 | 快速排序 归并排序
蓝桥杯宝藏排序 | 数据结构 | 快速排序 归并排序
|
6月前
|
算法
数据结构与算法-归并排序
数据结构与算法-归并排序
35 2
|
6月前
|
搜索推荐
深入理解数据结构第六弹——排序(3)——归并排序
深入理解数据结构第六弹——排序(3)——归并排序
48 1
|
6月前
|
存储 算法 搜索推荐
【数据结构】归并排序的非递归写法和计数排序
【数据结构】归并排序的非递归写法和计数排序
|
7月前
|
存储 搜索推荐 算法
[数据结构]————排序总结——插入排序(直接排序和希尔排序)—选择排序(选择排序和堆排序)-交换排序(冒泡排序和快速排序)—归并排序(归并排序)
[数据结构]————排序总结——插入排序(直接排序和希尔排序)—选择排序(选择排序和堆排序)-交换排序(冒泡排序和快速排序)—归并排序(归并排序)
|
6月前
|
算法 C语言
数据结构和算法——桶排序和基数排序(图示、伪代码、多关键字排序,基数排序代码)
数据结构和算法——桶排序和基数排序(图示、伪代码、多关键字排序,基数排序代码)
52 0

热门文章

最新文章