算法导论 - 基础知识 - 算法基础

在《算法导论》一书中,插入排序作为一个例子是第一个出现在该书中的算法。

插入排序:

对于少量元素的排序,它是一个有效的算法。

插入排序的工作方式像许多人排序一手扑克牌。开始时,我们手中牌为空,我们每次从牌堆中取出一张牌并将其放入正确的位置。为了找到一张牌的正确位置,我们从左到右将它与手中已有的每张牌进行比较。

将其伪代码过程命名为 INSERTION-SORT,参数是一个数组A,具体如下:

INSERTION-SORT(A):

  for j = 2 to A.length

    key = A[j]   // Insert A[j] into the second sequence A[1..j - 1]. 

    i = j - 1       // A[i]、A[j]

    while i > 0 and A[i] > key

      A[i + 1] = A[i]

      i = i - 1

    A[i + 1] = key

伪代码解释:下标 j 指出正被插入到手中的牌,在for循环的每次迭代开始,A[j]将数组A分为了两部分,一部分是A[1..j - 1]的子数组构成当前排序好的手中的牌,剩余的子数组A[j + 1..n]对应仍在桌上的牌堆。

我在刚看到插入排序的工作方式后,也写了一下伪代码。采用牌堆模拟的方法,但在具体的实现过程,我用一个数组表示牌堆,用一个空vector表示手中的空手牌,看了书中的伪代码才直到原来只用一个数组就行,减少了近一半的内存占用。这也算是这段伪代码的一个亮点。其实仔细想想,这种对空间复杂度的优化不算少见,非常典型的例子就是背包问题的一维、二维数组实现。背包问题在进行动态规划时,我们使用一个矩阵来记录它的状态,通过不断更新矩阵进行状态转移,由于我们只需要最后一行矩阵,而且每次更新矩阵的一行时只有矩阵上一行的值确定,故可采用二维数组优化,两个数组不断交换更新即可。对于01背包问题,可以更近一步使用一维数组(由于01背包的状态转移方程指示出,当一维数组的值更新时一定由该数组前面的值给出,我们每次从后向前更新数组即可动态更新整个矩阵)。

所以在以后,执行对数组的一些操作前可以想一想是否可以利用数组自身的状态更新自身,减少不必要的内存消耗。

 

该书在之后介绍了一个非常重要的概念:循环不变式

循环不变式主要用来证明算法的正确性。关于循环不变式,我们必须证明三条性质:

初始化:循环的第一次迭代之前,它为真。

保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真。(即对这一次迭代循环不变式保持为真)

终止:循环终止时,不变式为我们提供了一个有用的性质,该性质有助于证明算法的正确性。

类似于数学归纳法,为证明某条性质成立,需要证明一个基本情况和一个归纳步。

对插入排序而言,

初始化:第一次循环迭代之前,此时 j=2,子数组A[1..j - 1]仅有单个元素A[1]组成,已排序,循环不变式为真;

保持:非形式化地(指理解描述,未用数学符号严格表示),对某次迭代而言,for循环体的第4~7行语句将A[j - 1]、A[j - 2]、A[j - 3]等向右移动一个位置,直到找到A[j]的适当位置,第8行语句将A[j]的值插入该位置。这时子数组A[1..j]中的元素组成已排好序,在该次迭代中,循环不变式保持为真。

终止:导致for循环终止的条件是j > A.length。每次for迭代 j 增加一,那么循环结束时必有 j=n+1。在循环不变式的表述中用将 j 用 n+1 替换,我们有,子数组A[1..n]中的元素已排好序,子数组A[1..n]就是整个数组,因此我们推断整个数组已排好序。因此算法正确。

 

猜你喜欢

转载自www.cnblogs.com/Black-treex/p/12439883.html
今日推荐