Java【七大排序】算法详细图解,一篇文章吃透

news2024/9/23 5:20:26

文章目录

  • 一、排序相关概念
  • 二、七大排序
    • 1,直接插入排序
    • 2,希尔排序
    • 3,选择排序
    • 4,堆排序
    • 5,冒泡排序
      • 5.1冒泡排序的优化
    • 6,快速排序
      • 6.1 快速排序的优化
    • 7,归并排序
  • 三、排序算法总体分析对比
  • 总结


提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎评论区指点~ 废话不多说,直接发车~

一、排序相关概念

📌排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增📈或递减📉的排列起来的操作

👉以 int 类型数据从小到大排序为例:
排序前:4,1,3,6,8,7,2,5
排序后:1,2,3,4,5,6,7,8

📌稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

👉以 int 类型数据从小到大排序为例:
排序前:4,1,3a,6,8,7,2,3b,5(3a 在 3b 之前)
排序后:1,2,3a3b,4,5,6,7,8(3a 还在 3b 之前,稳定)
排序后:1,2,3b3a,4,5,6,7,8(3a 不在 3b 之前,不稳定)

以下是常见的 7大排序 算法
在这里插入图片描述


二、七大排序

1,直接插入排序

📌直接插入排序的基本操作是将一个记录插入到已经排有序的有序表中, 从而得到一个新的有序表

📌基本思想 : 类似于打扑克牌, 比如你现在手上有三张牌:3, 5, 10, 现在轮到你摸牌了, 你摸了一张7, 你应该把7放在哪里呢?

对于手上已有的3, 5, 10来说, 这几张牌已经排好序了, 那么把7插在那里还能保持有序呢? 很简单, 就在5和10的中间
🚗🚗🚗

那么给定一组无序的数据, 实现直接插入排序的具体过程是什么呢? 如图所示
注意 i , j 下标的变化在这里插入图片描述
在这里插入图片描述

弄懂了过程,看一下代码如何实现

为了方便在Test类中测试,把所有的方法设置为static修饰的静态方法

    /**
     * 直接插入排序
     * 时间复杂度:最坏 O(N^2),最好 O(N)
     * 空间复杂度:O(1)
     * 稳定性:稳定
     * @param array :待排序数组趋于有序时,使用插入排序效率高
     */
    public static void insertSort(int[] array) {
        // 从小到大排序
        if(array == null) {
            return;
        }
        int tmp = 0;
        for (int i = 1; i < array.length; i++) {
            tmp = array[i];
            for (int j = i - 1; j >= 0; j--) {
                // j往后走
                if (array[j] > tmp) {
                    array[j + 1] = array[j];
                    array[j] = tmp;
                } else {
                    break;
                }
            }
        }
    }

👉时间复杂度:
如果数组有序的情况下, 只需要 i 遍历一次数组即可,此时时间复杂度为O(N)
如果数组逆序的情况下, i 在便利数组的同时, j 也要多次遍历数组,此时时间复杂度为O(N^2)

👉空间复杂度:

所以在数组趋近于有序的情况下, 直接插入排序的效率比较高

只有常数个用于记录的额外空间,空间复杂度为O(1)

👉稳定性:
稳定 , 不过也可以修改代码使其变成不稳定的算法

