• 排序:为什么插入排序比冒泡排序更受欢迎?


    文章来源于极客时间前google工程师−王争专栏。

    需掌握的的排序:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。按照时间复杂度可以分为三类:

    image

    问题:插入排序和冒泡排序的时间复杂度相同,都是O(n^2),在实际的软件开发中,为什么我们更倾向于使用插入排序算法而不是冒泡排序算法呢

    如何分析一个“排序算法”?

    算法的执行效率

    1.最好情况、最坏情况、平均情况时间复杂度

    对于要排序的数据,有的接近有序,有的完全无序。有序度不同的数据,对于排序的执行时间肯定是有影响的,我们要知道排序算法在不同数据下的性能表现。

    2.时间复杂度的系数、常数、低阶

    时间复杂度反映的是数据规模n很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。

    但是在实际的软件开发中,我们排序的可能是10,100,1000这样数据规模很小的数据,所以在时间复杂度相同的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。

    3.比较次数和交换(或移动)次数

    基于比较的排序算法会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。所以分析排序算法的执行效率,应该把比较次数和交换(或移动)次数也考虑进去。

    排序算法的内存消耗

    算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。

    针对排序算法的空间复杂度,还引入了一个新的概念,原地排序。原地排序算法,就是特指空间复杂度是O(1)的排序算法。

    排序算法的稳定性

    用执行效率和内存消耗来衡量排序算法的好坏是不够的。针对排序算法,还有一个重要的指标,稳定性

    这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。

    比如有一组数据2,9,3,4,8,3排序之后就是2,3,34,8,9。

    这组数据里有两个3,如果经过排序算法之后,两个3的前后顺序不变,那我们就把这种排序算法叫作稳定排序算法;如果前后顺序发生变化,对应的算法就叫作不稳定的排序算法

    真正的软件开发中,我们要排序的往往不是单纯的整数,而是一组对象,我们需要按照对象的某个key来排序。

    需求:现在要给电商交易系统中的“订单”排序。订单中有两个属性,一个是下单时间,另一个是订单金额。如果现在有10万条订单数据,我们希望按照金额从小到大排序,对于金额相同的订单,我们希望按照下单时间从早到晚有序。

    最先想到的方法是:先按照金额排序,然后再遍历排序之后的订单数据,对于每个金额相同的小区间再按照下单时间排序。这种排序思路,理解简单,实现复杂。

    借助稳定排序算法:先按照下单时间对订单数据进行排序,然后
    用稳定排序算法,按照订单金额重新排序。两遍排序,实现需求。

    稳定排序算法可以保持金额相同的两个对象,在排序之后的前后顺序保持不变

    image

    冒泡排序(Bubble Sort)

    冒泡排序只会操作相邻的两个数据。一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序。

    对一组数据4,5,6,3,2,1,从小到大排序,第一次冒泡的详细过程如下图所示:
    image

    经过一次冒泡操作之后,6这个元素已经存储在正确的位置上。要想完成所有数据的排序,我们只需要进行6次这样的冒泡操作就行了。

    image

    优化:当某次冒泡操作已经没有数据交换时,说明已经达到了完全有序,不用再继续执行后续的冒泡操作。如下图所示,这里给6个元素排序,只需要4次冒泡操作就可以了。

    image

    代码如下:

    public void bubbleSort(int[] array){
        int len = array.length;
        if(len < = 1) return;
        for(int i=0;i array[j+1]){
                    //表示有数据交换
                    flag = true;
                    int temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                }
            }
            if(flag){//没有数据交换提前退出
                break;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这里有三个问题:

    第一,冒泡排序是原地排序算法吗?

    冒泡过程中只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为O(1),是一个原地排序算法。

    第二,冒泡排序是稳定的排序算法吗

    只有交换才可以改变两个元素的前后顺序。当有相邻的两个元素大小相等的时候,我们不做交换,所以冒泡排序是稳定的排序算法。

    第三,冒泡排序的时间复杂度是多少

    最好情况下,要排序的数据已经是有序的,只需要进行一次冒泡操作,就可以,所以最好情况复杂度是O(n)。最坏情况,需要进行n次冒泡操作,所以最坏情况时间复杂度是O(n^2)。

    image

    用概率论的方法定量分析平均时间复杂度,涉及的数学推理和计算比较复杂。可以通过“有序度”和“逆序度”这两个概念来进行分析。

    有序度是数组中具有有序关系的元素对的个数。数学表达式如下:

    有序元素对:a[i] <= a[j], 如果 i < j。

    image

    对于一个倒叙排列的数组,比如6,5,4,3,2,1,有序度是0;1,2,3,4,5,6,有序度就是n*(n-1)/2,也就是15.我们将这种完全有序的数组的有序度叫作满有序度

    逆序度的定义正好跟有序度相反。

    逆序元素对:a[i] > a[j], 如果 i < j。

    根据这三个概念,我们还可以得出一个公式:逆序度 = 满有序度 - 有序度

    排序的过程就是一种增加有序度,减少逆序度的过程,最后达到满有序度,就说明排序完成了。

    对于包含n个数据的数组进行冒泡排序,平均交换次数是多少?最坏情况下,初始状态的有序度为0,所以要进行n*(n-1)/2次交换。最好情况下,初始状态的有序度是n*(n-1)/2,就不需要进行交换。我们可以取个中间值n*(n-1)/4,来表示初始有序度既不是很高也不是很低的平均情况。所以平均情况下的时间复杂度就是O(n^2)。

    插入排序(Insertion Sort)

    先看一个问题。一个有序数组,我们往里面添加一个新的数据后,如何继续保持数据有序呢?很简单,只要遍历数组,找到数据应该插入的位置讲其插入即可。
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
    这是一个动态排序的过程,即动态地往有序集合中添加数据,我们可以通过这种方法保持集中的数据一直有序。而对于一组静态数据,我们也可以借鉴上面讲的插入方法,来进行排序,于是就有了插入排序算法

    插入排序具体是如何借助上面的思想来实现排序的呢

    首先,我们将数组中的数据分为两个区间,已排序区间未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置讲其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中的元素为空,算法结束。

    如图所示,要排序的数据是4,5,6,1,3,2,其中左侧为已排序区间,右侧是未排序区间。
    image
    插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。当我们需要将一个数据a插入到已排序区间时,需要拿a与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素a插入。

    对于不同的查找插入点方法(从头到尾、从尾到头),元素的比较次数是有区别的。但对于一个给定的初始序列,移动操作的次数总是固定的,就等于逆序度。

    如下图所示。满有序度是n*(n-1)/2=15,初始有序度是5,所以逆序度是10。插入排序中,数据移动的个数总和也等于3+3+4=10。
    image
    代码实现如下:

    public void InsertSort(int[] array){
        int len = array.length;
        if(len <= 1){return;}
        //i默认为1,分为两个区间,开始遍历无序区间
        for(int i=1;i=0;--j){
               if(array[j]>value){
                   //数据移动
                   array[j+1] = array[j];
               }else{
                   //小于无序区间元素跳出循环,记录j
                   break;
               }
           }
           //将数据插入有序区间的正确位置
           array[j+1] = value;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    自行实现插入降序算法

    第一,插入排序是原地算法吗?

    从实现过程可以看出,插入排序的算法运行不需要额外的存储空间,所以空间复杂度为O(1),也就是说,这是一个原地排序算法。

    第二,插入排序是稳定的排序算法吗?

    排序之后,相同元素前后顺序不变,所以插入排序是稳定的排序算法。

    插入排序的时间复杂度是多少?

    最好时间复杂度是O(n),只需要从尾到头遍历已经有序的数据

    如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为O(n^2)。

    在数组中插入一个数据的平均时间复杂度是O(n)。所以,对于插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环执行n次插入操作,所以平均时间复杂度是O(n^2)。

    选择排序(Selection Sort)

    选择排序算法的实现思路类似插入排序,也分排序区间和未排序区间。但是选择排序每次都会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
    image

    选择排序空间复杂度为O(1),是一种原地排序算法。选择排序最好、最坏和平均情况时间复杂度都是O(n^2)。

    选择排序是不稳定的排序算法。选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,破坏了稳定性。

    比如5,8,5,2,9这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素2,与第一个5交换位置,那第一个5和中间的5顺序就变了,所以就不稳定了。相对于冒泡排序和插入排序,选择排序就稍微逊色了。

    解答开篇

    冒泡排序和插入排序的时间复杂度都是O(n^2),都是原地排序算法,为什么插入排序要比冒泡排序更受欢迎呢?

    不管如何优化,元素交换的次数是一个固定值,是原始数据的逆序度

    从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要3个赋值操作,而插入排序只需要1个。

    冒泡排序中数据的交换操作:
    if (a[j] > a[j+1]) { // 交换
       int tmp = a[j];
       a[j] = a[j+1];
       a[j+1] = tmp;
       flag = true;
    }
    
    插入排序中数据的移动操作:
    if (a[j] > value) {
      a[j+1] = a[j];  // 数据移动
    } else {
      break;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    将执行一个赋值语句的时间粗略地记为单位时间(unit_time),对一个逆序度为K的数组进行排序。冒泡需要K次交换操作,交换操作耗时3*K单位时间。插入排序中数据移动操作只需要K个单位时间。

    随机生成10000个数组,每个数组中包含200个数据,冒泡排序需要584ms完成,插入排序只需要105ms就能搞定!

    //数组工厂
    public class ArrayFactory {
    	private static Random rand = new Random();
    	//获得10000个包含200个随机数的数组
    	public static int[][] get2Array(){
    		int[][] array = new int[10000][200];
    		for(int i=0;ia[j+1]){
    					//交换
    					int temp = a[j+1];
    					a[j+1] = a[j];
    					a[j] = temp;
    					bol = false;
    				}
    			}
    			if(bol){
    				break;
    			}
    		}
    	}
    	//插入排序
    	public static void InsertSort(int[] a){
    		if(a == null) return;
    		int len = a.length;
    		if(len <= 1) return;
    		//用下标1分为两个区间左边为有序区间,右边为无序区间
    		for(int i=1;i=0;--j){
    				//无序区间每一个数与value比较
    				if(a[j]>value){
    					//移动
    					a[j+1] = a[j];
    				}else{
    					break;
    				}
    			}
    			//插入数据
    			a[j+1] = value;
    		}
    	}
    	//打印数组
    	public static void printArray(int[] array){
    		int len = array.length;
    		for(int i=0;i
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102

    插入排序性能优化-希尔排序

    评价一个排序算法,需要从执行效率、内存消耗和稳定性三个方面看。重点掌握分析方法。
    image

    这三种排序算法,实现代码简单,对于小规模数据的排序,用起来非常高效。但是在大规模数据排序中,这个时间复杂度还是有点高,所以我们更倾向于时间复杂度为O(nlogn)的排序算法。

    思考

    特定的算法是依赖特定的数据结构的。都是基于数组实现,如果数据存储在链表中,这三种排序算法还能工作吗?相应的时间、空间复杂度是多少?

  • 相关阅读:
    Acwing 802. 区间和
    FOFAHUB使用测试
    内省机制(操作javaBean的信息)
    NFT 推荐|史蒂夫·青木 NFT 作品集
    2023最新SSM计算机毕业设计选题大全(附源码+LW)之java高校心理健康咨询平台vknhv
    小程序源码:王者荣耀吃鸡气泡等等头像框DIY在线生成N种风格-多玩法安装简单
    【强化学习】gymnasium自定义环境并封装学习笔记
    Docker 安装 MongoDB
    游戏思考15:全区全服和分区分服的思考
    flink cdc笔记(一):flink cdc简介
  • 原文地址:https://blog.csdn.net/wozaibohaibian/article/details/133818807