详解排序算法(一)之3种插入排序(直接插入、折半插入、希尔)


直接插入排序

打过牌的人都知道,当我们拿到一张新牌时,因为之前的牌已经经过排序,因此,我们只需将当前这张牌插入到合适的位置即可。而直接插入排序,正是秉承这一思想,将待插入元素与之前元素一一比较,从而找到合适的插入位置。

那么使用直接插入排序,具体是怎样操作的呢?我们取 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 来进行示范。

(1)第1轮排序,3之前无可比较值,因此我们从44开始操作,取44和3比较,大于3,顺序保持不变。得数据

? 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48

(2)第2轮排序,取38和44比较,38 < 44,再将38与3比较,38 > 3,故将38放于第2位,得数据

? 3, 38, 44, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48

(3)第3轮排序,取5与44比较,5 < 44,再将5与38比较,5 < 38,再将5与3比较,5 > 3, 置于第2位,得数据

? 3, 5, 38, 44, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48

(4)如此经过14轮排序后,得到最终结果
2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50

动态图

insertionSort

javascript实现

function directInsertSort (arr) {
    let compare,  // 对比元素下标
        current   // 待插入元素值

    for (let i = 1; i < arr.length; i++) {
        current = arr[i]
        compare = i - 1

        while (current < arr[compare] && compare >= 0) {
            arr[compare + 1] = arr[compare]
            compare--
        }

        arr[compare + 1] = current
    }

    return arr
}

折半插入排序

细心的同学可能已经注意到,当我们要将一个元素插入合适的位置时,其之前的元素是有序的,因此,我们可以用折半查找的方式来比对并插入元素,也就是所谓的折半插入排序。

打个简单的比方,有 3, 7, 9, 10, 13, 14, 15 七个有序数,我们现在要插入4,那么4先找到中间那个数10,一对比,好家伙,我比你小,所以我在你左侧继续找插入位置,左侧有三个元素3, 7, 9,与中间的7一对比,巧了,还是小,于是在7左侧继续找位置,7左侧只有一个元素3,再一比较,终于做了回大哥,最终找到最终归宿,即3的右侧,7的左侧,也就是第2位。

那么使用直接插入排序,具体是怎样操作的呢?我们仍然取 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 来进行示范。

为了表示方便,我们取对比元素范围的最小元素下标为 low,最大元素下标为 high,居中元素下标为 mid

(1)第1轮排序,3之前无可比较值,因此我们从44开始操作,其下标为 1,故

? low = 0high = 1 - 1 = 0mid = 0,mid对应的元素值为3,取44 > 3,应插入3右侧,即保持不变。得

? 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48

(2)第2轮排序,取38,其下标为 2,故

? low = 0high = 2 - 1 = 1mid = (0 + 1)/ 2 = 0 ,mid对应的元素值为3, 38 > 3,应插入3右侧,修改对比范围得

? low = mid + 1 = 1high = 1mid = 1,mid对应的元素值为44, 38 < 44,故应插入44左侧,得

? 3, 38, 44, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48

(3)第3轮排序,取5,其下标为 3,故

? low = 0high = 3 - 1 = 2mid = (0 + 2)/ 2 = 1 ,mid对应的元素值为38, 5 < 38,应插入38左侧,修改对比范围得

? low = 0high = mid - 1 = 0mid = 0,mid对应的元素值为3,5 > 3,应插入3右侧,得

? 3, 5, 38, 44, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48

(4)如此经过14轮排序后,得到最终结果
2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50

javascript实现

function BInsertSort(arr) {
    let low = 0, high = 0, mid
    let current = 0 // 待插入元素值

    for (let i = 1; i < arr.length; i++) {
        low = 0
        high = i - 1
        current = arr[i]

        // 采用折半查找法判断插入位置,最终变量 low 表示插入位置
        while (low <= high) {
            mid = Math.floor((low + high) / 2)

            if (arr[mid] > current) {
                high = mid - 1
            } else {
                low = mid + 1
            }
        }

        // 有序表中插入位置后的元素统一后移
        for (let j = i; j > low; j--) {
            arr[j] = arr[j - 1]
        }

        arr[low] = current // 插入元素
    }

    return arr
}

希尔排序 

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。

使用希尔排序,首先要选一组递减的整数作为增量序列。最小的增量必须为1,即??M>?????1>...>??1=1。

需要注意的是,增量序列的选取会影响希尔排序的速度,原始希尔排序的取法为:????=???/2?,????=?????+1/2?,???/2?表示小于等于??/2的最大整数。如 N=4,???/2?=2,N=7,???/2?=3。

我们取 7, 6, 9, 3, 1, 5, 2, 4 来进行具体操作

(1)第1轮排序,取初始增量 gap = ?length / 2? = 4,将元素按照步长为4,即元素之间相距4位分为4组

  • 7, 1
  • 6, 5
  • 9, 2
  • 3, 4

进行组内排序

  • 1, 7
  • 5, 6
  • 2, 9
  • 3, 4

1, 5, 2, 3, 7, 6, 9, 4

img

(2)第2轮排序,取初始增量 gap = ?gap/ 2? = 2,将元素按照步长为2,即元素之间相距2位分为2组

  • 1, 2, 7, 9
  • 5, 3, 6, 4

进行组内排序

  • 1, 2, 7, 9
  • 3, 4, 5, 6

1, 3, 2, 4, 7, 5, 9, 6

img

(3)第3轮排序,取初始增量 gap = ?gap/ 2? = 1,将元素按照步长为1,即元素之间相距1位分为1组

  • 1, 3, 2, 4, 7, 5, 9, 6

经过简单调整得最终排序结果 1, 2, 3, 4, 5, 6, 7, 9

img

javascript实现

function shellSort(arr) {
    const len = arr.length
    let temp,
        gap = Math.floor(len / 2) // 增量

    for (gap; gap > 0; gap = Math.floor(gap / 2)) {
        for (let i = gap; i < len; i++) {
            temp = arr[i]
            let j

            // 组内使用直接插入排序
            for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
                arr[j + gap] = arr[j]
            }

            arr[j + gap] = temp
        }
    }

    return arr
}

三种算法的复杂度及稳定性

排序算法 时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 空间复杂度 稳定性
直接插入排序 O(n2) O(n2) O(n) O(1) 稳定
折半插入排序 O(n2) O(n2) O(n) O(1) 稳定
希尔排序 O(n1.3) O(n2) O(n) O(1) 不稳定

相关概念

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。

不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。

时间复杂度:
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
上面这一段解释是很规范的,但是对于非专业性的我们来说并不是那么好理解,说白了时间复杂度就是时间复杂度的计算并不是计算程序具体运行的时间,而是算法执行语句的次数。通常我们计算时间复杂度都是计算最坏情况 。

最坏时间复杂度:
最坏情况下的时间复杂度称最坏时间复杂度。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。
这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。

平均时间复杂度:
所有可能的输入实例均以等概率出现的情况下,算法的期望运行时间。设每种情况的出现的概率为pi,平均时间复杂度则为sum(pi*f(n))

空间复杂度:
一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。  
(1)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。
一个算法所需的存储空间用f(n)表示。S(n)=O(f(n))  其中n为问题的规模,S(n)表示空间复杂度。