只需要把 if (array[j] > tmp) {
这一行代码中的大于号改成大于等于就变成了不稳定的排序算法

❗️任何稳定的排序算法都是可以实现不稳定的


2,希尔排序

起初,排序算法只有简单类型的:直接插入排序,冒泡排序,选择排序,平均情况下时间复杂都是O(NN),希尔算法是第一批突破O(NN)这个是激昂复杂度的算法之一

📌算法思路:把待排序数组分成X组,每组内有Y个元素进行直接插入排序,然后减小组数X,每组内数据个数Y随之增大,但X为1时,即可整体进行直接插入排序。这样只能够保证每次各组内插入排序完后都是有序的,但算不上数组基本有序基本有序是指,数组内较小数据基本都在前,较大数据基本都在后

例如给你一组数据:3,1,5,7,9,8,6,4,1
如果分组情况是:1️⃣第一组是3,1,5;2️⃣第二组是7,9,8;3️⃣第三组是6,4,1,这样可以吗
答案是不可以,因为这样各组内排序之后得到:1,3,5,7,8,9,1,4,6。很显然不满足基本有序,这叫局部有序

希尔排序的组数划分规则是进行跳跃式分割:将相距某个增量(也就是X的值,为啥?先别管)的记录组成一个子序列,这样才能保证子序列内分别进行插入排序后得到的结果是基本有序

有点晦涩,图解如下:
在这里插入图片描述
在这里插入图片描述

看完了图解过程,应该理解了希尔算法的核心思想就是“缩小增量”,而增量就是所谓的组数gap,理解了过程,来看代码如何实现:

    /**
     * 希尔排序
     * 时间复杂度:O(N^1.3~N^1.5)
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     * @param array
     */
    public static void shellSort(int[] array) {
        int gap = array.length; ;
        while (gap > 1) {
            gap /= 2;
            shell(array, gap);
        }
    }
    private static void shell(int[] array, int gap) {
        // 从小到大排序
        if(array == null) {
            return;
        }
        int tmp = 0;
        for (int i = gap; i < array.length; i++) {
            tmp = array[i];
            for (int j = i - gap; j >= 0; j -= gap) {

                // j往后走
                if (array[j] > tmp) {
                    array[j + gap] = array[j];
                    array[j] = tmp;
                } else {
                    break;
                }
            }
        }
    }

👉时间复杂度:
希尔排序的时间复杂度却决于增量,增量大小决定了循环次数,而因为数据量不同,很难确定一个最合适的增量大小,这也是迄今为止的数学难题,一般认为是O(N^1.3 ~ N ^1.5)

👉空间复杂度:
没有额外空间的开销,只有常数个记录的辅助空间,空间复杂度为O(1)

👉稳定性:不稳定


3,选择排序

📌选择排序 就是通过 n-i次比较,从n-i+1个数据中找到最小的一个记录, 并且和第 i 个数据交换

📌算法思想: 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

比如你预算充足的情况的下, 想选择一辆座驾🚗
1️⃣一开始在网上看上了一台 50w 奔驰E, 觉得蛮不错,这是你的第一个目标座驾
2️⃣然后你到了 4s 店, 往里走看到了 120w 的奔驰S比奔驰E的内饰更漂亮舒适, 外观更霸气, 于是又想买奔驰S
3️⃣接着再往里走看到了 180w 的奔驰AMGE63, 觉得这要性能有性能,要外观有外观, 于是又想买奔驰AMGE63
4️⃣最后等到去交钱签合同的时候,看到了奔驰最尊贵的座驾----- 500w 的迈巴赫更符合你尊贵的身份, 于是当机立断选择了迈巴赫

50w 是一个标准,然后找到了 120w , 暂时选择了1 20w ----后来又找到了 180w , 暂时选择了 180w ----最后又找到了 500w 的迈巴赫, 这已经是最贵的得了, 所以最终选择了 500w 的代替了 50w 的

这就是选则排序的思想

我们反过来,给定你一组数据
1️⃣第一个数据是300,暂时作为标准,
2️⃣在数组中往后找,找到了180,把180记录为最小值
3️⃣又找到了120,把120作为最小值
4️⃣又找到了50,把50最为最小值
没有比50更小的了,让最小值50和300 交换 , 值最小的50来到了前面,而相对比较大的300来到了后面

那么给定一组无序的数据, 实现直接插入排序的具体过程是什么呢? 如图所示
注意 i , j 下标的变化
在这里插入图片描述
❗️❗️注意,这里只分析了一躺排序,并没有把整个数组排有序

理解了过程,来看一下代码如何实现

  /**
     * 选择排序
     * 时间复杂度:O(N^2)
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     * @param array
     */
    public static void selectSort(int[] array) {
        if(array == null) {
            return;
        }
        for (int i = 0; i < array.length; i++) {
            int minInedx = i;
            int j = i + 1;
            for (; j < array.length; j++) {
                if (array[j] < array[minInedx]) {
                    minInedx = j;
                }
            }
            swap(array, minInedx, i);
        }
    }

本篇中还会经常使用到交换,所以单独封装一个方法出来

    private static void swap(int[] array,int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

👉时间复杂度:
最好最差情况,选择排序的数据之间的比较次数都是固定的,
而最好情况:数组有序时,需要交换0次,最坏情况:数组无序时,交换n-1次
总体的排序时间是比较+交换的总和,所以时间复杂度为O(N^2)

选择排序理解和实现起来都比较简单,但效率比较低,一般很少使用选择排序

👉空间复杂度:
没有额外空间的开销,只有常数个记录的辅助空间,空间复杂度为O(1)

👉稳定性:
不稳定


4,堆排序

显然,堆排序是在堆这种数据结构的基础上进行的,是对选择排序的一种优化:

在选择排序中,多次比较之后交换了两个数据,最终目的达到了,但是 并没有记录比较过程, 导致后续数据的排序,可能进行了相同的重复🤦

堆排序就做到了当找到最小数据时,根据比较结果,对其他数据做出相应调整

首先我们要把待排序数组建立一个堆的结构,那么,问题来了,应该建立大根堆,还是小根堆呢?
答案是大根堆

尽管建成了大根堆,物理结构仍然是数组,只是更改了数组的下标,重新安排了数据在数组中的位置,所以可以理解成堆

如果建立小根堆,就无法对其他数据做出相应调整了

在这里插入图片描述
结点变成灰色表示已经完成了对这个数据的排序,其余结点继续向下调整,范围是0~len
所以变灰的结点就可以不再考虑了
在这里插入图片描述

理解了过程,来看代码是如何实现的


    /**
     * 堆排序
     * 时间复杂度:O(N*log₂N)
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     * @param array
     */
    public static void heapSort(int[] array) {
        creatHeap(array);
        int len = array.length - 1;
        while (len > 0) {
            swap(array, 0, len);
            shiftDown(array,0,len);
            len--;
        }

    }
    private static void creatHeap(int[] array) {
        // 从最后一个树的根结点开始向下调整
        for (int parent = (array.length - 1 - 1) / 2
             ; parent >= 0; parent--) {
            shiftDown(array, parent, array.length);
        }
    }
    private static void shiftDown(int[] array, int parent, int length) {
        int child = parent * 2 + 1;
        while(child < length){
            if (child + 1 < length && array[child] < array[child + 1]) {
                child ++;
            }
            if (array[child] > array[parent]) {
                swap(array, child, parent);
                parent = child;
                child = parent * 2 + 1;
            }else {
                break;
            }
        }
    }

👉时间复杂度:
主要有两个部分:建堆➕重建堆
首先是把数组转化成大根堆的时间复杂度是O(N),然后是多次重建堆的时间复杂度是O(NlogN)
所以整体的时间复杂度是O(N
logN)

堆排序的时间复杂度对待排序数据的状态不敏感,所以没有最好最坏情况,性能上比较优越

👉空间复杂度:
没有额外空间的开销,只有常数个记录的辅助空间,空间复杂度为O(1)

👉稳定性:
不稳定


5,冒泡排序

📌冒泡排序是一种交换排序,他的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到有序为止

冒泡应该是最早接触的一种排序算法了,因为它很简单易懂,尽管如此,还是来看一看冒泡的图解过程:
注意 i , j 下标的变化
在这里插入图片描述
在这里插入图片描述
❓❓❓为什么 每趟冒泡排序之后, i++;而 j 总是从0下标开始呢,
因为一趟冒泡排序结束后只是做到了把 待排序数据的最大值 放在 待排序数据的最后, 而最小值不一定在最前面,所以 i 要从 0 下标开始重新找目前待排序数组的最大值,让他往后放, 循环往复, 最后剩下的那个数据一定是最小的

理解了过程, 代码如下:

    /**
     * 冒泡排序
     * 时间复杂度:最坏 O(N*N) 最好 O(N)
     * 时间复杂度:O(1)
     * 稳定性:稳定
     */
    public static void bubleSort(int[] array){
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array.length - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    swap(array, j, j + 1);
                }
            }
        }
    }

