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

排序算法(二)

前面介绍了排序算法的时间复杂度和空间复杂数据结构与算法—排序算法(一)时间复杂度和空间复杂度介绍-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 相关术语解释

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

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

相关文章

排序-归并排序与计数排序

文章目录 一、归并排序1、概念2、过程3、代码实现4、复杂度5、稳定性 二、 计数排序1、思路2、代码实现3、复杂度&#xff1a;4、稳定性 一、归并排序 1、概念 是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已…

关键点检测☞png格式换bmp,且labelme标注的json中imagePath同步修改格式

import os import cv2 import jsondef bmp2jpg(in_img_path, out_dir_name): # .png -> .bmp# img = cv2.imread(in_img_path) # 彩色图片,位深24img =</

GDPU 数据结构 天码行空13

文章目录 一、【实验目的】二、【实验内容】三、实验源代码四、实验结果五、实验总结 一、【实验目的】 (1) 理解插入排序算法的实现过程&#xff1b; &#xff08;2&#xff09;理解不同排序算法的时间复杂度及适用环境&#xff1b; &#xff08;3&#xff09;了解算法性能…

Win11 跑通tensorRT

准备 1.安装cuda&#xff0c;成功之后文件夹如下图所示 2.下载cudnn&#xff0c;把cudnn对应的文件放在cuda里面 3.安装vs 4.安装对应cuda版本的tensorRT https://developer.nvidia.com/tensorrt-download 5.opencv安装 编译好 打开vs&#xff0c;配置环境 用vs打开tens…

PLC-Recorder V3版本软件升级方法

PLC-Recorder V3软件进行了架构优化&#xff0c;包括采集服务器、客户端、授权管理等组件。升级方法与V2版本相似&#xff0c;但是也有一些变化&#xff0c;说明如下&#xff1a; 一、从V2向V3版本升级 1、退出原PLCRecorder&#xff1a;关闭右下角的图标。 2、退出打开的离线…

模型部署系列:10x速度提升,Yolov8检测模型稀疏化——CPU上超500FPS

YOLOv8由广受欢迎的YOLOv3和YOLOv5模型的作者 Ultralytics 开发&#xff0c;凭借其无锚设计将目标检测提升到了一个新的水平。YOLOv8 专为实际部署而设计&#xff0c;重点关注速度、延迟和经济性。 [1] 详细内容请参阅 MarkAI Blog [2] 更多资料及工程项目请关注 MarkAI Githu…

2023年【A特种设备相关管理(锅炉压力容器压力管道)】考试题及A特种设备相关管理(锅炉压力容器压力管道)考试内容

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 A特种设备相关管理&#xff08;锅炉压力容器压力管道&#xff09;考试题是安全生产模拟考试一点通总题库中生成的一套A特种设备相关管理&#xff08;锅炉压力容器压力管道&#xff09;考试内容&#xff0c;安全生产模…

DockerCompose部署RabbitMQ集群

DockerCompose部署RabbitMQ集群 最近小黄在工作中正好需要部署RabbitMQ集群&#xff0c;借此来记录一下&#xff0c;也希望可以帮助到大家 前置条件 简单介绍一下咱们公司现有的条件以及想要达成的效果 服务器3台&#xff0c;3台都是属于一个专有网络中&#xff0c;也就是说3…

【开源】基于JAVA的桃花峪滑雪场租赁系统

项目编号&#xff1a; S 036 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S036&#xff0c;文末获取源码。} 项目编号&#xff1a;S036&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 游客服务2.2 雪场管理 三、数据库设…

关于“Python”的核心知识点整理大全19

目录 ​编辑 8.6.4 使用 as 给模块指定别名 8.6.5 导入模块中的所有函数 8.7 函数编写指南 8.8 小结 第9章 类 9.1 创建和使用类 9.1.1 创建 Dog 类 dog.py 1. 方法__init__() 2. 在Python 2.7中创建类 9.1.2 根据类创建实例 1. 访问属性 2. 调用方法 3. 创建多…

快速搭建知识付费平台?我有才,为你提供一站式解决方案

在当今数字化时代&#xff0c;知识付费已经成为一种趋势&#xff0c;越来越多的人愿意为有价值的知识付费。然而&#xff0c;公共知识付费平台虽然内容丰富&#xff0c;但难以满足个人或企业个性化的需求和品牌打造。同时&#xff0c;开发和维护一个属于自己的知识付费平台需要…

Oracle md5

SQL CREATE OR REPLACE FUNCTION MD5(passwd IN VARCHAR2) RETURN VARCHAR2 ISretval varchar2(32); BEGINretval : utl_raw.cast_to_raw(DBMS_OBFUSCATION_TOOLKIT.MD5(INPUT_STRING > passwd));RETURN retval; END; 测试 select md5(lw112190) from dual 效果

70套大数据可视化大屏模板,总有一款适合你(含演示示例)

分享70款还不错的前端数据可视化大屏源码 其中包含行业&#xff1a;智慧社区、智慧物业、政务系统、智慧交通、智慧工程、智慧医疗、智慧金融银行等&#xff0c;全网最新、最多&#xff0c;最全、最酷、最炫大数据可视化模板。 你可以点击预览获取查看该源码资源的最终展示效果…

三、JS逆向

一、JS逆向 解释&#xff1a;在我们爬虫的过程中经常会遇到参数被加密的情况&#xff0c;这样只有先在前端搞清楚加密参数是怎么生成的才能继续我们的爬虫&#xff0c;而且此时我们还需要用python去执行这个加密的过程。本文主要讲怎么在浏览器调试JS&#xff0c;以及Python执…

基于ssm企业人事管理系统的设计与实现论文

摘 要 进入信息时代以来&#xff0c;很多数据都需要配套软件协助处理&#xff0c;这样可以解决传统方式带来的管理困扰。比如耗时长&#xff0c;成本高&#xff0c;维护数据困难&#xff0c;数据易丢失等缺点。本次使用数据库工具MySQL和编程技术SSM开发的企业人事管理系统&am…

后端对数据库查询的时间格式化

方式一&#xff1a; 在属性上加入注解&#xff0c;对日期进行格式化&#xff0c;如&#xff1a; JsonFormat(pattern "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;方式二&#xff1a; 在WebMvcConfiguration 中扩展Spring MVC的消息转换器&#xf…

基于ssm旅行社管理系统的设计与实现论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本旅行社管理系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&…

RFID助力光伏硅片生产:透明溯源,质量至上

RFID助力光伏硅片生产&#xff1a;透明溯源&#xff0c;质量至上 应用背景 光伏产业作为可再生能源的主要发展方向之一&#xff0c;在过去几十年中取得了显著的进展。为了提高光伏产业的效率和质量。在光伏硅片生产过程中&#xff0c;原材料的获取、管理和追溯对于保证硅片品…

换能器信号工作原理

一、ANB板子发送一个周期&#xff0c;频率为40M和60M的 78V的激励脉冲信号。如下图 频率越高&#xff0c;周期越短。图像分辨率更高。原因如下&#xff1a; ①由于采用的是纵向分辨率。相邻两个点之间必须要间隔 下图的2分之兰大才能被识别。 二、当信号给到换能器后&#xf…

邮政快递物流查询,分析筛选出提前签收件

批量查询邮政快递单号的物流信息&#xff0c;将提前签收件分析筛选出来。 所需工具&#xff1a; 一个【快递批量查询高手】软件 邮政快递单号若干 操作步骤&#xff1a; 步骤1&#xff1a;运行【快递批量查询高手】软件&#xff0c;并登录 步骤2&#xff1a;点击主界面左上角…