插入排序(Kotlin)

目标:将一个数组按照从低到高(或者从高到低)的顺序排序。
插入排序就是给定一个包含数字的数组,需要将它们放到正确的顺序。插入排序算法按照以下步骤运行:

  • 将所有的数字放到一个堆。这个堆中的数据是未排序的。
  • 从堆中取一个数字。具体取哪一个数字并不重要,但是最简单的方式是取堆顶部的那一个数字。
  • 将这个数字插入到新的数组中。
  • 从未排序的堆中取下一个数字同样插入到新的数组。它会被插入到第一个数字的左边或者右边,现在这两个数字是有序的。
  • 继续取下一个数字并将它插入到新的数组合适的位置。
  • 如此循环直到堆中的数字都取完。最终留下一个空的堆和一个排序好的数组。

这就是为什么这个算法被称为“插入”排序。因为我们是从一个堆中取出数字并将它插入到已排序数组中合适的位置。

示例

假设现在需要排序的数字是 [ 8, 3, 5, 4, 6 ]。这就是我们的未排序的堆。

取出第一个数字,8,并插入一个新的数组。当前这个数组还是空的,所以很简单。那么现在已排序的数组是 [ 8 ],堆中剩下的数字是 [ 3, 5, 4, 6 ]

从堆中取下一个数字 3, 并插入已排序的数组。它应该在 8 的前面,所以现在已排序数组变成了 [ 3, 8 ],堆中还剩下 [ 5, 4, 6 ]

再取下一个数字 5, 并插入已排序数组。它应该在 38 之间,现在已排序数组变成了 [ 3, 5, 8 ], 堆中还有 [ 4, 6]

如此反复直到堆中所有的数据都被插入到新的数组。

就地排序

上面的解释看起来你需要两个数组:一个用于存放未排序的数字,一个用于存放已经排序的数字。

但是你可以就地执行插入操作,而不必创建一个新的数组。你只需要跟踪数组中哪一部分是已经排序的数组,哪一部分是未排序的堆即可。

一开始我们的数组还是 [ 8, 3, 5, 4, 6 ]。用| 竖线标识了已排序数组的终点和未排序堆的起点:

[| 8, 3, 5, 4, 6 ]

这说明已排序的部分还是空的,堆从 8 开始。

第一个数字处理完成后,变成了:

[8 | 3, 5, 4, 6 ]

已排序部分是 [ 8 ],剩下的堆是 [ 3, 5, 4, 6]| 竖线向右移动了一个位置。

整个排序过程中数组的变化过程如下:

[| 8, 3, 5, 4, 6 ]
[ 8 | 3, 5, 4, 6 ]
[ 3, 8 | 5, 4, 6 ]
[ 3, 5, 8 | 4, 6 ]
[ 3, 4, 5, 8 | 6 ]
[ 3, 4, 5, 6, 8 |]

每执行一步, | 竖线就向右移动一个位置。你会发现 | 左侧的数字总是有序的。堆的数字一次减少一个,已排序部分一次增加一个,直到堆中的数字为空,不再有为排序的数字。

怎么插入

每一步从未排序堆的顶部取一个数字出来并插入已排序的数组中,你需要将这个数字插入到合适的位置,以保证竖线左侧的部分是有序的。这个是怎么实现的?

我们来看一看已经执行了几次插入操作的数组,现在是整个数组的内容如下:

[ 3, 5, 8 | 4, 6 ]

我们下一个要排序的数字是 4,我们需要将它插入到已经排序好的 [ 3, 5, 8 ] 的某一个地方。
有一种方式能够实现:查看前一个元素 8

[ 3, 5, 8, 4 | 6 ]
        ^

它比 4 大吗?是,所以它应该在 8 的前面。我们交换这两个元素:

[ 3, 5, 4, 8 | 6 ]
        <-->
       交换后

现在还没有完成,现在它前面的元素是 5,它也比 4 大,我们同样交换这两个元素的位置:

