哪些排序是不稳定的?稳定又意味着什么?

      最近有看到稳定排序和不稳定排序这一说,貌似还是挺常识的概念,排序可能大家在面试当中经常被问到,但是对于稳定性和不稳定性,又了解多少呢?下面通过这篇文章给大家全面讲解一下。


 对于排序稳定性的意义:

通俗地讲就是待排序的序列中有两元素相等,排序之后它们的先后顺序不变。例如:如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。

其次就是稳定性带来的好处:

我们平时自己在使用排序算法时用的测试数据就是简单的一些数值,本身没有任何关联信息,这在实际应用中一般没太多用处。实际应该中肯定是排序的数值关联到了其他信息,比如数据库中一个表的主键排序(主键是有关联到其他信息)另外比如对英语字母排序,英语字母的数值关联到了字母这个有意义的信息。可能大部分时候我们不用考虑算法的稳定性,两个元素相等位置是前是后不重要,但有些时候稳定性确实有用处。它体现了程序的健壮性。

场景:比如你网站上针对最热门的文章或啥音乐电影之类的进行排名,由于这里排名不会像我们成绩排名会有并列第几名之说,所以出现了元素相等时也会有先后之分。如果添加进新的元素之后又要重新排名了,之前并列名次的最好是依然保持先后顺序才比较好。

回到主题,现在分析一下常见的排序算法的稳定性。

(1).冒泡排序(稳定)与快速排序(不稳定)

为什么要把这两个放一块儿进行对比呢?因为他们都是属于交换排序。

冒泡排序是通过不停的遍历,以升序为例,如果相邻元素中左边的大于右边的则交换.碰到相等的时就不交换保持原位.所以冒泡排序是一种稳定排序算法

代码案例:

public void bubbleSort(int[] list) {

    int temp = 0; // 用来交换的临时数
 

    // 要遍历的次数

    for (int i = 0; i < list.length - 1; i++) {

        // 从后向前依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上

        for (int j = list.length - 1; j > i; j--) {

            // 比较相邻的元素,如果前面的数大于后面的数,则交换

            if (list[j - 1] > list[j]) {

                temp = list[j - 1];

                list[j - 1] = list[j];

                list[j] = temp;

            }
        }

        System.out.format("第 %d 趟:\t", i);

        printAll(list);

    }
}

快速排序是不稳定的.举例8   5   6  6 .以8为基准,第一趟交换后最后一个6跑到第一位,8到最后.第二趟交换.这个6跑到5的位置.变成有序的了.两个6位置变了,所以是不稳定的,最常见的也就是sort函数了。

代码案例:

**
 * 快速排序
 * 平均O(nlogn),最好O(nlogn),最坏O(n^2);空间复杂度O(nlogn);不稳定;较复杂    
 * @author: Jack_Wu    
 * @date: 2019年6月21日    
 */
	public class QuickSort {
	    public static void quickSort(int[] arr,int low,int high){
	        int i,j,temp,t;
	        if(low>high){
	            return;
	        }
	        i=low;
	        j=high;
	        //temp就是基准位
	        temp = arr[low];

	        while (i<j) {
	            //先看右边,依次往左递减
	            while (temp<=arr[j]&&i<j) {
	                j--;
	            }
	            //再看左边,依次往右递增
	            while (temp>=arr[i]&&i<j) {
	                i++;
	            }
	            //如果满足条件则交换
	            if (i<j) {
	                t = arr[j];
	                arr[j] = arr[i];
	                arr[i] = t;
	            }

	        }
	        //最后将基准为与i和j相等位置的数字交换
	         arr[low] = arr[i];
	         arr[i] = temp;
	        //递归调用左半数组
	        quickSort(arr, low, j-1);
	        //递归调用右半数组
	        quickSort(arr, j+1, high);
	    }


	    public static void main(String[] args){
	        int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
	        quickSort(arr, 0, arr.length-1);
	        for (int i = 0; i < arr.length; i++) {
	            System.out.println(arr[i]);
	        }
	    }
	}

结果如下:

(2)简单选择排序(不稳定)与堆排序(不稳定)