5.1冒泡排序的优化

其实 j < array.length - i - 1; 这一句代码也是经过简单的优化的, 避免了 j 的无效循环🔄

但这 还是存在效率较低 的情况, 当 待排数据有序 时, i 还是会继续遍历, 从而带动 j 的遍历, 是纯纯浪费❌

👉所以对上述代码的优化方案是:
每次 i 开始循环时 定义一个标记 false , 如果 j 遍历完数组, 发生了交换, 把 false 改成 true , 如果 j 的循环结束发现标记还是false,说明什么?
说明没有发生交换, 待排数据是整体有序的, 那么就不让 i 继续遍历了,直接返回即可

优化后代码:

    public static void bubleSort(int[] array){
        for (int i = 0; i < array.length; i++) {
            boolean mark = false;
            for (int j = 0; j < array.length - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    swap(array, j, j + 1);
                    mark = true;
                }
            }
            if (mark == false) {
                return;
            }
        }
    }

👉时间复杂度:
最好情况下(数组整体有序),也需要 j 遍历一遍数组,时间复杂度是O(N)
最坏情况下(数组逆序),此时时间复杂度是O(N^2)

👉空间复杂度:
没有额外空间的开销,只有常数个记录的辅助空间,空间复杂度是O(1)

👉稳定性:
稳定


