[Alg]排序算法之插入排序

[Alg]排序算法之插入排序


作者:屎壳郎 miaosg01@163.com

日期:July 2021

版次:初版


简介: 插入排序是排序算法大家族中的一个分支,其原理简单易懂,操作类似于玩扑克,假设你手中的牌已经按你的个人爱好排好序,新抓的一张牌从左到右或从右到左与手中的牌依次比较,找到应该插入的位置,然后插入即可。根据处理的数据结构及操作的不同,又细分为:直接插入排序、shell排序、list排序、地址计算排序等。

1. 直接插入排序

直接插入排序简单明了,不作过多说明,直接列出算法。

算法S:(直接插入排序)

  • S1[遍历] j = 2 , 3 , … , N j=2,3,\ldots,N j=2,3,,N,执行S2到S5,然后结束。
  • S2[设置 i , K , R i,K,R i,K,R.] 置 i ← j − 1 i\gets j-1 ij1 K ← K j K\gets K_j KKj R ← R j R\gets R_j RRj
  • S3[比较 K : K i K:K_i K:Ki.] 如果 K ≥ K i K\geq K_i KKi, 转到S5
  • S4[移动 R i R_i Ri i i i减1] 置 R i + 1 ← R i R_{i+1}\gets R_i Ri+1Ri i ← i − 1 i\gets i-1 ii1。如果 i > 0 i>0 i>0,返回S3。
  • S5[插入] 置 R i + 1 ← R R_{i+1}\gets R Ri+1R

从这个算法的内外两个循环嵌套,我们就大约猜出其时间复杂度 O ( N 2 ) O(N^2) O(N2),所以它的实际用处不大(是不是很像冒泡排序,地位很尴尬,不讲吧,里面有很多理论知识要说明,讲吧,没甚鸟用!)。但以它为例子来讲讲如何提高精炼一个算法,还是有好多东西可以阐述的。

下面,我们就以直接插入排序算法S为例,找到存在的问题,穷尽我们的能力,对算法S精益求精,看看我们能达到什么高度!

算法S改进1:消除边界条件检查

在直接插入排序算法S执行过程中,第 j j j项大约要进行 1 2 j {1\over2}j 21j次比较和移动操作,整体上要进行 ( 1 + 2 + ⋯ + N ) / 2 ≈ N 2 / 4 (1+2+\cdots+N)/2\approx N^2/4 (1+2++N)/2N2/4次比较和移动操作。在S4步骤中,平均移动数据 1 4 N 2 {1\over4}N^2 41N2,每次移动操作后都要执行边界条件检查 i > 0 i>0 i>0 N 2 / 4 N^2/4 N2/4次的边界条件检查是很让人脑壳疼,何况还要在编程的时候也小心翼翼的去处理边界条件(一不小心就掉坑里),我们如何彻底消灭它?下面提供两种思路去彻底摒弃边界条件检查。

  • A:设待排序 ( R 1 , R 2 , … , R N ) (R_1,R_2,\ldots,R_N) (R1,R2,,RN),我们对这个排列进行改造,在最前面( R 1 R_1 R1之前)插入 R 0 = − ∞ R_0=-\infty R0=。这样处理后就可以取消了边界条件检查,因为在 K ≥ K i K\geq K_i KKi的比较中,无论如何都不会越过 − ∞ -\infty
  • B:给你排序数据时,不是每次都有机会让你在其前面插值处理,那另一种思路是,假设我们要把 R j R_j Rj插入到前面已经排好序的序列 R 1 , R 2 , … , R j − 1 R_1,R_2,\ldots,R_{j-1} R1,R2,,Rj1中,我们先处理边界,首先和 R 1 R_1 R1进行比较,如果 R j < R 1 R_j<R_1 Rj<R1,这说明 R j R_j Rj应该插入 R 1 R_1 R1左面, R 1 … R j − 1 R_1\ldots R_{j-1} R1Rj1整体右移一位,把 R j R_j Rj插入到 R 1 R_1 R1的位置,否则 R 1 ≤ R j R_1\leq R_j R1Rj R j R_j Rj肯定插入到 R 1 R_1 R1右侧,根本无需边界条件检查。其解决问题的本质是增加了代码量,以空间换时间!

算法S改进2:改进对比—— 二分查找

直接插入排序要进行大约 N 2 / 4 N^2/4 N2/4次的比较操作,我们首先搞明白——进行如此大量的对比操作的目的是什么?

在找东西!

找什么呢?

R j R_j Rj插入位置!

既然明白了事情的本质,那就好办了。查找算法中比 O ( N 2 ) O(N^2) O(N2)优秀的一抓一大把,高端大气上档次的有之,低调奢华有内涵的有之。我们就选低调奢华有内涵的二分查找,就可以把对比操作由 O ( N 2 ) O(N^2) O(N2)提高到 N lg ⁡ ( N ) N\lg(N) Nlg(N)。OK,又提高了一点。

算法S改进3:改进数据移动—— 两路插入

现在我们来考虑数据移动的问题,每次仅移动一位,平均要移动 1 4 N 2 {1\over4}N^2 41N2次,我们借鉴上面二分查找的思路,第一项放置在中间,然后向两边移动插入——是为两路插入,这样就可减少一半的数据移动量,约 1 8 N 2 {1\over8}N^2 81N2。这会引出另一个麻烦,假设你踩的狗屎型号不对,没走狗屎运,第一个为最小项插入了中间,然后所有后续项都要放置在右侧,反之,第一个为最大项,所有后续项插入左侧。为了能够容纳这两种极端情况,需要 2 N + 1 2N+1 2N+1的空间来应对。对于喝酸奶都要舔瓶盖的我们来说,这怎么能够容忍!我们先找找问题的根源,如果我们中间插入的为最小值,那右侧尾部空间不够,且不能扩展,而此时左侧一半空间空闲;如果中间插入最大值,头尾出现相反的问题。既然是头尾问题,那无头无尾可好?

OK,用环形数据结构,无头无尾,满足要求。

构建环形数据结构

其实构建环形数据结构并不复杂,我们申请一个数组,指针F指向数组的头部,R指向尾部。假设我们插入的第一个值为P,比P大的值插入,是以F为基点向右扩展;当插入的值小于P时,以R为基准向左扩展。通过改变数据结构,我们只需N+1的空间就可满足两路插入。见图:在这里插入图片描述

数组前后两部分交换(如何旋转一个数组)

应用环形数据结构带来另一个问题,它产生形如 ( 4   5   1   2   3 ) (4\,5\,1\,2\,3) (45123)的数组,我们还要进一步处理,交换前后两部分得到形如 ( 1   2   3   4   5 ) (1\,2\,3\,4\,5) (12345)我们需要的形式。下面介绍一个解决这个问题的算法:设 m = 2 m=2 m=2为前部分长度 ( 4   5 ) (4\,5) (45) n = 3 n=3 n=3为后部分长度,我们的目的是交换这前后两部分。首先设定一个变量指定交换位置 k ← 0 k\gets0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值