这两种算法都属于选择排序。(从待排序的元素中挑选出最大或最小值,下面的例子以最小值为例)

简单选择排序由于选出最小值后需要交换位置,位置一变就会变得不稳定.例如8  3  8  1.当从左往右遍历找最小值时,找到了1,这就需要把8跟1交换.这样两个相等元素8的位置就变了,所以选择排序不是一个稳定的排序算法。

代码案例:

/**    
 * 选择排序
 * @author: Jack_Wu    
 * @date: 2019年6月21日     
 */
public class SelectionSort {
	    public static void main(String[] args) {
	        int[] arr={1,3,2,45,65,33,12};
	        System.out.println("交换之前:");
	        for(int num:arr){
	            System.out.print(num+" ");
	        }        
	        //选择排序的优化
	        for(int i = 0; i < arr.length - 1; i++) {// 做第i趟排序
	            int k = i;
	            for(int j = k + 1; j < arr.length; j++){// 选最小的记录
	                if(arr[j] < arr[k]){ 
	                    k = j; //记下目前找到的最小值所在的位置
	                }
	            }
	            //在内层循环结束,也就是找到本轮循环的最小的数以后,再进行交换
	            if(i != k){  //交换a[i]和a[k]
	                int temp = arr[i];
	                arr[i] = arr[k];
	                arr[k] = temp;
	            }    
	        }
	        System.out.println();
	        System.out.println("交换后:");
	        for(int num:arr){
	            System.out.print(num+" ");
	        }
	    }

}

运行结果如图:

堆排序的话,也会存在跟上面一样的交换最大值的位置会导致不稳定.例如有大堆 8 8 6 5 2.先选出第一个最大值8,放最末尾.此时就不稳定了.因为第二个8就跑它前面去了。所以,堆排序不是稳定的排序算法。

(3)插入排序(稳定)

直接插入时是先在已排序好的的子序列中找到合适的位置再插入。假设左边是已排序的,右边是没排序的.通过从后向前遍历已排序序列,然后插入,此时相等元素依然可以保持原有位置.但是如果你从前向后遍历已排序序列就会是不稳定排序了。

二分插入排序是不稳定的,因为通过二分查找时得到的位置不稳定.例如3 4 4 5 4,但把最后一个4插入时肯定会跑到第二个4前面去了.所以是不稳定的。

代码案例:

/**    
 * @author: Jack_Wu    
 * @date: 2019年6月21日    
 */
public class InsertionSort {

	public static void insertSort(int[] arr) {
        // 空数组 或 只有一个元素的数组,则什么都不做。
        if (arr == null || arr.length == 0) return;
 
        // 外层循环的 i 代表有序区的最后一个元素。
        for (int i = 0; i < arr.length - 1; i++) {
            // 待插入的元素暂存到value.
            int value = arr[i + 1];
            int j = i;
            // j < 0 时退出循环,说明 j 是最小的元素的索引值。
            // 或者 arr[j] <= value 时退出循环,说明 j 是比value小的元素的索引值。
            for (; j >= 0 && arr[j] > value; j--) {
                // 把元素往后挪。
                arr[j + 1] = arr[j];
            }
            // 把待插入元素,放到正确位置。
            arr[j + 1] = value;
 
            // 把每一趟排序的结果也输出一下。
            System.out.print("第 "+ (i+1) + " 趟: ");
            print(arr);
        }
    }
 
    public static void main(String[] args) {
        int[] arr = {6, 9, 1, 4, 5, 8, 7, 0, 2, 3};
 
        System.out.print("排序前:  ");
        print(arr);
 
        insertSort(arr);
 
        System.out.print("排序后:  ");
        print(arr);
    }
    // 打印数组
    public static void print(int[] arr) {
        if (arr == null)    return;
 
        for(int i : arr) {
            System.out.print(i + " ");
        }
        System.out.println();
    }
}

运行结果:

(4)归并排序(稳定)

