排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序

排序算法(二)

前面介绍了排序算法的时间复杂度和空间复杂数据结构与算法—排序算法(一)时间复杂度和空间复杂度介绍-CSDN博客,这次介绍各种排序算法——冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序。

文章目录

  • 排序算法(二)
    • 1.冒泡排序
      • 1.1 基本介绍
      • 1.2 冒泡排序应用实例
      • 1.3 冒泡排序时间复杂度测试
    • 2.选择排序
      • 2.1 基本介绍
      • 2.2 排序思想
      • 2.3 选择排序应用实例
      • 2.4 选择排序时间复杂度测试
    • 3. 插入排序
      • 3.1 基本介绍
      • 3.2 排序思想
      • 3.3 插入排序应用实例
      • 3.4 插入排序时间复杂度测试
    • 4. 希尔排序
      • 4.1 简单的插入排序存在的问题
      • 4.2 希尔排序法介绍
      • 4.3 基本思想
      • 4.4 希尔排序法应用实例
        • 4.4.1 交换法
        • 4.4.2 移位法
      • 4.5 希尔排序时间复杂度测试
        • 4.5.1 交换法
        • 4.5.2 移位法
    • 5. 快速排序
      • 5.1 基本介绍
      • 5.2 应用实例
      • 5.3 快速排序时间复杂度测试
    • 6. 归并排序
      • 6.1 基本介绍
      • 6.2 基本思想
      • 6.3 应用实例
        • 6.3.1 归并排序—自顶向下
        • 6.3.2 归并排序—自下而上
        • 6.3.3 归并排序+插入排序
      • 6.4 归并排序时间复杂度测试
    • 7 基数排序
      • 7.1 基本介绍
      • 7.2 基本思想
      • 7.3 应用实例
      • 7.4 时间复杂度测试
    • 8.常用排序算法总结和对比

1.冒泡排序

1.1 基本介绍

  **冒泡排序(Bubble Sorting)**的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大
的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒。

  因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置
一个标志flag判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,再进行)

1.2 冒泡排序应用实例

  将五个无序的数:3,9,-1,10,20使用冒泡排序法将其排成一个从小到大的有序数列。

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第1张图片

图1 排序过程图

小结:冒泡排序规则

  1. 一共进行数组大小-1次大循环
  2. 每一趟排序的次数在逐渐减少
  3. 如果在某躺排序中,没有发生过一次交换,可以提前结束冒泡排序。这就是优化

代码如下:

package com.atguigu.sort;

/**
 * @author 小小低头哥
 * @version 1.0
 * 冒泡排序
 */
public class BubbleSort {
    public static void main(String[] args) {
        int arr[] = {3, 9, -1, 10, -2};
        bubble(arr);
        System.out.println("排序后的数组");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }

    //将前面的冒泡排序 封装成一个方法
    public static void bubble(int[] arr){
        boolean flag = false;   //标识变量 表示是否进行过交换
        for (int i = 0; i < arr.length - 1; i++) {
            flag = false;   //重新置false 看这一轮是否进行过交换
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[j + 1] < arr[j]) {  //如果后面的小于前面的 则交换
                    flag = true;    //表示进行过交换
                    int t = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = t;
                }
            }
            if(!flag){  //说明此次排序中 一次交换都没有发生过 说明已经排序完毕
                break;  //结束继续排序
            }
        }
    }
}

1.3 冒泡排序时间复杂度测试

代码如下

    public static void main(String[] args) {
//        int arr[] = {3, 9, -1, 10, -2};

        //测试一下冒泡排序的速度O(n^2),给80000个数据进行测试
        //创建80000个随机的数组

        //处理80000个数据所花的时间为:9360
        //处理160000个数据所花的时间为:37083
        int[] arr = new int[160000];


        for (int i = 0; i < arr.length; i++) {
            //Math.random() [0 1)的小数
            //(Math.random() * 8000000) [0 8000000)小数
            //(int) (Math.random() * 8000000) [0-8000000)的整数
            arr[i] = (int) (Math.random() * 8000000);
        }
        long start = System.currentTimeMillis();
        bubble(arr);
        long end = System.currentTimeMillis();
        System.out.println("处理" + arr.length + "个数据所花的时间为:" + (end - start));
    }

结果为:

  1. 处理80000个数据所花的时间为:9360
  2. 处理160000个数据所花的时间为:37083