6,快速排序

🔎希尔排序是对直接插入排序的优化,同属于插入排序
🔎堆排序是选择排序的优化,同属于选择排序
🔎快速排序是对冒泡排序的优化,同属于交换排序

快速排序被称为20世纪十大算法之一,也是很多语言的源码中使用的排序算法


基本思想: 通过一趟排序,找到基准(枢轴)📍,将待排序数组分割成独立的两部分,基准(枢轴)📍的一边的数据均比另一边的大/小,再对两边的数据分别继续快速排序,最终整个数组有序

有点晦涩难懂,如图所示:

快速排序有几个不同的实现方式,这里介绍:Hoare法和挖坑法,效率上没有区别,只是核心代码的实现方式有细微差异

挖坑法:
在这里插入图片描述在这里插入图片描述

挖坑法可以理解为,数据每移动一次,就会在原地留下一个“坑”,实际上数组还在原处,只是覆盖到了目标位置上,可是这个数据已经走了,可以当它不在了,空出来一个“坑”

以上是找到快速排序算法中最核心的部分:**找到基准(枢轴)**📍
同时算法中加入了二叉树的思想,找到基准后,把基本当作 “根” ,利用 “左右子树” 递归遍历的思想完成整个数组的排序

所以我们把找到基准这个方法 pirtiton1() 独立封装出来

   /**
     * 快排
     * 时间复杂度:最好情况:O(N^log₂N)
     *          最坏情况:有序或逆序:O(N^N) 数据量大时有可能栈溢出异常
     * 空间复杂度:最好情况:O(logN)
     *        最坏情况:有序或逆序:O(N)
     * 稳定性:不稳定
     */
    public static void quickSort(int[] array) {
         quick(array, 0, array.length - 1);
    }
    private static void quick(int[] array, int start, int end) {
        // 取大于号,防止 start > end
        if (start >= end) {
            return;
        }
        
        int pivot = pirtiton2(array, start, end);
        quick(array, start, pivot - 1);
        quick(array, pivot + 1, end);
    }

    
    // 挖坑法
    private static int pirtiton1(int[] array, int left, int right) {
        int tmp = array[left];
        while (left < right) {
            // 为什么判断条件加等号,
            // 用int[] arr = {5, 7, 3, 1, 4, 9, 6, 5}测试
            while (left < right && array[right] >= tmp) {
                right--;
            }
            array[left] = array[right];
            while (left < right && array[left] <= tmp) {
                left++;
            }
            array[right] = array[left];
        }
        array[left] = tmp;
        return left;
    }

以上是挖坑法的实现,下面图解 Hoare法的实现方式

在这里插入图片描述
在这里插入图片描述

由于快速排序算法最早是由Tony Hoare设计出来的,所以Hoare法算是比挖坑法更原始正统的算法

挖坑法和 Hoare 法的不同体现在找到基准的实现方式,二者效率上基本一致

理解了过程,来看代码如何实现:

	// Hoare法
    private static int pirtiton2(int[] array, int left, int right) {
        int tmp = array[left];
        int tmpIndex = left;
        while (left < right) {
            while (left < right && array[right] >= tmp) {
                right--;
            }
            while (left < right && array[left] <= tmp) {
                left++;
            }
            swap(array, left, right);
        }
        swap(array, tmpIndex, left);
        return left;
    }

👉时间复杂度:
时间性能取决于递归的深度
最好情况:二叉树几乎平衡的时候,也就是数组划分的比较均匀,递归的作用发挥到最大,递归次数最少,只需要 log₂N 次,时间复杂度是O(log₂N),递归过程中还要对 i,j 下标一起扫描数组,所以总体时间复杂度是O(N* log₂N)
最坏情况:二叉树极度不平衡时,也就是二叉树是一颗单分支的斜树(每次划分完基准后,基准总是当前数组中的最小/大值,导致左/右侧没有数据),这种情况下,递归就是一个“冤种”,递归的作用只有徒增时间空间的开销,要递归N-1次,并且加上 i,j 下标一起扫描数组,整体时间复杂度达到(N*N)

