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

排序算法(二)
前面介绍了排序算法的时间复杂度和空间复杂数据结构与算法—排序算法(一)时间复杂度和空间复杂度介绍-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次大循环
每一趟排序的次数在逐渐减少
如果在某躺排序中,没有发生过一次交换,可以提前结束冒泡排序。这就是优化。
代码如下:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
结果为:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2.4 选择排序时间复杂度测试
测试代码和1.3中几乎相同,不过是bubble()排序函数换成了selectSort函数。

结果为:

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

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

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;    //返回新的有序数组
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
弹幕很多人推荐使用链表的形式,确实会简单,直接插入进去就好了,不需要像数组一样还要后移操作。但是目前只是为了熟悉这个算法,就还是使用的是大家普遍了解的引用数据类型一维数组的形式。没想到后面韩老师使用数组的方法更加方便!

韩老师代码如下

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
**太强了!!**韩老师就用一个数组,两个循环就解决了。相比于我写的少用了一个数组和一个循环。

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

两个循环:我的代码中是通过从前完后比较大小,然后多的一个循环是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; //直接将其插入到首位
            }
        }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
3.4 插入排序时间复杂度测试
测试代码和1.3中几乎相同,只是将bubble()排序函数换成了InsertSort函数。

我的代码结果为:

处理80000个数据所花的时间为:908
处理160000个数据所花的时间为:4185
韩老师代码结果为:

处理80000个数据所花的时间为:692
处理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;
                    }
                }
            }
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  为啥希尔排序法也属于插入法呢,需要从它第一个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;
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
4.5 希尔排序时间复杂度测试
4.5.1 交换法
结果为:

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

处理80000个数据所花的时间为:10
处理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);
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
在尽可能理解老师代码的基础上 自己也写了一份代码 测试过不同种数据 感觉没啥毛病

    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);  //左递归
        }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
5.3 快速排序时间复杂度测试
结果如下:

处理80000个数据所花的时间为:19
处理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);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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);
            }
        }

    }
    

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29


图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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
相比于自顶向下的归并排序而言,在治的时候不是到1才返回,而是直接数据量小于32的时候使用插入排序进行排序后再返回。妙哉!

6.4 归并排序时间复杂度测试
三种结果分别如下:

自顶向下:
处理80000个数据所花的时间为:23
处理160000个数据所花的时间为:36
自下而上:
处理80000个数据所花的时间为:24
处理160000个数据所花的时间为:37
归并+插入
处理80000个数据所花的时间为:15
处理160000个数据所花的时间为:32
由结果可知,确实加入插入后速度快了许多

7 基数排序
7.1 基本介绍
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
基数排序(Radix Sort)是桶排序的扩展
基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
7.2 基本思想
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序成以后,数列就变成一个有序序列。
这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤。


图9 第一轮排序示意图


图10 第二轮排序示意图


图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; //取完元素后置零 为下一次循环做准备
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
  这份代码不能判断负数,会报错。可以听弹幕的先将所有数加上最小值的模,排完序后再减去最小值的模。

7.4 时间复杂度测试
结果如下:

处理80000个数据所花的时间为:49
处理160000个数据所花的时间为:80
对比一下,前两次排序,速度还是算慢的,原因是基数排序适合大数据排序。思路和实现确实比较简单。

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


图12 常用排序算法对比

————————————————
版权声明:本文为CSDN博主「小小低头哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_49429082/article/details/134988544

猜你喜欢

转载自blog.csdn.net/2301_79354153/article/details/135010643