可以看出当数据量翻倍的时候,由冒泡排序时间复杂度 O ( n 2 ) O(n^2) O(n2)知,当变成2n时,时间复杂度为 O ( 4 n 2 ) O(4n^2) O(4n2)。时间复杂度变成了四倍,正好和测试结果对的上,太神奇了!

2.选择排序

2.1 基本介绍

  选择排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。

2.2 排序思想

  选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从arr[o]~arr[n-1]中选取最小值,与arr[0]交换,第二次从arr[1] arr[n-1]中选取最小值,与arr[1]交换,第三次从arr[2]~arr[n-1]中选取最小值, 与arr[2]交换,…,第i次从arr[i-1]~arr[n-1]中选取最小值,与arr[i-1]交换,…,第n-1次从arr[n-2] ~arr[n-1]中选取最小值,与arr[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第2张图片

图2 选择排序思路图

2.3 选择排序应用实例

有一群牛,颜值分别是101,34,119,1请使用选择排序从低到高进行排序[101,34,119,1]。

代码如下

package com.atguigu.sort;

/**
 * @author 小小低头哥
 * @version 1.0
 * 选择排序
 */
public class SelectSort {
    public static void main(String[] args) {
        int[] arr = {101, 34, 119, 1,3,2342,532,5};
        //选择排序
        selectSort(arr);
        System.out.println("排序后的数据为:");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }

    private static void selectSort(int[] arr) {
        int index;  //记录每次大循环中最小数的位置
        for (int i = 0; i < arr.length; i++) {
            index = i;  //每次大循环初始化为第i个 没被排序的第一个
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[index]) {//如果小于指定位置的数
                    //则重新指向更小位置的数
                    index = j;
                }
            }
            //一次大循环结束后 Index就记录下了最小数的位置
            if(index != i){ //说明最小数确实不是第i个位置的数 index发生了变换
                //将其与第i个位置的数进行交换
                int temp = arr[i];
                arr[i] = arr[index];
                arr[index] = temp;
            }
        }
    }
}

2.4 选择排序时间复杂度测试

测试代码和1.3中几乎相同,不过是bubble()排序函数换成了selectSort函数。

结果为:

  1. 处理80000个数据所花的时间为:3350
  2. 处理160000个数据所花的时间为:13237

可以看出当数据量翻倍的时候,由选择排序时间复杂度 O ( n 2 ) O(n^2) O(n2)知,当变成2n时,时间复杂度为 O ( 4 n 2 ) O(4n^2) O(4n2)。时间复杂度变成了四倍,正好和测试结果对的上。与冒泡排序相同。

**进一步分析:**选择排序法相对于冒泡排序速度更快,主要是减少了交换的次数。冒泡排序每次大循环中符合条件就进行值交换,而选择排序每次大循环中只进行一次。

3. 插入排序

3.1 基本介绍

  插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。

3.2 排序思想

  插入排序 (Insertion Sorting) 的基本思想是:把n个待排序的元素看成一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第3张图片

图3 插入排序思路图

3.3 插入排序应用实例

自己写的如下

package com.atguigu.sort;

/**
 * @author 小小低头哥
 * @version 1.0
 * 插入排序
 */
public class InsertSort {
    public static void main(String[] args) {
        int[] arr1 = {17, 3, 25, 60, 4, 15,34,23,45,53,2}; //无序数组
        arr1 = insertSort(arr1);
        System.out.println("插入排序后的数组为:");
        for (int i = 0; i < arr1.length; i++) {
            System.out.print(arr1[i] + " ");
        }
    }

    public static int[] insertSort(int[] arr1){
        int[] arr2 = new int[arr1.length];    //有序数组
        arr2[0] = arr1[0];  //先将第一个值直接移入有序数组
        for (int i = 1; i < arr1.length; i++) { //总共比较arr2.length - 1次
            for (int j = 0; j < i; j++) {   //每次比较i次
                if (arr1[i] < arr2[j]) {  //说明可以插入了
                    for (int k = i; k > j; k--) {
                        arr2[k] = arr2[k - 1];    //从第j个元素开始将arr2的元素往后移
                    }
                    arr2[j] = arr1[i];  //将元素插入
                    break; //结束本次小循环
                }
                if (j == i - 1) { //如果执行到这一步 则说明arr1[i] 在arr2中最大 直接放在最后
                    arr2[i] = arr1[i];
                }
            }
        }
        return arr2;    //返回新的有序数组
    }
}