👉空间复杂度:
空间性能取决于递归消耗的栈空间
最好情况:已经分析过,需要递归 log₂N 次,空间复杂度为O(log₂N )
最坏情况,已经分析过,需要递归 N-1 次,空间复杂度为O(N)

👉稳定性:
不稳定


6.1 快速排序的优化

上述的快速排序还有很多值得优化的地方,刚刚分析过时间复杂度最坏情况下 达到O(N*N),是因为基准(枢轴)📍的位置太“刁钻”
那么第一步优化就是,如何让基准的位置更合适一些,让他每次都尽量出现在数组的中间

🟥1️⃣优化选取基准
不能使用生成随机数,这也是看运气的,如果数据量很大,而刚好运气很差,随机值生成在极端,还是在做无用功,生成随机数本身也有时间上的开销,更合适的改进方法应该是 三数取中法✅,一般选取 left,right,mid 下标三个数

比如 left,right,mid 下标的值分别是,200,300,50,一眼就能看出中间大小的数是200,那么就选取200作为基准,200和300交换即可

对于一组待排序数据来说, left,right,mid 下标的值都相对很小/大的概率是很小的,因此取 mid 的值作为中间值的可能性是相对比较高的

来看代码实现:

 private static int getMid(int[] array, int left, int right) {
        int mid = (left + right) >>> 1;
        if (array[left] < array[right]) {
            // 左比右小
            if (array[mid] < array[left]) {
                return left;
            } else if (array[mid] > array[right]) {
                return right;
            }else {
                return mid;
            }
        }else {
            if (array[mid] < array[right]) {
                return right;
            } else if (array[mid] > array[left]) {
                return left;
            }else {
                return mid;
            }
        }
    }

对于数据非常大的情况下,三数取中还是不足以保证取到合适的中间值,所以可以再改进为九数取中法,感兴趣的可以自己了解一下

改进之后,数组被划分的过程可以更近似于平衡二叉树,那么问题又来了
当结点个数越来越多,树深度最大的两层结点个数占了整棵树的很大一部分
而放在快速排序当中,这 “结点” 就是被划分之后的 子区间
对于这些数组片段来说还是会继续使用递归,那么深度最大的两层结点的子区间递归开销就会变得非常大,极有可能会导致栈溢出❗️

基于这一点,就必须考虑如何才能减少递归次数

🟥2️⃣优化小数组的排序方案

答案是可以使用直接插入排序

好处:
🔎无论什么排序,一定是越排约有序,总不能越排越无序,那得排到猴年马月天荒地老海枯石烂,而直接插入排序!!!是数据集合越有序,效率越高!!!
🔎不仅是原始待排序数组很大时需要避免栈溢出,当 给定的原始数组很小 的时候也不太适合用递归,因为数据量太小了,再使用递归实际上是”大材小用“”杀鸡用牛刀“”大炮打蚊子“,所以更加适合简单的排序算法

解决方案很简单,就是每次递归前判断此时数组的长度,可以给定一个界限,看心情,这里给定10

在原本的直接插入排序代码上稍作修改即可

private static void insertSort2(int[] array, int left, int right) {
        // 从小到大排序
        if (array == null) {
            return;
        }
        int tmp = 0;
        for (int i = left+1; i <= right; i++) {
            tmp = array[i];
            for (int j = i - 1; j >= left; j--) {
                // j往后走
                if (array[j] > tmp) {
                    array[j + 1] = array[j];
                    array[j] = tmp;
                } else {
                    break;
                }
            }
        }
    }

优化后的 quick()方法:

   private static void quick(int[] array, int start, int end) {
        // 取大于号,防止 start > end
        if (start >= end) {
            return;
        }
        
        // 优化1,三数取中法,让start位置尽量靠近中间
        int mid = getMid(array, start, end);
        swap(array, start, mid);
        // 优化2,最后小区间使用插入排序,减少递归次数
        if(end-start+1 <= 10) {
            insertSort2(array, start, end);
            return;
        }

        int pivot = pirtiton2(array, start, end);
        quick(array, start, pivot - 1);
        quick(array, pivot + 1, end);
    }