归并排序使得了递归的思想,把序列递归的分割成小序列,然后合并排好序的子序列.当有序列的左右两子序列合并的时候一般是先遍历左序列,所在左右序列如果有相等元素,则处在左边的仍然在前,这就稳定了。但是如果你非得先遍历右边序列则算法变成不稳定的了,虽然这样排出来的序也是对的,但变成了不稳定的,所以是不太好的实现。

在这就不去详细介绍了,如果有兴趣的可以看这边文章:https://www.cnblogs.com/wyongbo/p/Devide.html

(5)希尔排序(不稳定)

希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell是一种不稳定排序算法

代码案例:

/**    
 * 希尔排序
 * @author: Jack_Wu    
 * @date: 2019年6月21日     
 */
public class Shell {

	public static void main(String[] args)
	{
		int[] ins = {2,3,5,1,23,6,78,34,23,4,5,78,34,65,32,65,76,32,76,1,9};
		int[] ins2 = sort(ins);
		for(int in: ins2){
			System.out.println(in);
		}
	}
	public static int[] sort(int[] ins){
		
		int n = ins.length;
		int gap = n/2;
		while(gap > 0){
			for(int j = gap; j < n; j++){
				int i=j;
				while(i >= gap && ins[i-gap] > ins[i]){
					int temp = ins[i-gap]+ins[i];
					ins[i-gap] = temp-ins[i-gap];
					ins[i] = temp-ins[i-gap];
					i -= gap;
				}
			}
			gap = gap/2;
		}
		return ins;
	}
}

(5)基数排序(稳定)

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。

代码案例:

/**    
 * 基数排序
 * @author: Jack_Wu    
 * @date: 2019年6月21日    
 */
public class RadixSort {
	 public static void main(String[] args) {
	        int[] data = {51, 944, 1, 9, 57, 366, 79, 6, 1, 345};// 待排序数组
	        sort(data, 3);
	        print(data);
	    }

	    /**
	     * 从小到大排序
	     * @param data 待排序数组
	     * @param d 表示最大的数有多少位
	     */
	    public static void sort(int[] data, int d) {
	        int n = 1;
	        // 数组的第一维表示可能的余数0-9
	        int[][] bask = new int[10][data.length];
	        // 数组index[i]用来表示该位(个、十、百.......)是 i 的数的个数
	        int[] index = new int[10];
	        // i:控制键值排序依据在哪一位(个、十、百.......)
	        for (int i = 0; i < d; i++) {
	            for (int j = 0; j < data.length; j++) {
	                int lsd = ((data[j] / n) % 10);
	                bask[lsd][index[lsd]++] = data[j];
	            }
	            int pos = 0;
	            for (int j = 0; j < 10; j++) {
	                for (int k = 0; k < index[j]; k++) {
	                    data[pos++] = bask[j][k];
	                }
	                index[j] = 0;
	            }
	            n *= 10;
	        }
	    }

	    public static void print(int array[]) {
	        for (int j = 0; j < array.length; j++) {
	            System.out.printf("%5d", array[j]);
	        }
	        System.out.println();
	    }
}

下面这张图解释了稳定和不稳定的排序是如何工作的:

       这就是稳定和不稳定排序算法之间的区别。请记住,如果在排序的输出中保持相等键或数字的原始顺序,则该算法称为排序算法。稳定排序算法的一些常见示例是合并排序,插入排序和冒泡排序。

最后从算法应用角度谈谈自己看法, 例如我们某项目应用场景的数据结构比较复杂, 有数据量较小的数据集合, 也有大的数据库或大数据, 这时可能在小数据和大数据的排序算法上都用的冒泡排序, 那么在数据量较小时冒泡感觉较快,数据量大时冒泡就较慢, 而快速排序算法可能正好相反,大数据量是快速排序确实快,小数据量时这种类似二分法理念的算法反而慢了。这时候对开发人员来说,就会产生主观臆断, 认为某某算法慢,或者不稳定。

解决办法就是在不同数据需要排序的应用场景用最优的排序算法, 灵活配置, 这样会快,显得稳定。

其实真正意义上的不稳定,可能是算法写错了,或者写了个烂算法。

综上,得出结论

                           选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法;

                           冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。

 

  • 34
    点赞
  • 123
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值