稳定指的是什么?
 
稳定排序是指原来相等的两个元素前后相对位置在排序后依然不变。

常见的几种排序算法稳定性分析

本文讨论的排序算法有以下这几种:

  • 冒泡排序
  • 选择排序
  • 插入排序
  • 希尔排序
  • 归并排序
  • 快速排序
  • 堆排序

说在最前面,本文分析出来的所谓的稳定性排序,前提是你对该排序算法的实现是正确的,(简单的举个例子,每种排序都少不了比较的过程,比较的时候运算符使用的是 " < " 还是 ” <= “,这就会对你实现的排序算法稳定性产生影响),另外本文给出的稳定排序的代码实现都是正确的,即能保证稳定性的实现。

所以本文在分析完给出的结论是这样的两种情况:

  • 可以保证稳定性
  • 非稳定

注意我的说法,我没有说稳定性的排序,一定是稳定的,这跟你代码实现息息相关!

想要具体了解这个内容的朋友,可以参考我这篇文章,里面有具体的阐述和代码示例。

稳定排序的代码你写对了吗?

1. 冒泡排序

在冒泡排序的算法中,每一次循环都是比较两个相邻的元素,前面比后面大的话,就把前面的往后移,后面的往前挪,说白了就是交换一下这俩元素,相等的话不会执行交换,所以相等的元素的前后相对位置不会发生改变,所以冒泡排序可以是稳定的