7,归并排序

📌归并排序 是建立在归并操作上的一种有效的排序算法, 该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

📌基本思想:假如一个学校只有两个班,怎么算出全校成绩排名呢,是现在各自班里排好序,然后两个班再一起排序,在两个班的成绩表各自有序的情况下,合并起来排序肯定要比整体混乱着排序效率高
假如一个班1000个人,那在班内排名也是相对效率低的,那咋办?可以分成若干个小组先排,再合并几个小组整体排序,这不就是递归吗

归并归并,我的理解就是,递归分割原始数组,分割到足够小时,递归结束,然后返回时合并,并且完成排序

过程图解:

在这里插入图片描述
在这里插入图片描述
⚠️⚠️需要重点理解的是;
❗️❗️在递归进行分割的过程中,是没有真正把数组切开(new了新空间)的,只是函数传参中是有数组,left 和 right 下标的,只是改变了 left 和 right 的值

❗️❗️但是在归并的过程中,是真正的把两个数组的数据合起来(new了新的数组空间),然后再遍历挨个拷贝回原始数组中的。

体现封装的思想:把分割和合并两个方法独立封装起来,并设置成private

  /**
     * 归并排序
     * 时间复杂度:O(N^logN)
     * 空间复杂度:O(N)
     * 稳定性:稳定
     * @param array
     */
    public static void mergeSort(int[] array) {
        divid(array, 0, array.length - 1);
    }

    private static void divid(int[] array, int left, int right) {
        if (left >= right) {
            return;
        }
        int mid = (left + right) >>> 1;
        divid(array, left, mid);
        divid(array, mid + 1, right);
        merge(array, left, right, mid);
    }

    private static void merge(int[] array, int left, int right, int mid) {
        // 其实就是合并两个数组,并使合并后的数组有序
        int l1 = left;
        int l2 = mid + 1;
        int[] tmp = new int[right - left + 1];
        int i = 0;
        while(l1 <= mid && l2 <= right) {
            // 为什么要加等号,防止死循环
            if(array[l1] <= array[l2]) {
                tmp[i++] = array[l1++];
            }
            if (array[l2] <= array[l1]) {
                tmp[i++] = array[l2++];
            }
        }
        // 判断哪个数组还有数据
        while(l1 <= mid) {
            tmp[i++] = array[l1++];

        }
        while(l2 <= right) {
            tmp[i++] = array[l2++];
        }
        for (int j = 0; j < tmp.length; j++) {
            array[j + left] = tmp[j];
        }
    }

⚠️⚠️注意最后一个for循环,这段代码作用是把合并好的有序子数组挨个拷贝回原始数组,但是 array[ j + left ] = tmp[j] 如何理解❓
因为你左右树都递归进行分割合并啊!你总不能原本在原始数组右边的子数组排有序之后挨个从原始数组的0下标开始拷贝吧!


👉时间复杂度:
和快速排序类似,也是递归次数+每次的 i,j 遍历时间,最好最坏平均情况的时间复杂度都是O(N*log₂N)

👉空间复杂度:
递归的开销是O(log₂N),但是需要总长度为N的额外数组空间的消耗,所有总体空间复杂度是O(N+log₂N)

👉稳定性:
稳定


三、排序算法总体分析对比

没有完美的排序算法,任何一种算法都是有优点和缺陷的,即便是大名鼎鼎的快速排序,也只是整体上效率比较高,性能优越

现在就整体分析一下各种排序的优缺点📊
在这里插入图片描述

在分析希尔排序时提到, 早期的排序算法平均时间复杂度都是O(N^2); 因为原理比较简单, 但性能较差, 所以 一般把直接插入排序,选择排序,冒泡排序归为简单排序一类 其他的都归于 改进排序

📚从平均情况看:
改进过的排序: 希尔排序, 堆排序, 归并排序, 快速排序要胜过 简单排序的性能, 而四个改进算法中, 希尔排序的性能最差

📚从最好情况看:
直接插入排序和冒泡排序最快

📚从最坏情况看:
堆排序和归并排序的性能更胜过快排和其他简单排序

📚综合来看:
堆排序和归并排序比较稳定和强大, 情况最坏时好使
直接插入排序和冒泡排序在基本有序时最好用,
而快速排序比较极端, 最好最坏情况都有缺陷 但是 快速排序能够称之为快速排序, 是因为它的综合性能最强💪,一般情况下是最快的