弹幕很多人推荐使用链表的形式,确实会简单,直接插入进去就好了,不需要像数组一样还要后移操作。但是目前只是为了熟悉这个算法,就还是使用的是大家普遍了解的引用数据类型一维数组的形式。没想到后面韩老师使用数组的方法更加方便!

韩老师代码如下

public static void main(String[] args) {
    int[] arr1 = {17, 3, 25, 60, 4, 15,34,23,45,53,2}; //无序数组
    insertSort(arr1);
    System.out.println("插入排序后的数组为:");
    for (int i = 0; i < arr1.length; i++) {
        System.out.print(arr1[i] + " ");
    }
}

public static void insertSort(int[] arr){

    for (int i = 0; i < arr.length; i++) {
        //定义待插入的数
        int insertVal = arr[i];
        int insertIndex = i - 1;    //即arr[i]前面这个数的下标

        //给insertVal找到插入的位置
        //1. insertIndex >= 0 保证在给insertVal 找插入位置 不越界
        //2. insertVal < arr[insertIndex] 待插入的数 还没有找到插入位置
        while (insertIndex >=0 && insertVal < arr[insertIndex]){
            arr[insertIndex + 1] = arr[insertIndex];
            insertIndex--;
        }
        //退出循环是 说明插入的位置找到了 insertIndex + 1
        arr[insertIndex + 1] = insertVal;
    }
}

**太强了!!**韩老师就用一个数组,两个循环就解决了。相比于我写的少用了一个数组和一个循环。

**一个数组:**其实对比无序和有序数组,一个在减,一个在加,无序的头前面一个正好对应有序的尾。两个数组完全可以用一个数组替代。

两个循环:我的代码中是通过从前完后比较大小,然后多的一个循环是k变量的循环,主要是为了后移。但韩老师的代码中是从后往前比较大小,在比较大小的过程中就已经实现后移了,相当于把我代码中两个for循环合并成一个while循环了。如果我要把我代码中两个for循环化成一个for循环,也需要改变一下比较的顺序,从后往前比较。

我改进后的代码

    public static void insertSort(int[] arr1) {
        int j;
        for (int i = 1; i < arr1.length; i++) { //总共比较arr2.length - 1次
            int temp = arr1[i]; //取出无序的第1个 并暂时将其作为有序的第i个位置的数据
            for ( j = i - 1; j >= 0; j--) {   //对i个数据的有效序列进行排序 每次比较i次
                if (temp < arr1[j]) {  //说明第i个大于第j个位置的
                    arr1[j + 1] = arr1[j];  //将arr1[j]后移 那么下次arr1[j+1]则是无效位置
                }else { //说明temp小于第j个位置的数 那么temp直接用temp去填入arr[j+1]的地方
                    arr1[j + 1] = temp; //此时将此无序数据插入成功
                    break;  //因为是有序数组 所以后面的无须再比较 本次排序结束
                }
            }
            //这个判断语句一定要写出来 从语法上来说可以写在里面
            //但是为了让执行时间少一点,不用每次小循环都判断 一定要写出来
            if(j == 0){ //如果执行到这一步 说明一直在后移 那么则此无序数据最小
                arr1[0] = temp; //直接将其插入到首位
            }
        }
    }

3.4 插入排序时间复杂度测试

测试代码和1.3中几乎相同,只是将bubble()排序函数换成了InsertSort函数。

我的代码结果为:

  1. 处理80000个数据所花的时间为:908
  2. 处理160000个数据所花的时间为:4185

韩老师代码结果为:

  1. 处理80000个数据所花的时间为:692
  2. 处理160000个数据所花的时间为:2634

几乎是韩老师代码的三倍,不过其实最坏时间复杂度是相同的。导致相差这个的原因可能是我的代码中的小循环多了点判断语句。

4. 希尔排序

4.1 简单的插入排序存在的问题

简单的插入排序可能存在的问题:

  • 数组 arr ={2,3,4, 5,6,1} 这时需要插入的数 1(最小),这样的过程是:
    • {2,3,4,5,6,6}
    • {2,3,4,5,5,6}
    • {2,3,4, 4,5,6}
    • {2,3,3,4,5,6}
    • {2,2,3,4,5,6}
    • {1,2,3,4,5,6}

结论: 当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响。

4.2 希尔排序法介绍

  希尔排序是希尔(Donaldshell) 于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为
缩小增量排序

4.3 基本思想

  希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第4张图片

图4 希尔排序示意图

