插入排序
目标:排序一个数组从底到高(或从高到底)。
给你一个数字数组并需要把它们排成正确的顺序。插入排序算法的工作步骤:
- 把所有的数字放到堆中。这个堆是无序的;
- 从堆中取出一个数字。取哪个数字都无所谓,但最简单的做法是从堆顶取;
- 把这个数字插入到新的数组中;
- 从无序堆中取出下一个数字并同样的插入到数组中。它在第一个被选出来的数字的前面或者后面,这样这两个数字排好序了;
- 再次,从无序堆中取出数字插入到新数组相应的有序位置;
- 不断重复直到无序堆中没有任何数字。堆为空时结束,并数组也排好序了。
这就是为什么被称为“插入”排序的原因,因为你从堆中取出一个数字然后把它插入到数组中相应的有序位置。
例子
比如说,这是一个需要排序的[8, 3, 5, 4, 6]
。这是我们的无序堆。
取出第一个数字,8
,并插入到新数组。这个数组中什么都没有,所以这步很简单。现在这个有序数组为[8]
,堆为[3, 5, 4, 6]
。
从堆中取出下一个数字,3
,并且插入到有序数组中。它应该放在8
前面,所以有序数组现为[3, 8]
,堆为[5, 4, 6]
。
从堆中取出下一个数字,5
,并且插入到有序数组中。它应该放在3
与8
之间。有序数组为[3, 5, 8]
,堆为[4, 6]
。
重复这个过程直到堆为空。
In-place(就地) 排序
上面的解释使它看起来好像你需要两个数组:一个是无序堆,另一个用来装排好序的数字。
不过你可以In-place(就地)排序,不需要额外的数组。你只要记录哪部分是已经排好序的数组和哪部分是未排序的堆。
开始时,这个数组是[8, 3, 5, 4, 6]
。这|
竖线显示在排好序部分的末端,在无序堆的首端:
|
|
上面显示了排序部分为空,而无序堆从8
开始。
处理第一个数字后,变为:
|
|
排序部分是[8]
,无序堆为[ 3, 5, 4, 6 ]
。|
竖线向右挪了一位。
下面展示了在排序过程中数组内容的变化:
|
|
每一步,|
都向后移动一步。正如你所看到的,数组起始到|
之简的部分都排好序的。无序堆减一,而排序部分增一。直到无序堆为空,不再有无序数字可移动。
怎样插入
每一步,你取出无序堆最前面的数字,然后插入到排序数组部分中。你必须把这个数字放到排序部分的合适位置。这一步怎么做呢?
比如说,我们已经完成了前面几步,然后数组就像这样:
|
|
下一个要排序的数字是4。我们需要把它插入到排序部分[ 3, 5, 8 ]
数组的某位置。
这里有一个方法可以实现它:查看前一个元素,8
。
|
|
是否大于4
?大于,所以4
应该在8
的前面。交换它们的位置得到:
|
|
还没完。前面的新元素,5
,又大于4
。同样的,交换它们:
|
|
再次,查看前面的元素。3
大于4
?不大于,这意味着4
已经完成操作。开始部分的数组又变为有序了。
这是内循环插入排序算法的描述,你会在下一章看到。它采用数字交换的方法,把数字从截头部插入到有序部分。
代码
这里采用Swift实现插入排序:
|
|
使用Playground测试上面的代码,例如:
|
|
这里解释下,上面的代码怎么运行:
- 复制array。这一步是必须的,因为我们不能直接修改参数变量array。就像Swift自带的
sort()
,insertionSort()
函数将返回原始数组复制后的排序数组。 - 这里有两个循环在这个函数中。外循环依次查询数组中的所有元素;用来从堆中取出最上面的数字。
x
变量用来标识排好序部分的结尾与堆起始位置(即|
的位置)。记住,任何时刻,数组的起始 — 从0到x
— 总是排好序的。剩余的,从x
到最后一个元素,是无序堆。 - 内循环从
x
下标的元素开始。这是堆最顶端的数字,它可能小于前面所有元素。内循环一步步向后通过排序数组;每次查找前一个元素是否大于它,如果大于则交换位置。当内循环完成后,数组起始部分们又变成有序了,并且有序部分增加一个元素。
注意: 外循环从下标1开始而不是0。移动第一个元素从堆到排序部分,没有发生实质的变化,所以我们跳过了它。
去掉交换
上面的版本工作的很好,不过可以删除swap()
来提升一些速度。
你已经知道,我们通过交换数字来移动下一个元素到排序位置:
|
|
替代的做法是,我们只要向右挪动这些元素一个位置,然后复制一个新数字到目标位置即可:
|
|
在代码中,就像这样:
|
|
//1
行的代码是挪动前一个元素一个位置。在内循环的最后,y
是新元素插入到有序数组部分的目标位置,//2
行表示复制这个数字到目标位置。
增加泛型
如果可以用来排序其他类型,那就更好了。我们可以给数组的数据类型添加泛型并使用用户提供(user-supplied)函数(或闭包)去执行小于号比较,这只需要改变函数中两个地方。
函数的参数签名变为:
|
|
数组为类型[T]
,T
表示通用的占位类型。现在insertionSort()
函数可以接受任何类型的数组了,无论它包含数字,字符串,或者其他对象。
新参数isOrderedBefore: (T, T) -> Bool
是一个函数,这个函数比较两个T
对象,当第一个对象在第二个对象前面时返回true,相反时,返回false。这跟Swift内置函数sort()
的做法一样。
其他只要改变的地方在内循环中,现在改为:
|
|
我们调用isOrderedBefore()
函数来替换temp < a[y-1]
。这样是完全相同的,除了我们现在可以比较任何类型的对象,不仅仅只是数字。
在playground中做如下测试:
|
|
<
与>
决定排序顺序,相当于底-高,高-底。
当然,你也可以使用其他对象进行排序,比如字符串:
|
|
或者,较复杂的对象:
|
|
闭包告诉insertionSort()
函数,使用对象的priority
属性来排序。
插入排序是稳定排序。稳定排序是指当元素中有相同的元素,当排序完成后相同元素将保持原有的顺序。这不是很重要,对于简单值来说,比如:数字和字符串,不过对于较复杂的对象来说非常重要。在上面的例子中,如果两个对象有相同的priority
,不管他们的属性的值是多少,他们都不会交换位置。
性能
如果是已经排好序的数组那么插入排序非常的快。这听起来显而易见,不过这对于所有的搜索算法来说不太正确。在实际情况中,大部分数据已经排好序了,所以在这种情况下,插入排序工作的不错。
插入排序的最坏和平均的时间复杂度为O(n^2)。因为函数中有两个嵌套循环。其他排序算法,比如快速排序与合并排序的时间复杂度为O(n log n),它们输入数据很快。
在排好序的小数组使用插入排序是非常快的。有些标准库中,当数组分配粒度为10或更小时,使用插入排序代替快速排序。
我做过一个快速测试,来比较我们的insertionSort()
与Swift内置的sort()
。使用大小大约为100的数组,它们的时间花费差距非常小。然而,如果你输入一个比较大的数组,O(n^2)比O(n log n)开始有非常大的性能差距,并且插入排序不能工作。
参考阅读
Written for Swift Algorithm Club by Matthijs Hollemans