Swift算法-插入排序Insertion sort

声明:算法和数据结构的文章均是作者从github上翻译过来,为方便大家阅读。如果英语阅读能力强的朋友,可以直接到swift算法俱乐部查看所有原文,以便快速学习。作者同时也在学习中,欢迎交流

插入排序算法的目的:将一个无序数组按照升序或者降序排列起来。

当你得到一个无序数组,并需要将该数组按照需求进行排序时,插入排序算法会按照以下步骤执行实现:

1.将所有数字放在一起,我们假设此时所有的数字被无序但又整齐的排列在一个通道上。
2.从通道上随意取出一个数字,可以从最前端取,也可以从最后端去,无所谓。不过最简单的每次都从最前端取出。
3.将取出来的数字放入新的数组里面。
4.从通道中取出第二个数字并且插入第三步建立的新数组里面,根据升降序将新数字放在第一个数字的前面或者后面,得到一个有顺序的数组。
5.继续从通道中取出新数字,根据大小插入到新数组合适的位置。
6.重复以上步骤直到通道中没有数字,排序完毕,新的数组就是我们得到的排序好的数组。

例子

我们需要整理一个无序数组为[8,3,5,4,6]。
取出第一个数字8,得到新的数组[8]。无序数组变为[3,5,4,6]。
取出第二个数字3,插入新的数组里,3比8小,得到[3,8]。无序数组变为[5,4,6]。
取出第三个数字5,插入新的数组里,5比3大,比8小,得到[3,5,8]。无序数组变为[4,6]。
取出第四个数字4,插入新的数组里,4比3大,比5小,得到[3,4,5,8]。无序数组变为[6]。
最后取出6,插入新数组里,6比5大,比8小,得到[3,4,5,6,8]。排序完成。

原地排序

我们刚才对插入排序算法的解释,可能会让大家误以为插入排序必须要有两个数组配合完成,一个放无序数组,一个为有序数组。其实不然,我们可以对一个无序数组直接进行原地排序,不需要多建一个数组。这里我们需要注意的就是区分开数组里已排序和未排序的部分。

还是刚才的数组[8,3,5,4,6]。这次我们用|来划分数组中已排序和未排序。

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

我们从左向右开始整理数组,|放在最前面,此时已整理部分为空。当我们开始向右移动|,我们得到

[ 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]。先将|向右移动一个位置,让4进入左侧区域,此时,我们将4与其左侧数字8(已排序好数组里最大的)进行比较:

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

4比8小,所以4应该在8前面,将两个数字交换位置:

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

我们继续4和左侧数字5进行对比,4比5小,所以4应该在5前面,继续交换位置:

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

再次对比4和左侧数字3,4比3大,不需要交换位置,此时左侧数组又变成排序好的数组。

以上过程为插入排序算法的内循环描述,在下一部分的代码中会体现出来。总体过程就是将右侧数值放入左侧已排序数组的最右端,然后通过大小比较不断地交换位置,直到结束。

代码

func insertionSort(_ array: [Int]) -> [Int] {
  var a = array                             // 1
  for x in 1..<a.count {                    // 2
    var y = x
    while y > 0 && a[y] < a[y - 1] {        // 3
      swap(&a[y - 1], &a[y])
      y -= 1
    }
  }
  return a
}

可以将上述代码放在playground并进行以下测试:

let list = [ 10, -1, 3, 9, 2, 27, 8, 5, 1, 3, 0, 26 ]
insertionSort(list)

代码的运行逻辑如下:

1.首先复制整个数组。这个步骤是必须的因为我们不能直接对原始数组的内容进行修改。就像Swift自己的函数sort()insertionSort()只会返回一个整理好的原始数组的复制数组。

2.整个函数有两个循环。外部循环按照顺序遍历数组里的每一个数字,同时每次外部循环所选择的数字,就是每次|向右侧移动进来的数字。这里x表示左侧已排序数组最后一位数字的位置,记住,数组里从0到x的部分就是已经排序好的部分。其余的为未排序的部分。