经过上面的“宏观调控”,整个数组的有序化程度成果喜人此时,仅仅需要对以上数列简单微调,无需大量移动操作即可完成整个数组的排序。

  ==我的理解:==就是将一次很大的插入排序变成了几次小的插入排序。但是这几次小的插入排序的运算量(移动次数)很少,比如最后一组,再使用插入排序时,只要判断前一个元素是否满足条件即可,顶多每次就后移一位。大大减小了后移量。则时间复杂度也减少了。

4.4 希尔排序法应用实例

  希尔排序法在对有序序列进行插入时有两种方式:交换法移动法

4.4.1 交换法

  此方法旨在希尔排序时,对有序序列插入时采用交换法

韩老师代码如下(不过注释都是我按我自己理解写的):

package com.atguigu.sort;

/**
 * @author 小小低头哥
 * @version 1.0
 * 希尔排序
 */
public class ShellSort {
    public static void main(String[] args) {
        int[] arr = {5, 6, 1, 7, 61, 3, 41, 46, 3, 1, 55};
        shellSort(arr);
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }

    private static void shellSort(int[] arr) {
        int len = arr.length;   //就算是奇数也无所谓
        while (len != 1) { //当len不等于1时可以继续分组进行排序
            len = len / 2;    //分组 相当于每组的步长
            //之所以从len开始 是从每一组中第二个元素开始与本组中前一个元素进行比较
            //大循环就是控制每次每组中参与排序的元素的个数
            //比如i=len+8 那么就是将arr[len+8]所在组进行排序 且只进行排序arr[len+8]及其之前的数据
            for (int i = len; i < arr.length; i++) {
                //第二个循环之所以i-len 是用来比较本组前一个元素
                //j-=len 是确保比较的元素都是本组中的元素 len是步长
                //类似于进行了一次从后往前遍历的冒泡排序的大循环
                //但由于除arr[i]个元素外 前面本组的元素都是有序的
                //所以一次从后往前遍历的冒泡排序大循环对于将arr[i]排好序足矣
                for (int j = i - len; j >= 0; j -= len) {//此时相当于将arr2[1,2,3,4,5,arr[i]] 进行排序
                    if (arr[j] > arr[j + len]) {    //如果此元素比本组中后一个元素要小
                        //交换元素 将大的元素换到后面去
                        int temp = arr[j];
                        arr[j] = arr[j + len];
                        arr[j + len] = temp;
                    } else { //说明此时本元素最大 就该放在后面
                        //而由于前面本组元素都是有序的 所以不需要再进行判断了
                        //本次判断结束
                        break;
                    }
                }
            }
        }
    }
}

  为啥希尔排序法也属于插入法呢,需要从它第一个for循环来理解。每次进行此for循环,其实就是将arr[i]这个无序的数据插入到本组中已经排好序的有序数组中(即arr[i-len],arr[i-2*len]…)。而完成此过程就是第二个for循环来完成的。第二个for循环类似于冒泡排序,不过是从后面开始一个个比较(也想过能不能从前往后比较,其实不行。首先就是本组的头元素不好确定,其次最重要的原因就是:由于交换法逐个比较,类似于冒泡排序,一次冒泡排序的大循环难以把一个从小到大排序的数组(外加最后一个待排序的元素)排好序)。

4.4.2 移位法

理解了交换法,再理解移位法其实挺好理解的

韩老师代码如下

private static void shellSort2(int[] arr) {
    int len = arr.length;   //就算是奇数也无所谓
    while (len != 1) {  //当len不等于1时可以继续分组进行排序
        len = len / 2;
        for (int i = len; i < arr.length; i++) {    //仍然是从每一组的第二个元素开始进行插入
            //接下来就是复现简单插入排序法
            int j = i;
            int temp = arr[i];    //保存需要插入的数据值
            while (j >= len && arr[j - len] > arr[j]) {  //如果还没比较完最开头的元素以及不满足插入条件
                arr[j] = arr[j - len];    //则后移
                j = j - len;
            }
            //当退出循环后 则说明找到了插入的位置
            arr[j] = temp;
        }
    }
}

4.5 希尔排序时间复杂度测试

4.5.1 交换法

结果为:

  1. 处理80000个数据所花的时间为:14
  2. 处理160000个数据所花的时间为:30
4.5.2 移位法

结果为:

  1. 处理80000个数据所花的时间为:10
  2. 处理160000个数据所花的时间为:13

是的!你没有看错!!两种方法都是这么快,太他喵猛了!!!