代码实现如下:

    /*** 冒泡排序,每次都能将最大的元素干到最后,一共进行n-1趟冒泡* 优点:每次放完都能减少一次比较* @param arr*/public static <T extends Comparable<? super T>> void bubbleSort(T[] arr) {//外层循环代表的是趟数//内层循环代表的是比较两个元素for(int i = 0; i < arr.length-1; i++) {for(int j = 0; j < arr.length - i - 1; j++) {if(arr[j].compareTo(arr[j+1]) > 0) {T temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}}

2. 选择排序

每次考虑一个位置(从前到后,每次循环递增),找出从该位置(当前 index)开始到数组末尾中的最小元素,与该位置进行交换(将当前的最小值与当前 index 处的元素进行交换),可能破坏稳定性。

比如 [5,8,3,5,2],第一次交换把第一个 5 换到最后去了,第二个 5 此时在第一个5位置之前了。所以选择排序是不稳定的。

    /*** 选择排序* @param arr 待排序数组*/public static <T extends Comparable<? super T>> void selectionSort(T[] arr) {int n = arr.length;for(int i = 0; i < n; i++) {//寻找i到n区间内的最小值int minIndex = i;for(int j = i + 1; j < n; j++) {if (arr[j].compareTo(arr[minIndex]) < 0) {minIndex = j;}}//交换下元素swap(arr, i, minIndex);}}

3. 插入排序

从后往前插,一直找到不比它大的元素(说明这时候前面的元素小于等于当前考察的元素),该元素后面就是当前元素该插入的位置,所以并未改变相等元素的先后顺序,所以插入排序可以是稳定的。

    /*** 插入排序* @param arr*/public static <T extends Comparable<? super T>> void insertSort(T[] arr) {int n = arr.length;for(int i = 1; i < n; i++) {T e = arr[i];int j;for(j = i; j > 0 && arr[j - 1].compareTo(e) > 0; j--) {//前面的往后挪,一直到j-1不再大于e了arr[j] = arr[j-1];}//j-1不大于e了。那填入j位置吧arr[j] = e;}}

4. 希尔排序

希尔排序是按照不同步长对元素进行插入排序,一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。

    /*** 希尔排序* @param arr*/public static <T extends Comparable<? super T>> void shellSort(T[] arr) {//gap即为增量,直到gap为1时,此时是对全序列进行一次直接插入排序for(int gap = arr.length / 2; gap > 0 ; gap /= 2) {for(int i = gap; i < arr.length; i++) {T e = arr[i];int j;for(j = i;j >= gap && arr[j - gap].compareTo(e) > 0; j -= gap) {arr[j] = arr[j - gap];}arr[j] = e;}}}

5. 归并排序

每次将当前数组一分为 2 ,将两个排好序的数组进行合并,小的放入,移动指针。

归并排序可以是稳定的。

    // 归并排序,外部调用接口public static <T extends Comparable<? super T>> void mergeSort(T[] arr) {mergeSort(arr, 0, arr.length - 1);}/*** 递归使用归并排序,对arr[l,....r]的范围进行排序* @param arr* @param l 左边界* @param r 右边界*/private static <T extends Comparable<? super T>> void mergeSort(T[] arr,int l,int r) {// l >= r 代表着我们只需要处理一个元素或者一个元素都没有的情况了if (l >= r) {return;}// 二分正确写法,防止整型溢出int mid = (l + r) >>> 2;mergeSort(arr, l, mid);mergeSort(arr, mid + 1, r);// 一个优化方案,如果 arr[mid] 比  arr[mid+1] 小,说明已经有序了,不必 mergeif (arr[mid].compareTo(arr[mid + 1]) > 0) {merge(arr, l, mid, r);}}/*** 将arr[l...mid]和arr[mid+1,.....r]两部分进行归并* @param arr* @param l* @param mid* @param r*/private static <T extends Comparable<? super T>> void merge(T[] arr,int l,int mid,int r) {//声明一个归并过程需要使用的辅助空间T[] aux = (T[] )new Comparable[r - l + 1];//因为需要对原数组进行排序,所以用aux拷贝一份原数组,之后方便后序的归并排序过程for (int i = l; i <= r; i++) {aux[i - l] = arr[i];}//i,j为两个待合并的数组头部索引int i = l;int j = mid + 1;//归并过程for(int k = l; k <= r; k++) {//如果i已经超过mid指针,说明前一个已经排完了,把第二个剩下的都接上就行了if (i > mid) {arr[k] = aux[j - l];j++;}//如果j已经超过r右边界,说明后一个已经排完了,把第一个剩下的都接上就行了else if (j > r) {arr[k] = aux[i - l];i++;}//接下来的两个分支就是对比哪个元素小就进到arr数组里面去else if (aux[i-l].compareTo(aux[j-l]) <= 0) {arr[k] = aux[i - l];i++;}else {arr[k] = aux[j - l];j++;}}}

6. 快速排序

快排的核心思想就是每次我选取一个枢纽元,将小于这个枢纽元的元素划分到一块儿,大于这个枢纽元的元素划分到另一块儿,然后将枢纽元放在这两块儿的中间。快排是不稳定的。看下面这个简单的例子:

假设,当前的枢纽元为 5 ,我已经标上红色,我们已经成功的把小于 5 的元素划分到左半部分(绿色的 3,3,4,3),大于 5 的元素划分到了右半部份(蓝色的 8,9,10,11)。

[ 5 3 3 4 3 8 9 10 11 ]

此时我们将枢纽元交换到两个部分的中间,方法就是将 5 与 左半部分最后一个元素 3 进行交换。到这里你就会发现,原来在后面的 3 现在跑到其它 3 的前面来了。

    //三路快排代码实现(分成三个区间 小于 /等于 /大于 这样以来在重复元素比较多的情况下,效率显著的提高)public static <T extends Comparable<? super T>> void quickSort3ways(T[] arr) {quickSort3ways(arr,0,arr.length - 1);}private static <T extends Comparable<? super T>> void quickSort3ways(T[] arr,int l,int r) {if (l >= r) {return;}swap(arr, l, random.nextInt(r - l) + l);T v = arr[l];//lt小于v的最后一个元素索引int lt = l;//arr[l+1..lt] < v (因为lt = l,所以初始时这个区间为空)//gt为大于v的第一个元素索引int gt = r + 1;//arr[gt...r] > v  (因为gt = r + l,所以初始时这个区间为空)//i为当前待观察的元素int i = l + 1;//arr[lt + 1...i) == v (因为lt + 1 = l + l,所以初始时这个区间为空)//开始正式的三路快排过程while (i < gt) {if (arr[i].compareTo(v) < 0) {swap(arr, lt + 1, i);lt++;i++;}else if (arr[i].compareTo(v) > 0) {swap(arr, gt - 1, i);gt--;}else {//arr[i] == v 啥也不干只移动i指针i++;}}//交换lt与l处的元素,自此原数组被分成了三部分//[l...lt-1] < v; [lt...gt-1] == v ; [gt..r] > vswap(arr, l, lt);quickSort3ways(arr, l, lt-1);quickSort3ways(arr, gt, r);}

7. 堆排序

我们知道堆的结构是节点 i 的孩子为 2 * i 和 2 * i + 1 节点,大顶堆要求父节点大于等于其 2 个子节点,小顶堆要求父节点小于等于其 2 个子节点。在一个长为 n 的序列,堆排序的过程是从第 n / 2 开始和其子节点共 3 个值选择最大(大顶堆)或者最小(小顶堆),这 3 个元素之间的选择当然不会破坏稳定性。但当为 n / 2 - 1, n / 2 - 2, … 1 这些个父节点选择元素时,就会破坏稳定性。有可能第 n / 2 个父节点交换把后面一个元素交换过去了,而第 n / 2 - 1个父节点把后面一个相同的元素没有交换,那么这 2 个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法

package SeniorSort;/*** 堆排序,主要依赖于堆这种数据结构* 1.第一种堆排序的方法:对数组进行Heapify,之后不停的出队就完事儿了* 2.第二种,原地堆排序,不断的把根节点放到最后,然后对剩下的构建堆,再做同样的事* @author xudaxia0610**/
public class HeapSort<T extends Comparable<T>> {/*** 交换数组中的两个元素* @param arr* @param i* @param j*/private static <T> void swap(T[] arr,int i,int j) {T temp = arr[i];arr[i] = arr[j];arr[j] = temp;}/*** 第一种堆排序方法,需要辅助空间,因为需要把待排序的数组数据全部放入堆中* @param arr*/public void heapSort1(T[] arr) {MaxHeap<T> maxHeap = new MaxHeap<>(arr);for(int i = arr.length - 1;i >= 0;i --) {arr[i] = maxHeap.extractMax();}}/*** 第二种堆排序方法:原地堆排序,不需要辅助空间,空间复杂度为O(1)* @param arr*/public static <T extends Comparable<? super T>> void heapSort2(T[] arr) {int len = arr.length;//对原数组heapify的过程(原地heapify)for(int i = (len - 1)/2;i >= 0;i --) {//下沉shiftDown(arr,len,i);}//注意 i是大于0的,因为i等于0时就剩最后一个元素了,不必再去执行排序了for(int i = len-1; i > 0; i--) {//每次将堆顶与最后一个元素交换swap(arr, i, 0);//注意每次下沉都是在i个元素中进行(排除末尾已排好序的元素)shiftDown(arr,i,0);}}/*** 下沉操作* @param arr* @param nums 数组元素个数* @param index 待下沉的索引*/private static <T extends Comparable<? super T>> void shiftDown(T[] arr,int nums,int index) {while ((2*index + 1) < nums) {int j = 2*index + 1;if (j + 1 < nums && arr[j].compareTo(arr[j + 1]) < 0) {j++;}if (arr[index].compareTo(arr[j]) >= 0) {break;}swap(arr, index, j);index = j;}}}

总结

  • 可以保证稳定性的排序:冒泡、插入、归并
  • 不稳定的排序:选择、希尔、快排、堆排

还是那句话,是可以保证,并非是一定是稳定,这跟你的排序算法代码实现正确与否有直接的关系。

具体的可以参考我的另一篇文章。

稳定排序的代码你写对了吗?

几种排序算法的稳定性分析相关推荐

  1. 各种排序算法的稳定性分析

    各种排序算法的稳定性分析 稳定性排序算法 冒泡排序 插入排序 归并问题 不稳定排序算法 选择排序 快速排序 堆排序 各种排序算法思路不清楚的请看 链接 稳定性:稳定性指在排序完成以后,相同元素之间的相 ...

  2. 堆排序稳定性举例_常见排序算法的稳定性分析

    一.不稳定排序算法有哪些 1.堆排序 2.希尔排序 3.快速排序 4.选择排序 口诀:一堆(堆)希尔(希尔)快(快速)选(选择) 二.常见排序算法稳定性分析 1.堆排序稳定性分析 我们知道堆的结构是节 ...

  3. 堆排序稳定性举例_几种排序算法的稳定性

    (1)冒泡排序 冒泡排序就是把小的元素往前调或者把大的元素往后调.比较是相邻的两个元素比较,交换也发生在这两个元素之间.所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的:如果两个相等的元 ...

  4. 常用的8种排序算法稳定性分析

    [1]选择排序.快速排序.希尔排序.堆排序不是稳定的排序算法 冒泡排序.插入排序.归并排序和基数排序都是稳定的排序算法. [2]研究排序算法的稳定性有何意义? 首先,排序算法的稳定性大家应该都知道,通 ...

  5. Java常见的几种排序算法-插入、选择、冒泡、快排、堆排等

    本文就是介绍一些常见的排序算法.排序是一个非常常见的应用场景,很多时候,我们需要根据自己需要排序的数据类型,来自定义排序算法,但是,在这里,我们只介绍这些基础排序算法,包括:插入排序.选择排序.冒泡排 ...

  6. 归并排序改良 java_Java 八种排序算法总结

    image 前言 好久没复习基础了,写个冒泡排序都要想一会.感觉自己好像老了好多,今天手痒总结一下排序算法.目前网上博客普遍都有详细介绍,写的很清楚.说实话我是没必要再写一遍的,感觉就是在啰嗦.还是重 ...

  7. 数据结构中各种排序算法的稳定性比较

    1.简单选择排序 2.堆排序        (1和2是属于选择排序) 3.直接插入排序 4.希尔排序     (3和4属于插入排序,有时把改进后的直接插入排序叫做二分插入) 5.冒泡排序        ...

  8. 八大排序算法的稳定性及时间复杂度

    前言: 首先清楚何谓稳定性? 通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同.在简单形式化一下,如果Ai = Aj,Ai原来在Aj位置前,排序后Ai还是要 ...

  9. 12种排序算法:原理、图解、动画视频演示、代码以及笔试面试题目中的应用

    0.前言 从这一部分开始直接切入我们计算机互联网笔试面试中的重头戏算法了,初始的想法是找一条主线,比如数据结构或者解题思路方法,将博主见过做过整理过的算法题逐个分析一遍(博主当年自己学算法就是用这种比 ...

最新文章

  1. 3.5 面向连接的运输:TCP
  2. ES6 WeakMap的实际用途是什么?
  3. 用vue制作饿了么首页(1)
  4. 【转】usermod 添加用户多个附属组
  5. FLEX实例:GOOGLE地图.
  6. 【最新合集】编译原理习题(含答案)_答案全集_MOOC慕课 哈工大陈鄞
  7. [转载] KAFKA分布式消息系统
  8. 华为手表表盘的数字什么意思_手表的陀飞轮、月相、逆跳都是什么意思?
  9. js代码跳转页面时的路径问题
  10. Layui laypage分页
  11. jQuery - 不同版本的差异汇总(版本选择建议)
  12. 自己动手 DIY 一个读写200MB/s 的高速 U 盘
  13. 计算机,通信,自动化等方向期刊排名
  14. 和Keyle一起学ShaderForge - Overview
  15. 《世界历史》—史前时期的分期
  16. OpenFOAM 粘弹性流体求解器rheoTool
  17. 常用英语口语1000句
  18. 如何设置云服务器语言,云服务器如何更换语言
  19. rstudio中logit模型代码
  20. 已解决:error: cannot connect to daemon

热门文章

  1. Unity给手加入圆柱骨骼
  2. fsolve:对非线性方程组求解
  3. 如何制作一个高转化的广告落地页(文末有福利)
  4. 2017 ACM ICPC Asia Regional - Daejeon Programming Constest
  5. CSS -- CSS字体样式、文本样式、去掉列表的小圆点、背景、背景渐变
  6. linux系统中如何查ip,在Linux系统中查看ip的命令是什么
  7. Vue 模板语法、事件和表单使用
  8. 使用CacheFS+TmpFS加速NFS读取速度
  9. 平凡之路 | 科班小硕的2020年面试小结
  10. 技巧:修改Chrome默认搜索引擎为Google.com