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

news2024/11/17 0:20:36

排序算法(二)

前面介绍了排序算法的时间复杂度和空间复杂数据结构与算法—排序算法(一)时间复杂度和空间复杂度介绍-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 归并排序时间复杂度测试

1.冒泡排序

1.1 基本介绍

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

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

1.2 冒泡排序应用实例

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

在这里插入图片描述

图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.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 插入排序应用实例

自己写的如下

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.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.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 归并排序思想示意图

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

在这里插入图片描述

图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中所示,则

  • 第一个大循环中,每个区间有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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1309633.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

transformer文章翻译【Attention is all you need】

参考博客&#xff1a;https://blog.csdn.net/nocml/article/details/103082600 论文目录 摘要1. Introduction2. Background3. Model Architecture3.1 Encoder and Decoder Stacks3.2 Attention3.2.1 Scaled Dot-Product Attention3.2.2 Multi-Head Attention3.2.3 Application…

实验室移液器配套PFA吸头性能稳定特氟龙吸头特点

PFA吸头是一种高性能移液器配件&#xff0c;由聚全氟丙烯&#xff08;Perfluoroalkoxy&#xff09;材料制成。这种材料具有优异的耐化学品、耐热和电绝缘性能&#xff0c;使得PFA吸头在应用中表现出色。 首先&#xff0c;PFA吸头具有卓越的耐化学腐蚀性能。无论是酸性溶液、碱性…

环境保护:人类生存的最后机会

随着科技的进步和人类文明的不断发展&#xff0c;地球上的自然资源也在以惊人的速度消耗殆尽。人类对于环境的无止境的掠夺&#xff0c;使得我们的地球正面临着前所未有的环境危机。环境污染、全球变暖、大规模灭绝等问题不断困扰着我们&#xff0c;似乎指向了人类生存的最后机…

世微AP5414 锂电池升降压 恒流恒压 LED电源驱动IC

产品简介 AP5414 是一种输入电压范围宽&#xff08;0.8~5.5V&#xff09;&#xff0c;可调恒定电流和限定电流两种模式来 驱动白光 LED 而设计的升压型 DC/DC 变换器。该器件能利用单节或双节干电池驱动单 颗大功率白光 LED&#xff0c;同样可以利用一节锂电池驱动两颗、三颗或…

docker gpu 详细部署 video-retalking(跟着步骤打指令就完事了)

本地操作系统&#xff1a;centos 搞个小塔 1、首先在centos下安装宝塔&#xff08;后面会用到&#xff09; 建个容器 2、根据自己要的python版本修改一下语句&#xff0c;这里拉取的是python3.8.8的镜像 docker run -itd --name video-retalking --gpus all --shm-size&qu…

医院污水处理设备远程监控超标报警解决方案

行业背景 近年来&#xff0c;我国医疗机构建设得到了巨大的发展。根据《2022年我国卫生健康事业发展统计公报》&#xff0c;2022年末&#xff0c;全国医疗卫生机构总数达1032918个。截至2022年10月&#xff0c;根据全国排污许可证管理信息平台&#xff0c;共有 13316家医院核发…

ElasticSearch - networking配置global

版本8.11 单机部署了一个节点 在elasticsearch.yml中 配置了network.host: 8.8.8.8(之前为127.0.0.1) 但启动服务失败 报错信息为: BindTransportException: Failed to bind to 8.8.8.8:[9300-9399] 为啥要配置8.8.8.8 是因为参考的官方说明 Networking | Elasticsearch Gu…

RHEL8_Linux下载ansible

本章内容主要介绍RHEL8中如何安装ansible ansible时如何工作的在RHEL8中安装ansible 1.ansible工作原理 如果管理的服务器很多&#xff0c;如几十台甚至几百台&#xff0c;那么就需要一个自动化管理工具了&#xff0c;ansible就是这样的一种自动化管理工具。 1&…

AWS Ubuntu设置DNS解析(解决resolve.conf被覆盖问题)

众所周知&#xff1a; Ubuntu在域名解析时&#xff0c;最直接使用的是/etc/resolve.conf文件&#xff0c;它是/run/systemd/resolve/resolve.conf的软链接&#xff0c;而对于刚装完的ubuntu系统&#xff0c;该文件的内容如下 ubuntuip-172-31-36-184:/etc$ cat resolv.conf #…