5. 快速排序

5.1 基本介绍

  快速排序 (Quicksort)是对冒泡排序的一种改进:基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第5张图片

图5 快速排序示意图

5.2 应用实例

韩老师代码如下

public static void quickSort3(int[] arr, int left, int right) {
    int l = left;   //左指针
    int r = right;  //右指针
    int pivot = arr[(left + right) / 2];
    int temp = 0;

    //while循环的目的是让比pivot 值小的放到左边
    //比pivot值大的放到右边
    while (l < r) {
        //再pivot的左边一直找 找到大于等于pivot值 才退出
        while (arr[l] < pivot) {
            l++;
        }
        //找出右边小于等于pivot的数
        while (arr[r] > pivot) { //当右边的数小于等于pivot时才跳出循环
            r--;    //没找到就找下一个
        }

        //如果 l >= r 说明pivot的左右两边的值 已经按照左边全部是
        //小于等于pivot值 右边全部是大于等于pivot值
        if (l >= r) {
            break;
        }
        //交换位置  将左边找到的大于等于pivot的数放在右边
        //将右边找到的小于等于pivot的数放在左边
        temp = arr[r];
        arr[r] = arr[l];
        arr[l] = temp;

        //如果交换完后 发现这个arr[l] == pivot值 r-- 前移
        if (arr[l] == pivot) {
            r -= 1;
        }
        //如果交换完后 发现arr[r] == pivot值 l++ 后移
        if (arr[r] == pivot) {
            l++;
        }
    }
    //如果l == r 必须l++ r-- 否则可能出现栈溢出
    if (l == r) {
        l++;
        r--;
    }
    //左递归
    if (left < r) {
        quickSort3(arr, left, r);
    }
    //右递归
    if (right > l) {
        quickSort3(arr, l, right);
    }

}

在尽可能理解老师代码的基础上 自己也写了一份代码 测试过不同种数据 感觉没啥毛病

    public static void quickSort(int[] arr, int left, int right) {
        int l = left;   //左指针
        int r = right;  //右指针
        int pivot = arr[(left + right) / 2];
//        System.out.println(pivot);
        int temp = 0;

        //一下操作就为了将小于pivot的数放在其左边 大于pivot的数放在其右边
        while (l < r) {  //当l < r的时候才循环

            //找出左边大于等于pivot的数
            while (arr[l] < pivot) { //当左边的数大于等于pivot时才跳出循环
                //没找到就找下一个 最差的情况就是
                //l此时刚好指向arr[l] = pivot的值
                //此时说明此位置下的pivot左边的数都小于pivot
                l++;
            }
            //找出右边小于等于pivot的数
            while (arr[r] > pivot) { //当右边的数小于等于pivot时才跳出循环
                r--;    //没找到就找下一个
            }

            //两个循环结束后 l 和 r 都找到了不满足位置条件的数 的位置 对应分别为arr[l] 和 arr[r]的值
            //不可能出现l > r的情况 因为最后总会有r或l指向privot 顶多出现l和r同时指向privot
            if (l >= r) { //说明所有数都遍历了 结束了
                break;
            }
            //交换位置  将左边找到的大于等于pivot的数放在右边
            //将右边找到的小于等于pivot的数放在左边
            temp = arr[r];
            arr[r] = arr[l];
            arr[l] = temp;
            //此时l左边的数肯定都小于pivot
            //r右边的数肯定都大于pivot

            //但是 存在一种情况 就是都刚好等于 pivot
            //这个时候如果直接继续下一次循环 那么直接死循环了
            //为了防止这种情况
            while (arr[l] == arr[r] && arr[r] == pivot) {   //如果执行了这个循环 那么已r为基准 arr[r]指向pivot
                //那么我令左边的arr[l]是排在pivot左边的数
                l++;    //l左边的都是小于pivot的数
                if (l >= r) {
                    break;
                }
            }
            /*或者
            while (arr[l] == arr[r] && arr[l] == pivot) {
                //那么我令arr[r]是排在pivot右边的数
                r--;    //r右边的都是大于pivot的数
            }*/
        }
        if (l != r) {
            System.out.println("意料之外");
        }
        //从上面可以看出 退出循环的条件就是l=r; 且此时指向数组中最后边的等于privot的数的位置
//        System.out.println("L=" + l + " r=" + r);
        //除非左边只有一个数或者没数了才不左递归
        if (r > left + 1) {   //说明左边起码还有两个数
            quickSort(arr, left, r - 1);  //左递归
        }

        //除非右边只有一个数或者没数了才不右递归
        if (l < right - 1) {   //说明左边起码还有两个数
            quickSort(arr, l + 1, right);  //左递归
        }
    }