📚从稳定性来看:
改进排序中只有归并排序

📚从数据个数上看:
数据量越少, 越适合用简单排序, 因为堆排, 快速排序, 归并排序, 都用到了递归, 对于少量数据排序有点"炮弹打蚊子"

只要是交换时, 两数据相邻就是稳定的算法,只要是跳跃式的交换就是不稳定, 当然别忘了, 稳定的算法也可以修改代码更改成不稳定的


总结

以上就是七大算法的全部解析, 最后对比了他们的优缺点, 不过通过各项指标来看, 快速排序还是王者👑, 但是对于不同的场景, 还是要选择更符合需求的算法

如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~


上山总比下山辛苦
下篇文章见

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

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

相关文章

多层感知机

多层感知机理论部分 本文系统的讲解多层感知机的pytorch复现&#xff0c;以及详细的代码解释。 部分文字和代码来自《动手学深度学习》&#xff01;&#xff01; 目录多层感知机理论部分隐藏层多层感知机数学逻辑激活函数1. ReLU函数2. sigmoid函数3. tanh函数多层感知机的从零…

Allegro如何快速把推挤的走线变平滑操作指导

Allegro如何快速把推挤的走线变平滑操作指导 Allegro有个非常强大的功能,推挤命令,可以快速的让走线以不报DRC的形式避让目标 推挤后的效果如下图 但是走线不够平滑,如果每一段都去再推一下比较费时间,下面介绍allegro本身自带的优化类似走线的功能 具体操作如下 点击Rout…

sklearn学习-朴素贝叶斯

文章目录一、概述1、真正的概率分类器2、sklearn中的朴素贝叶斯二、不同分布下的贝叶斯1、高斯朴素贝叶斯GaussianNB2、探索贝叶斯&#xff1a;高斯朴素贝叶斯擅长的数据集3、探索贝叶斯&#xff1a;高斯朴素贝叶斯的拟合效果与运算速度总结一、概述 1、真正的概率分类器 算法…

计算机组成与体系结构

目录 1.计算机结构 2.寻址方式 3.CISC与RISC 4.流水线 1.计算机结构 运算器 算术逻辑单元ALU&#xff1a;数据的算术运算和逻辑运算累加寄存器AC&#xff1a;通用寄存器&#xff0c;为ALU提供一个工作区&#xff0c;用在暂存数据数据缓存寄存器DR&#xff1a;写内存中&…

Linux LVM逻辑卷

目录 LVM逻辑卷 什么是LVM LVM常用术语 管理逻辑卷相关命令 创建LVM逻辑卷 LVM扩容 LVM缩小 LVM快照卷 删除LVM LVM逻辑卷 什么是LVM LVM&#xff08;Logical Volume Manager&#xff09;逻辑卷管理器&#xff0c;是一种硬盘的虚拟化技术&#xff0c;能够实现用户对硬…

基于微信小程序的校园顺路代送小程序

文末联系获取源码 开发语言&#xff1a;Java 框架&#xff1a;ssm JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏览器…

还真不错,今天 Chatgpt 教会我如何开发一款小工具开发(Python 代码实现)

上次使用 Chatgpt 写爬虫&#xff0c;虽然写出来的代码很多需要修改后才能运行&#xff0c;但Chatgpt提供的思路和框架都是没问题。 这次让 Chatgpt 写一写GUI程序&#xff0c;也就是你常看到的桌面图形程序。 由于第一次测试&#xff0c;就来个简单点的&#xff0c;用Python…

Linux命令之grep

Linux grep是一个非常强大的文本搜索工具。按照给定的正则表达式对目标文本进行匹配检查&#xff0c;打印匹配到的行。grep命令可以跟其他命令一起使用&#xff0c;对其他命令的输出进行匹配。 grep语法如下&#xff1a; grep [options] [pattern] content 文本检索 grep可以对…

51单片机蜂鸣器的使用

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录前言一、有源蜂鸣器和无源蜂鸣器的区别二、代码编写总结前言 本文旨在介绍如何使用51单片机驱动蜂鸣器。 一、有源蜂鸣器和无源蜂鸣器的区别 有源蜂鸣器是一种电子…

easyExcel 写复杂表头