[ 3, 4, 5, 8 | 6 ]
     <-->
    交换后

继续查看前一个元素,34 要小。这意味着我们已经将 4 放入了合适的位置。现在竖线左侧的数字又变成了有序的。

这就是插入排序算法内部循环的描述,具体的代码实现我们在下一节再讨论。它通过交换元素位置来实现将堆顶部的数字插入到数组中已排序的部分。

代码实现

下面是 Kotlin 实现的一个插入排序

class InsertionSort {

    fun insertSort(sourceArray: IntArray): IntArray {
        val resultArray = sourceArray.clone()                          //1
        for (i in 1 until resultArray.size) {                          //2
            var y = i
            while (y > 0 && resultArray[y] < resultArray[y - 1]) {     //3
                swap(resultArray, y - 1, y)
                y--
            }
        }
        return resultArray
    }


    private fun swap(array: IntArray, start: Int, end: Int) {
        val temp = array[start]
        array[start] = array[end]
        array[end] = temp
    }
}

添加一段测试代码来验证一下:

@Test
    fun insertSort() {
        val sorter = InsertionSort()

        val emptyList = intArrayOf()
        assertEquals(emptyList.clone().apply{ sort() }.joinToString(),
                sorter.insertSort(emptyList).joinToString())

        val listWithOneElement = intArrayOf(23)
        assertEquals(listWithOneElement.clone().apply { sort() }.joinToString(),
                sorter.insertSort(listWithOneElement).joinToString())

        val list = intArrayOf(10, -1, 3, 9, 2, 27, 8, 5, 1, 3, 0, 26)
        assertEquals(list.clone().apply { sort() }.joinToString(),
                sorter.insertSort(list).joinToString())
    }

来看一看代码是怎么运行的。

  1. 复制一份 sourceArray,这一点很有必要,因为我们不能直接修改传入的数组的内容。和 Java 标准的 sort() 函数不一样,Java 标准的 sort() 函数是直接修改传入的数组,我们这里实现的排序算法是返回一份排序后的数组,不修改原始内容。
  2. 插入函数中有两个循环。外层的循环依次处理数组中的每一个元素;也就是我们说的从堆的顶部取数据。变量 i 就是已排序数组的结束点的位置,也就是未排序数据的开始位置(|竖线的位置)。记住,在任意时刻,这数组的开始部分--从 index 0 到 index i --总是有序的。剩余部分,从 index i 到数组的最后一个元素是未排序的堆。
  3. 内部循环检查处于位置 x 的元素。这就是处于堆顶部的数字,它有可能比它前面的元素都小。这一个循环向后逐一检查已经排序的数组;每当发现前面的元素比自己大,就交换两者的位置。当内部循环结束时,数组的开始部分就又变成有序的了,并且有序部分会增加一个元素。

注意: 外部循环是从 index 1 而不是 0 开始的。因为将堆中的第一个元素放入到已排序部分不会有任何改变,所以我们直接跳过他。

不使用交换函数

上面这个版本的插入排序可以正常的工作,但是我们可以通过移除对 swap 的调用,从而让它运行的更快一点。前面你已经看到我们通过交换数字的位置将下一个元素放入正确的位置:

[ 3, 5, 8, 4 | 6 ]
        <-->
        swap
        
[ 3, 5, 4, 8 | 6 ]
     <-->
     swap

我们可以不用和前面的元素交换位置,改而直接将这些元素向右移动一个位置,然后将新的数字放入正确的位置。

[ 3, 5, 8, 4 | 6 ]   记住 4
           *

[ 3, 5, 8, 8 | 6 ]   将 8 向右移动
        --->
        
[ 3, 5, 5, 8 | 6 ]   将 5 向右移动
     --->
     
[ 3, 4, 5, 8 | 6 ]   将 4 放入正确的位置
     *

来看看代码实现:

class InsertionSort {

