当前位置: 首页 > news >正文

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

排序算法(二)

前面介绍了排序算法的时间复杂度和空间复杂数据结构与算法—排序算法(一)时间复杂度和空间复杂度介绍-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次大循环
  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个数据所花的时间为:37083int[] 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 + 1arr[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左边的数都小于pivotl++;}//找出右边小于等于pivot的数while (arr[r] > pivot) { //当右边的数小于等于pivot时才跳出循环r--;    //没找到就找下一个}//两个循环结束后 l 和 r 都找到了不满足位置条件的数 的位置 对应分别为arr[l] 和 arr[r]的值//不可能出现l > r的情况 因为最后总会有r或l指向privot 顶多出现l和r同时指向privotif (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 = 1merge(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 - 1int 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-1int 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 = 1merge(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 第一轮排序示意图

在这里插入图片描述

图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; //取完元素后置零 为下一次循环做准备}}}
}

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

7.4 时间复杂度测试

结果如下:

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

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

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

在这里插入图片描述

图12 常用排序算法对比

在这里插入图片描述

图13 相关术语解释

相关文章:

  • 电脑开机出现:CLIENT MAD ADDR (网卡启动系统)的解决办法
  • 如何将从GitHub上弄下来的Three.js本地官网设为中文
  • 云架构的思考4--云上灾备
  • 香港科技大学数据建模(MSc DDM)硕士学位项目(2024年秋季入学)招生宣讲会-四川大学专场
  • docker私有仓库
  • Kubernetes 容器编排 -- 1
  • SpringIOC之FilterType
  • squid SSL https
  • Draw.io绘图操作
  • Linux-----2、虚拟机安装Linux
  • REST与RPC = 面向对象和函数编程
  • 云原生之深入解析K8s中的微服务项目设计与实现
  • vscode 环境配置
  • 实验01:静态路由配置实验
  • 前端体系:前端应用
  • 77. Combinations
  • Docker 1.12实践:Docker Service、Stack与分布式应用捆绑包
  • es6(二):字符串的扩展
  • JS题目及答案整理
  • miniui datagrid 的客户端分页解决方案 - CS结合
  • Netty源码解析1-Buffer
  • node和express搭建代理服务器(源码)
  • Phpstorm怎样批量删除空行?
  • Python 基础起步 (十) 什么叫函数?
  • SpringBoot几种定时任务的实现方式
  • Wamp集成环境 添加PHP的新版本
  • Yeoman_Bower_Grunt
  • 跨域
  • 前端面试总结(at, md)
  • 通过git安装npm私有模块
  • 用Visual Studio开发以太坊智能合约
  • ​业务双活的数据切换思路设计(下)
  • !!java web学习笔记(一到五)
  • #1014 : Trie树
  • #经典论文 异质山坡的物理模型 2 有效导水率
  • #我与Java虚拟机的故事#连载09:面试大厂逃不过的JVM
  • (14)Hive调优——合并小文件
  • (Python) SOAP Web Service (HTTP POST)
  • (附源码)springboot教学评价 毕业设计 641310
  • (附源码)计算机毕业设计SSM保险客户管理系统
  • (教学思路 C#之类三)方法参数类型(ref、out、parmas)
  • (四)Android布局类型(线性布局LinearLayout)
  • (淘宝无限适配)手机端rem布局详解(转载非原创)
  • (转)机器学习的数学基础(1)--Dirichlet分布
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .Net 4.0并行库实用性演练
  • .Net Framework 4.x 程序到底运行在哪个 CLR 版本之上
  • .NET3.5下用Lambda简化跨线程访问窗体控件,避免繁复的delegate,Invoke(转)
  • .Net6支持的操作系统版本(.net8已来,你还在用.netframework4.5吗)
  • .NET框架类在ASP.NET中的使用(2) ——QA
  • /var/log/cvslog 太大
  • @Bean有哪些属性
  • @RestControllerAdvice异常统一处理类失效原因
  • [].shift.call( arguments ) 和 [].slice.call( arguments )
  • [22]. 括号生成