写模板 模板图片&#xff1a; 实体类&#xff08;这里没有用Data 是因为Lombok和easyExcal的版本冲突&#xff0c;在导入读取的时候获取不到值&#xff09; package cn.iocoder.yudao.module.project.controller.admin.goods.vo;import com.alibaba.excel.annotation.ExcelI…

编译安装MySQL

MySQL 5.7主要特性 随机root 密码&#xff1a;MySQL 5.7 数据库初始化完成后&#xff0c;会自动生成一个 rootlocalhost 用户&#xff0c;root 用户的密码不为空&#xff0c;而是随机产生一个密码。原生支持&#xff1a;Systemd 更好的性能&#xff1a;对于多核CPU、固态硬盘、…

【蓝桥集训】第四天——双指针

作者&#xff1a;指针不指南吗 专栏&#xff1a;Acwing 蓝桥集训每日一题 &#x1f43e;或许会很慢&#xff0c;但是不可以停下&#x1f43e; 文章目录1.字符串删减2.最长连续不重复子序列3.数组元素的目标和1.字符串删减 给定一个由 n 个小写字母构成的字符串。 现在&#xff…

GCC 同名符号冲突解决办法

一、绪论 作为 C/C 的开发者&#xff0c;大多数都会清楚课本上动态库以及静态库的优缺点&#xff0c;在教科书上谈及到动态库的一个优点是可以节约磁盘和内存的空间&#xff0c;多个可执行程序通过动态库加载的方式共用一段代码段 &#xff1b;而时至今日&#xff0c;再看看上…

数据库浅谈之常见树结构

数据库浅谈之常见树结构 HELLO&#xff0c;各位博友好&#xff0c;我是阿呆 &#x1f648;&#x1f648;&#x1f648; 这里是数据库浅谈系列&#xff0c;收录在专栏 DATABASE 中 &#x1f61c;&#x1f61c;&#x1f61c; 本系列阿呆将记录一些数据库领域相关的知识 &#…

metasploit穷举模块

目录 工具介绍 常用模块 参数介绍 工具使用 工具介绍 Metasploit框架(Metasploit Framework, MSF)是一个开源工具&#xff0c; 旨在方便渗透测试&#xff0c;它是由Ruby程序语言编写的模板化框架&#xff0c;具有很好的扩展性&#xff0c;便于渗透测试人员开发、使用定制的…

浅析C++指针与引用,栈传递的关系

目录 前言 C 堆指针 栈指针 常量指针 指针常量 引用 常量引用 总结 前言 目前做了很多项目&#xff0c;接触到各种语言&#xff0c;基本上用什么学什么&#xff0c;语言的边际就会很模糊&#xff0c;实际上语言的设计大同小异&#xff0c;只是语言具备各自的特性区别。…

Python学习-----模块3.0(正则表达式-->re模块)

目录 前言&#xff1a; 导入模块 1.re.match() 函数 &#xff08;1&#xff09;匹配单个字符 &#xff08;2&#xff09;匹配多个字符 (3) 匹配开头和结尾 2.re.search() 函数 3.re.findall() 函数 4.re.finditer() 函数 5.re.split() 函数 6.re.sub() 函数 7.re.sub…

JAVA BIO,NIO,AIO区别(建议收藏)

Java中的IO原理 首先Java中的IO都是依赖操作系统内核进行的&#xff0c;我们程序中的IO读写其实调用的是操作系统内核中的read&write两大系统调用。 操作系统内核是如何进行IO交互的呢&#xff1f; 网卡中的收到经过网线传来的网络数据&#xff0c;并将网络数据写到内存…

Flink01: 基本介绍

一、什么是Flink 1. Flink是一个开源的分布式&#xff0c;高性能&#xff0c;高可用&#xff0c;准确的流处理框架 &#xff08;1&#xff09;分布式&#xff1a;表示flink程序可以运行在很多台机器上&#xff0c; &#xff08;2&#xff09;高性能&#xff1a;表示Flink处理性…

LabVIEW使用实时跟踪查看器调试多核应用程序

LabVIEW使用实时跟踪查看器调试多核应用程序随着多核CPU的推出&#xff0c;开发人员现在可以在LabVIEW的帮助下充分利用这项新技术的功能。并行编程在为多核CPU开发应用程序时提出了新的挑战&#xff0c;例如同步多个线程对共享内存的并发访问以及处理器关联。LabVIEW可自动处理…