5.3 快速排序时间复杂度测试

结果如下:

  1. 处理80000个数据所花的时间为:19
  2. 处理160000个数据所花的时间为:38

6. 归并排序

6.1 基本介绍

  归并排序(MERGE-SORT 是利用归并的思想实现的排序方法,该算法采用经典的分治 (divide-and-conquer) 策略 (分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案“修补“在起,即分而治之)

6.2 基本思想

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第6张图片

图6 归并排序思想示意图

  可以看到这种结构很像一棵完全二叉树,本节的归并排序我们采用递归去实现(也可采用迭代的方式去实现)分阶段可以理解为就是递归拆分子序列的过程。

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第7张图片

图7 归并排序思想示意图(二)

  治阶段是将两个有序地子序列合并成一个有序序列。图7是治阶段的最后一次合并,实现步骤如图7所示。

6.3 应用实例

原谅我去看了黑马的数据机构与算法才看懂的

6.3.1 归并排序—自顶向下

由于使用了递归 称之为自顶向下。

黑马程序如下

package com.atguigu.sort;

import java.util.Arrays;

/**
 * @author 小小低头哥
 * @version 1.0
 * 归并排序
 */
public class MergeSort2 {
    public static void main(String[] args) {
        int[] a = {9, 3, 7, 2, 8, 5, 1, 4};
        sort(a);
        System.out.println(Arrays.toString(a));
    }

    public static void sort(int[] a1) {
        int[] a2 = new int[a1.length];
        split(a1, 0, a1.length - 1, a2);
    }

    public static void split(int[] a1, int left, int right, int[] a2) {
        int[] array = Arrays.copyOfRange(a1, left, right + 1);
//        System.out.print(Arrays.toString(array));
        //2. 治
        if (left == right) {  //此时只有一个数了
            return; //递归结束
        }
        //1.分
        //>>> 无符号右移 逻辑右移
        //>> 算数右移
        int m = (left + right) >> 1;
        split(a1, left, m, a2);
        split(a1, m + 1, right, a2);
        //左右递归结束

        //3.合 走到这一步 其实就说明其中有一项左右递归结束了
        //从后往前 从左往右 按顺序写出来应该是array = [9 3] m = 0 [7 2] m = 0 [9 3 7 2] m = 1
        // [8 5] m = 0 [1 4] m = 0 [8 5 1 4] m = 1
        merge(a1, left, m, m + 1, right, a2);
        System.arraycopy(a2, left, a1, left, right - left + 1);
        //则返回后a1依次为[3 9 ...] [3 9 2 7...] [2 3 7 9...] [2 3 7 9 5 8...]
        //[2 3 7 9 5 8 1 4...] [2 3 7 9 1 4 5 8] [1, 2, 3, 4, 5, 7, 8, 9]
    }

    /**
     * 将a1有序的两部分 进行排序后合并
     * 从小到大按顺序将数放在a2数组的i到(i+jEnd-j+iEnd-i)位置
     *
     * @param a1   原始数组
     * @param i    第一个有序数组的开头
     * @param iEnd 第一个有序数组的结尾
     * @param j    第二个有序数组的开头
     * @param jEnd 第二个有序数组的结尾
     * @param a2   临时数组
     */
    public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
        int k = i;
        while (i <= iEnd && j <= jEnd) {     //每一次循环都将最小数放在a2[k++]位置 直到某一个数组全部放置完毕
            if (a1[i] < a1[j]) {
                a2[k] = a1[i];
                i++;
            } else {
                a2[k] = a1[j];
                j++;
            }
            k++;
        }
        if (i > iEnd) {   //如果第一个有序数组放置完毕 那肯定第二个有序数组没有放置完毕
            //从a1第j个开始起数jEnd - j + 1个数 将这些数依次放在放在a2第k个数的后面
            System.arraycopy(a1, j, a2, k, jEnd - j + 1);   //因为是顺序的数组 所以直接把剩下的数接在a2的后面
        }
        if (j > jEnd) {   //如果第二个有序数组放置完毕 那肯定第一个有序数组没有放置完毕
            System.arraycopy(a1, i, a2, k, iEnd - i + 1);
        }
    }
}
6.3.2 归并排序—自下而上