3.内部循环从位于x位置的数字,也就是从|向右移动进来的数字,开始不断向前一位进行大小比较,当发现当前位置的数字小于前一位,立刻向左移动并继续比较,直到排序结束。

去掉交换过程

上述版本的代码运行效果良好,但是,我们仍然可以进行优化,方法就是去掉swap()方程。你已经知道我们需要将顺序不对的数字进行位置调整:

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

去掉图中的交换过程,我们可以将图中需要交换位置的数字直接向右移动,然后将新的数字直接复制到正确的位置:

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

[ 3,5,8,8|6 ]  将8转移到右侧
        -->

[ 3,5,5,8|6 ]  将5转移到右侧
     -->

[ 3,4,5,8|6 ]  将4复制粘贴到新的位置
     *

代码如下:

func insertionSort(_ array: [Int]) -> [Int] {
  var a = array
  for x in 1..<a.count {
    var y = x
    let temp = a[y]
    while y > 0 && temp < a[y - 1] {
      a[y] = a[y - 1]                // 1
      y -= 1
    }
    a[y] = temp                      // 2
  }
  return a
}

其中//1表示将需要交换位置的数字向右移动一个位置。在内循环的最后,y是新数字最后正确的位置的索引,//2就是将新数字复制到该位置。

通用化

很显然,上述的代码只适合对数字类型的数据进行排序,但这里我们只需要对代码进行两处修改,即可完成对大部分类型数据的排序。首先,函数名更改如下:

func insertionSort<T>(_ array: [T], _ isOrderedBefore: (T, T) -> Bool) -> [T] {

此时数组的数据类型为[T],这里的T是通用类型,可以是数值,字符串,或者其他数据类型。新的参数isOrderedBefore: (T, T) -> Bool函数带有两个T对象,函数返回true表示第一个对象排在第二个对象前面,返回false则表示第一个对象排在第二个对象后面。效果等同于Swift自带函数sort()

第二个修改的地方是内循环:

while y > 0 && isOrderedBefore(temp, a[y - 1]) {

这里我们用isOrderedBefore()取代了temp < a[y - 1],它们的目的相同,唯一不一样的是前者可以对任何数据类型进行比较,不单单是数字。

我们可以在playground进行以下测试:

let numbers = [ 10, -1, 3, 9, 2, 27, 8, 5, 1, 3, 0, 26 ]
insertionSort(numbers, <)
insertionSort(numbers, >)

这里<>表示序列的顺序,从小到大或者从大到小。当然我们也可以对其他数据类型进行排序,比如字符:

let strings = [ "b", "a", "d", "c", "e" ]
insertionSort(strings, <)

或者是更复杂的对象:

let objects = [ obj1, obj2, obj3, ... ]
insertionSort(objects) { $0.priority < $1.priority }

这里的闭环告诉insertionSort()函数对对象的priortity特性进行排序。

插值排序属于稳定排序。当数组里含有同样的整理参考对象的元素在经过整理后仍然保持原来的顺序,我们就说这个排序就是稳定的。这个特性对于数值或者字符排序来说并不是很重要,但是对于更复杂的对象来说就非常重要了。正如上面的例子,如果数组里面的两个对象拥有同样的priority,我们可以无视掉它们的其它特性值,不对它们进行位置的调整。

性能

当数组是已经排序好的数组,插值排序算法的运行速度非常快。这句话听起来好像理所当然,但是对于所有搜索算法来说,并不是这样的。实际上,在大部分数据已经排序好的前提下,不是全部,插值排序依然可以取得不赖的效果。

对于插值排序算法来说,O(n^2)是它最差和平均性能表现。因为它的函数里含有两个循环。其它类型的排序算法,比如快速排序和归并排序,在输入数据量很大的情况下,可以达到O(nlogn)的效果。

插值排序对于小数据量的排序来说非常快。在一些标准程序库里,如果数据大小不大于10,它们会用插值排序来取代快速排序。

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

推荐阅读更多精彩内容