数据结构与算法——插入排序

本专栏是学习王争老师的《数据结构与算法之美》的学习总结,详细内容可以去学习王争老师的专栏,希望大家都能够有所收获。同时也欢迎大家能够与我一起交流探讨!

插入排序(Insertion Sort)

1、插入排序介绍

一个有序数组,往里添加一个新数据,只需要遍历数组,找到数据应该插入的位置将其插入即可。这是一个动态排序的过程,即动态地往有序集合中添加数据,通过这种方法可以保持集合中的数据一直有序。
而对于一组静态数据,也可以借鉴这种插入方法来进行排序,即插入排序的思想。

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

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

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

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

为什么说移动次数就等于逆序度呢?如上图,满有序度是 n*(n-1)/2=15,初始序列的有序度是 5,所以逆序度是 10。插入排序中,数据移动的个数总和也等于 10=3+3+4。
在这里插入图片描述
插入排序的代码实现如下图所示:

public class InsertionSort {
    // 插入排序,arr表示数组,n表示数组大小
    public void insertionSort(int[] arr, int n) {
        if (n <= 1) return;

        // 从第二个元素开始,查找插入的位置
        for (int i = 1; i < n; i++) {
            int value = arr[i];
            int j = i - 1;
            // 查找插入的位置
            for (; j >= 0; j--) {
                if (arr[j] > value) {
                    arr[j + 1] = arr[j]; // 数据移动
                } else {
                    break;
                }
            }

            arr[j + 1] = value; // 插入数据
        }
    }
}
2、插入排序是原地排序算法吗?

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

3、插入排序是稳定的排序算法吗?

在插入排序中,对于值相同的元素,可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法

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

若要排序的数组已有序,则不需要搬移任何数据。如果从尾到头在有序区间里面查找插入位置,每次只需要比较一个数据就能确定插入的位置。所以这种情况下,最好是时间复杂度为 O(n)。注意,这里是从尾到头遍历已经有序的数据,即从尾到头遍历有序区间。

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

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

数组插入的平均时间复杂度分析:先考虑概率,每个位置插入元素的概率都为1/n。
插入数组的第一位,需要将剩余元素向右移动n个位置,插入数组的第二位,需要将剩余元素向右移动n-1个位置,以此类推,由此得到:( n + n-1 + n-2 + … + 1 + 0 ) * 1/n = (n+1)/2,去掉系数则复杂度为O(n)。

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

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

首先,冒泡排序不管怎么优化,元素交换的次数是一个固定值,是原始数据的逆序度。插入排序是同样的,不管怎么优化,元素移动的次数也等于原始数据的逆序度

但从代码实现上,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 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;
}

如果将一个赋值语句的时间粗略地计为单位时间(unit_time),分别用冒泡排序与插入排序对同一个逆序度是K的数组进行排序。

冒泡排序需要 K 次交换操作,每次需要 3 个赋值语句,所以交换操作总耗时是 3*K 单位时间。而插入排序中数据移动操作只需要 K 个时间单位。

针对冒泡排序和插入排序的 Java 代码,随机生成 10000 个数组,每个数组中包含 200 个数据,然后在机器上分别用冒泡和插入排序算法来排序,冒泡排序算法大约 700ms 才能执行完成,而插入排序只需要 100ms 左右就能搞定!

虽然冒泡排序和插入排序在时间复杂度上是一样的,都是 O(n2),但是如果我们希望把性能优化做到极致,那肯定首选插入排序。如果对插入排序的优化感兴趣吗,可以学习希尔排序。

总结

1、将数组中的数据分为已排序区间未排序区间,插入排序的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程直到未排序区间中元素为空,算法结束。

2、插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),即插入排序是一个原地排序算法。

3、在插入排序中,对于值相同的元素,可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法。

4、当要排序的数组数据已有序,并不需要搬移任何数据。从尾到头遍历有序区间(此时整个数组都已经是有序区间)查找插入位置时,每次只需要比较一个数据就能够确定插入位置。这种情况下最好的时间复杂度为O(n)。

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值