黑马程序如下

    public static void sort(int[] a1) {
        int n = a1.length;
        int[] a2 = new int[n];
        //i 代表半个区间的宽度 不同大循环对应的宽度区间不同 每次宽度都会变大两倍
        for (int i = 1; i < n; i *= 2) {
            //[left,right] 分别代表待合并区间的左右边界
            //分别合并宽度为2*i的不同区间中数
            for (int left = 0; left < n; left += 2 * i) {
                //如果合并的数据长度不等 比如a1长度为9 当合并最后两个时 左边为8 右边为1 则会出现left + 2 * i - 1 大于 n-1的情况
                //但是实际此时就是右边界就是n - 1
                int right = Math.min(left + 2 * i - 1, n - 1);
                //不可以写成m = (left + right) / 2
                //如果合并的数据长度不等 比如a1长度为9 当合并最后两个时 左边为8 右边为1
                //此时left=0 right=8 则m = (left + right) / 2=4
                //但实际上应该是m=0+8-1=7 即左边界指向第一个有序数组的边界 m+1指向第二有序数组的开头
                //m在merge中的本质就是第一个有序数组的边界 m+1指向第二有序数组的开头
                //且m可能会超过数组长度 比如当a1长度为9 i=4 即区间宽度为8时,此时有两个有序数组
                //一个是左边的八个 一个是右边的一个
                //如果在判断第二个有序数组m的时候还是这样判断 那么由于此时只有一个数字 且排序好的
                //此时m = n-1
                int m = Math.min(left + i - 1, n - 1);
//                System.out.printf("宽度为 %d [%d,%d]\n",2*i,left,right);
                merge(a1, left, m, m + 1, right, a2);
                System.arraycopy(a2, left, a1, left, right - left + 1);
            }
        }

    }
    

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第8张图片

图8 自下而上示意图

  自下而上方式比如图8中所示,则

  • 第一个大循环中,每个区间有2个元素,宽度为2,半区间长为1,共4个区间,将每个区间的前后半空间进行顺序合并
  • 第二个大循环中,每个区间有4个元素,宽度为4,半区间长为2,共个2区间[0 3] [4 7],将每个区间的前后半空间进行顺序合并
  • 第三个大循环中,每个区间有8个元素,宽度为8,半区间长为4,共1个区间[0 7],将每个区间前后半空间进行顺序合并
6.3.3 归并排序+插入排序

  此方法可以在前面两种方法的基础上提高运行速度。经验表明:归并排序适合数据量比较大的排序运算,插入排序适合数据量比较小的排序算法,且越有序越好

黑马程序如下


    public static void split(int[] a1, int left, int right, int[] a2) {
        int[] array = Arrays.copyOfRange(a1, left, right + 1);
        //2. 治
        if(right - left <= 32){ //当数据量小于32时就认为分结束了 此时采用插入排序将数据治起来
            insertion(a1,left,right);   //插入排序
            return;
        }
        //1.分
        //>>> 无符号右移 逻辑右移
        //>> 算数右移
        int m = (left + right) >> 1;
        split(a1, left, m, a2);
        split(a1, m + 1, right, a2);
        // [8 5] m = 0 [1 4] m = 0 [8 5 1 4] m = 1
        merge(a1, left, m, m + 1, right, a2);
        System.arraycopy(a2, left, a1, left, right - left + 1);

    }
    
private static void insertion(int[] a1, int left, int right) {
    for (int low = left + 1; low <= right; low++) {
        int t = a1[low] ;
        int i = low - 1;
        //自右向左插入位置 如果比待插入元素大 则不断右移 空出出入位置
        while (i >=left && t < a1[i]){
            a1[i + 1] = a1[i];
            i--;
        }
        //找到插入位置
        if(i != low -1){
            a1[i + 1] = t;
        }
    }
}

相比于自顶向下的归并排序而言,在治的时候不是到1才返回,而是直接数据量小于32的时候使用插入排序进行排序后再返回。妙哉!

6.4 归并排序时间复杂度测试

三种结果分别如下:

  • 自顶向下:
    1. 处理80000个数据所花的时间为:23
    2. 处理160000个数据所花的时间为:36
  • 自下而上:
    1. 处理80000个数据所花的时间为:24
    2. 处理160000个数据所花的时间为:37
  • 归并+插入
    1. 处理80000个数据所花的时间为:15
    2. 处理160000个数据所花的时间为:32

