算法设计与分析——排序算法(一):插入排序

分类目录:《算法设计与分析》总目录
相关文章:
· 排序算法(一):插入排序
· 排序算法(二):归并排序
· 排序算法(三):堆排序
· 排序算法(四):选择排序
· 排序算法(五):冒泡排序
· 排序算法(六):希尔排序
· 排序算法(七):快速排序
\qquad · ①基础知识
\qquad · ②快速排序的性能
\qquad · ③快速排序的随机化
\qquad · ④快速排序的分析
· 排序算法(八):计数排序
· 排序算法(九):基数排序
· 排序算法(十):桶排序
· 排序算法:比较排序算法的下界
· 排序算法:十大排序算法总结


插入排序对于少量元素的排序是一个有效的算法。插入排序的工作方式像许多人排序一手扑克牌。开始时,我们的左手为空并且桌子上的牌面向下。然后,我们每次从桌子上拿走一张牌并将它插入手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较,而拿在手上的牌总是排序好的。

对于插入排序,我们将其代码过程命名为insertionSort(arr),其中的参数是一个数组 a r r [ 1 , 2 , ⋯   , n ] arr[1, 2, \cdots, n] arr[1,2,,n],包含长度为 n n n的要排序的一个序列。该算法原址排序输入的数:算法在数组 a r r arr arr中重排这些数,在任何时候,最多只有其中的常数个数字存储在数组外面。在过程insertionSort(arr)结束时,输入数组arr包含排序好的输出序列。

def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j >= 0 and key < arr[j]:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
    return arr

下图表明对 A = ( 5 , 2 , 4 , 6 , 1 , 3 ) A = (5, 2, 4, 6, 1, 3) A=(5,2,4,6,1,3)该算法如何工作。下标 j j j指出正被插入到手中的“当前牌”。在for循环(循环变量为 j j j)的每次迭代的开始,包含元素 A [ 1 , 2 , ⋯   , j − 1 ] A[1, 2, \cdots, j - 1] A[1,2,,j1]的子数组构成了当前排序好的手中的牌,剩余的子数组 A [ j + 1 , j + 2 , ⋯   , n ] A[j + 1, j + 2, \cdots,n] A[j+1,j+2,,n]对应于仍在桌子上的牌堆。事实上,元素 A [ 1 , 2 , ⋯   , j − 1 ] A[1, 2, \cdots, j - 1] A[1,2,,j1]就是原来在位置 1 1 1 j − 1 j - 1 j1的元素,但现在已按序排列.我们把 A [ 1 , 2 , ⋯   , j − 1 ] A[1, 2, \cdots, j - 1] A[1,2,,j1]的这些性质形式地表示为一个循环不变式:在第2~8行的for循环的每次迭代开始时,子数组 A [ 1 , 2 , ⋯   , j − 1 ] A[1, 2, \cdots, j - 1] A[1,2,,j1]由原来在 A [ 1 , 2 , ⋯   , j − 1 ] A[1, 2, \cdots, j - 1] A[1,2,,j1]中的元素组成,但已按序排列。
插入排序示意图
循环不变式主要用来帮助我们理解算法的正确性。关于循环不变式,我们必须证明三条性质:

  • 初始化:循环的第一次选代之前,它为真。
  • 保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真。
  • 终止:在循环终止时,不变式为我们提供一个有用的性质,该性质有助于证明算法是正确的。

当前两条性质成立时,在循环的每次迭代之前循环不变式为真。当然,为了证明循环不变式在每次迭代之前保持为真,我们完全可以使用不同于循环不变式本身的其他已证实的事实。注意,这类似于数学归纳法,其中为了证明某条性质成立,需要证明一个基本情况和一个归纳步。这里,证明第一次迭代之前不变式成立对应于基本情况,证明从一次迭代到下一次迭代不变式成立对应于归纳步。第三条性质也许是最重要的,因为我们将使用循环不变式来证明正确性。通常,我们和导致循环终止的条件一起使用循环不变式。终止性不同于我们通常使用数学归纳法的做法,在归纳法中,归纳步是无限地使用的,这里当循环终止时,停止“归纳”。

让我们看看对于插入排序,如何证明这些性质成立。

  • 初始化:首先证明在第一次循环迭代之前(当 j = 2 j = 2 j=2时),循环不变式成立。所以子数组 A [ 1 , 2 , ⋯   , j − 1 ] A[1, 2, \cdots, j - 1] A[1,2,,j1]仅由单个元素 A [ 1 ] A[1] A[1]组成,实际上就是 A [ 1 ] A[1] A[1]中原来的元素。而且该子数组是排序好的。这表明第一次循环迭代之前循环不变式成立。
  • 保持:其次处理第二条性质:证明每次选代保持循环不变式。非形式化地,for循环体的第4~7行将 A [ j − 1 ] A[j - 1] A[j1] A [ j − 2 ] A[j - 2] A[j2] A [ j − 3 ] A[j - 3] A[j3]等向右移动一个位置,直到找到 A [ j ] A[j] A[j]的适当位置,第8行将 A [ j ] A[j] A[j]的值插入该位置。这时子数组 A [ 1 , 2 , ⋯   , j ] A[1, 2, \cdots, j] A[1,2,,j]由原来在 A [ 1 , 2 , ⋯   , j ] A[1, 2, \cdots, j] A[1,2,,j]中的元素组成,但已按序排列。那么对for循环的下一次迭代增加 j j j将保持循环不变式。第二条性质的一种更形式化的处理要求我们对第5~7行的while循环给出并证明一个循环不变式。然而,这里我们不愿陷入形式主义的困境,而是依赖以上非形式化的分析来证明第二条性质对外层循环成立。
  • 终止:最后研究在循环终止时发生了什么。导致for循环终止的条件是 j > A . l e n g t h = n j > A.length = n j>A.length=n。因为每次循环迭代 j j j增加 1 1 1,那么必有 j = n + 1 j = n+1 j=n+1。在循环不变式的表述中将 j j j n + 1 n+1 n+1代替,我们有:子数组 A [ 1 , 2 , ⋯   , n ] A[1, 2, \cdots, n] A[1,2,,n]由原来在 A [ 1 , 2 , ⋯   , n ] A[1, 2, \cdots, n] A[1,2,,n]中的元素组成,但已按序排列。注意到,子数组 A [ 1 , 2 , ⋯   , n ] A[1, 2, \cdots, n] A[1,2,,n]就是整个数组,我们推断出整个数组已排序。因此算法正确。

最后,我们用动图演示一下插入排序的全过程:
插入排序动图

  • 0
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

von Neumann

您的赞赏是我创作最大的动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值