Mysql workbench

下载地址: https://download.csdn.net/download/a876106354/88616595

EasyExcel实现⭐️本地excel数据解析并保存到数据库的脚本编写,附案例实现

目录 前言 一、 EasyExcel 简介 二、实战分析 1.Controller控制层 2. service方法和方法实现 3.EasyExcel相关类 3.1 excel表实体类 3.2 自定义监听器类 4.测试 4.1 准备工作 4.2 断点调试 5.生成脚本文件 三、分析总结 章末 小伙伴们大家好&#xff0c;最近开发的时…

spring 笔记三 Spring与Web环境集成

文章目录 Spring与Web环境集成ApplicationContext应用上下文获取方式导入Spring集成web的坐标置ContextLoaderListener监听器通过工具获得应用上下文对象SpringMVC概述SpringMVC快速入门 Spring与Web环境集成 ApplicationContext应用上下文获取方式 应用上下文对象是通过new …

QT-CAD-3D显示操作工具

QT-CAD-3D显示操作工具 一、效果展示二、核心程序三、程序链接 一、效果展示 二、核心程序 TDF_LabelSequence DxfReader::transfer(DocumentPtr doc, TaskProgress* progress) {TDF_LabelSequence seqLabel;Handle_XCAFDoc_ShapeTool shapeTool doc->xcaf().shapeTool();…

ProcessOn在线绘制部分项目流程图

目录 一、ProcessOn 1.1 简介 1.2 官方网站 二、Axure自定义元件库 2.1 新建元件库 2.2 自定义元件 2.3 添加元件库 三、HIS系统门诊流程图 四、HIS系统住院流程图 五、HIS系统药品采购入库流程图 六、OA会议流程图 一、ProcessOn 1.1 简介 ProcessOn是一款在线的流…

快递鸟「物流导盲犬」助力鞋服头部企业客户全链路物流数字化升级

数字化时代&#xff0c;企业全域经营已成为数字商业新浪潮&#xff0c;多店铺多平台多仓库同步发货成为经营常态&#xff0c;消费者对物流服务体验的要求越来越高&#xff0c;企业对物流精细化管理的需求也越来越强烈。快递鸟基于对物流数字化领域的深耕和对行业及客户需求的深…

域名备案通过啦

整体来看还算比较顺利&#xff1a;遇到的问题&#xff1a; 1.香港服务器没有免费的备案码&#xff0c;就单独购买一个备案码用于备案。 2.备案的时候小程序审核优先通过&#xff0c;导致阿里云备案域名不能使用初次备案&#xff0c;不能通过初审&#xff0c;就放弃重新申请备…

css画饼图

<template slot-scope"scope"> <div class"pie" :style"{--p: scope.row.rate}" style"--p:10;--b:10px;--c:#FFAB79;"> <!-- --p&#xff1a;这个变量应该包含百分比值作为一个数字&#xff08;不带%符号&#xff09;…

智能防雷监测系统+智能SPD在线监测系统解决方案

雷电是一种自然现象&#xff0c;也是一种灾害。雷电对人类的生命财产安全造成巨大的威胁&#xff0c;尤其是对于电子设备、通信网络、电力系统等敏感设备&#xff0c;雷电浪涌会导致设备损坏、数据丢失、系统故障等严重后果。因此&#xff0c;防雷措施是必不可少的。 传统的防雷…

Narak

靶场下载 https://download.vulnhub.com/ha/narak.ova 信息收集 # nmap -sn 192.168.1.0/24 -oN live.nmap Starting Nmap 7.94 ( https://nmap.org ) at 2023-12-09 22:18 CST Nmap scan report for 192.168.1.1 (192.168.1.1) Host is up (0.…

class080 状压dp-上【算法】

class080 状压dp-上【算法】 算法讲解080【必备】状压dp-上 Code1 464. 我能赢吗 // 我能赢吗 // 给定两个整数n和m // 两个玩家可以轮流从公共整数池中抽取从1到n的整数&#xff08;不放回&#xff09; // 抽取的整数会累加起来&#xff08;两个玩家都算&#xff09; // 谁在…