由结果可知,确实加入插入后速度快了许多

7 基数排序

7.1 基本介绍

  1. 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
  2. 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
  3. 基数排序(Radix Sort)是桶排序的扩展
  4. 基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。

7.2 基本思想

  1. 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序成以后,数列就变成一个有序序列。
  2. 这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤。

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第9张图片

图9 第一轮排序示意图

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第10张图片

图10 第二轮排序示意图

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第11张图片

图11 第三轮排序示意图

  最后第三轮取出来的数据就是顺序排序的

  我的理解:

  • 其实还挺好理解的。从底位起排序其实就是先将位数底的数先排好序,因为位数相同,数越小的会排在越前面。
  • 比如53,14。因为14的十位数小于53的十位数,所以14会放在53的前面。就算把53假设为13,虽然十位相同,但是由于低位早已做过判断,13的3是小于14的4,所以13早已排在14前面。如果十位不比14大,那么就一直在前面,和实际判断相同。注意:此时个位数都会放在第0个桶中,按照个位数排序的顺序再次被出去。并且此时百位数(千位数…)都已经按照十位大小又重新排好序了(之前按个位排好序,现在用十位排序覆盖了。也符合正常判断),等待百位(千位)判断的逆袭。
  • 也就是每一次排序(第n次),会把n位数及小于n位数的数都排好序,n+1位数以上的就按照n位数的大小去判断,等待n+1位的逆袭。

至于能不能按照从高位到地位的判断,感觉可行。但是确定最大数的位数有点麻烦,过程也麻烦一点,对于相同高位,但次高位不同的数不友好,需要额外判断,没从小到大方便。

7.3 应用实例

韩老师代码如下

package com.atguigu.sort;

import java.util.Arrays;

/**
 * @author 小小低头哥
 * @version 1.0
 * 基数排序
 */
public class RadixSort {

    public static void main(String[] args) {
        int[] arr = {53, 3, 542, 748, 14, 214,46,13,46,1,34,13,46,1,3};
        radixSort(arr);
        System.out.println("arr=" + Arrays.toString(arr));
    }

    //基数排序方法
    public static void radixSort(int[] arr) {

        //1. 得到数组中最大的数的位数
        int max = arr[0];   //假设第一个数就是最大数
        for (int i = 1; i < arr.length; i++) {
            if (max < arr[i]) {
                max = arr[i];
            }
        }
        //得到最大数是几位数
        int maxLength = (max + "").length();

        //定义一个二维数组 表示10个桶 每个桶都是一个一维数组
        //说明
        //1. 二维数组包含10个一维数组
        //2. 为了防止在放入数的时候 数据溢出,则每个一维数组(桶) 大小定为arr.length
        //3. 明确,基数排序是使用空间换时间的经典算法
        int[][] bucket = new int[10][arr.length];

        //为了记录每个桶中 实际存放了多少个数据 定义一个一维数组记录各个桶每次放入的数据个数
        int[] bucketElementCounts = new int[10];


        for (int i = 0; i < maxLength; i++) {   //从低位循环到最高位
            //放置元素
            for (int j = 0; j < arr.length; j++) {
                int digitOfElement = arr[j] / (int) Math.pow(10, i) % 10;   //得到每个为的数
                //由digitOfElement将arr[j]放入到对应数组位置
                bucket[digitOfElement][bucketElementCounts[digitOfElement]++] = arr[j];
            }
            int index = 0;  //索引
            //按顺序取出函数
            for (int j = 0; j < 10; j++) {//循环遍历每一个桶
                //循环遍历每个桶中的元素
                for (int k = 0; k < bucketElementCounts[j]; k++) {
                    arr[index++] = bucket[j][k];
                }
                bucketElementCounts[j] = 0; //取完元素后置零 为下一次循环做准备
            }
        }
    }
}

  这份代码不能判断负数,会报错。可以听弹幕的先将所有数加上最小值的模,排完序后再减去最小值的模。

7.4 时间复杂度测试

结果如下:

  1. 处理80000个数据所花的时间为:49
  2. 处理160000个数据所花的时间为:80

对比一下,前两次排序,速度还是算慢的,原因是基数排序适合大数据排序。思路和实现确实比较简单。

8.常用排序算法总结和对比

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第12张图片

图12 常用排序算法对比

排序算法(二)-冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、基数排序_第13张图片

图13 相关术语解释

你可能感兴趣的:(数据结构与算法,排序算法,算法,java,数据结构)