    fun insertSort(sourceArray: IntArray): IntArray {
        val resultArray = sourceArray.clone()
        for (i in 1 until resultArray.size) {
            var y = i
            val insertValue = resultArray[y]
            while (y > 0 && insertValue < resultArray[y - 1]) {
                resultArray[y] = resultArray[y - 1]       //1
                y--
            }

            resultArray[y] = insertValue                  //2
        }

        return resultArray
    }

}

这里 //1 这一行代码将前一个元素向右移动一个位置。最循环的最后,y 就是新的数字在已排序部分的位置,所有在 //2 这一行将这个数字放入对应的位置。

通用的插入排序(泛型)

插入排序应该支持数字以外的其他类型排序。我们可以将参数的类型修改为泛型,并且使用外部传入的比较方法来实现大小比较。只需要在代码中修改两个地方就可以实现。

函数定义变成:

fun <T>insertSort(array: Array<T>, compare: (T, T) -> Boolean): Array<T> 

数组的类型是 T,也就是泛型的占位类型。现在 insertSort()方法可以接收任意类型的数组,不管它包含的是数字、字符串还是其他的数据。

新的参数 compare: (T, T) -> Boolean 是一个函数,它接收两个 T 对象并返回两个对象的大小比较结果,如果第一个参数小于第二个返回 true,反之则返回 false

另一处修改就是内部循环,现在变成了:

while (y>0 && compare(insertValue, resultArray[y-1])){

我们不再使用 insertValue < resultArray[y - 1]比较大小,改为使用compare(insertValue, resultArray[y-1])函数,它们的作用是一样的,唯一不同的是我们现在可以比较任何类型的数据,不再局限于数字。

来看一看测试代码:

@Test
    fun insertSort() {
        val sorter = InsertionSort()
        val list: Array<Int> = arrayOf(10, -1, 3, 9, 2, 27, 8, 5, 1, 3, 0, 26)
        val sortedList = sorter.insertSort(list) { x: Int, y: Int -> x < y }
        assertEquals(list.clone().apply { sort() }.joinToString(),
                sortedList.joinToString())

        val stringlist: Array<String> = arrayOf("b", "a", "d", "c", "e")
        val sortedStringList = sorter.insertSort(stringlist) { x: String, y: String -> x < y }
        assertEquals(stringlist.clone().apply { sort() }.joinToString(),
                sortedStringList.joinToString())
    }

从上面的测试代码可以看到,我们不仅可以对数字数组进行排序,同样可以对字符串数组排序。或者更复杂的对象进行排序:

val objList : Array<ComplextObject> = arrayOf(ComplextObject(3),ComplextObject(4),ComplextObject(0))
        sorter.insertSort(objList){obj1:ComplextObject, obj2:ComplextObject -> obj1.priority < obj2.priority}

上面的 lambda 表达式告诉 insertSort 方法根据 priority 属性对所有的对象进行排序。

插入排序是一种稳定的排序方法。所谓稳定的排序算法就是指:对于拥有相同排序键值的元素在排序后保持原来的相对位置不变。这一点对于一些简单的数据如数字或者字符串显得不是那么重要,但是对一些复杂的对象排序时非常重要。在上面的例子中,如果两个对象拥有相同的 priority,那么不管它们其他属性的值如何,这两个对象不会互相交换位置。

性能

插入排序对于有序队列会非常的快。这一点当然不用多说,但是这一点并非对所有的搜索算法都成立。在实际使用中,很多数据已经大部分排好序--如果没有完全排好序--插入排序会运行的非常好。

插入排序的最差复杂度和平均复杂度都是 O(n^2)。因为这个函数中有两重嵌套循环。其他的排序算法,比如快速排序和合并排序的复杂度是O(n log n),这在数据量比较大的时候会获得更快的处理速度。

实际上插入排序在处理小的数组时会非常地快。有一些标准库的排序算法在待排序数据小于等于10时,会从快速排序切换到插入排序。

本文编译自:Swift 算法俱乐部 - 插入排序

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容