【算法导论】第2章:算法基础

2.1    插入排序

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

插入排序伪代码如下:

1INSERTION-SORT(A) 2 for j = 2 to A.length 3 key = A[j] //为了便于元素后移,先将要插入的那个元素保存起来 4 // Insert A[j] into the sorted sequence A[1...j-1] 5 i = j-1 6 while i>0 and A[i]>key 7 A[i+1] = A[i] 8 i = i-1 9 A[i+1] = key //因为while循环退出时,A[i]<=key,所以将key插入到第i+1个位置即可 10

循环不变式与插入排序的正确性

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

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

当前两条性质成立时,在循环的每次迭代之前循环不变式为真。这类似与数学归纳法,其中为了证明某条性质成立,需要证明一个基本情况和一个归纳步。这里,证明第一次迭代之前不变式成立对应于基本情况,证明从一次迭代到下一次迭代迭代不变式成立对应于归纳步。

第三条性质是最重要的,因为我们将使用循环不变式来证明算法的正确性。通常我们和导致循环终止的条件一起使用循环不变式。终止性不同于我们通常使用数学归纳法的做法,在归纳法中,归纳步是无限地使用的,这里循环终止时停止归纳。

对于插入排序证明这些性质成立。

**初始化:**首先证明在第一次循环迭代之前(当 j = 2 时),循环不变式成立。所以子数组A[1...j-1]仅由单个元素A[1]组成,实际上就是A[1]中原来的元素。而且该子数组是排好序的(当然很平凡)。这表明第一次循环迭代之前循环不变式成立。

保持:其次处理第二条性质:证明每次迭代保持循环不变式。非形式地,for循环体将A[j-1]、A[j-2]、A[j-3]等向右移动一个位置,直到找到A[j]的合适位置,最后将A[j]的值插入该位置。这时子数组A[1..j]由原来在A[1..j]中的元素组成,但以按序排列。那么对for循环的下一次迭代增加j将保持循环不变式。

终止:最后研究在循环终止时发生了什么。导致for循环终止的条件是j>A.length=n。因为每次循环迭代 j 增加1,那么必有 j =n+1。在循环不变式的表述中将 j 用 n+1 代替,我们有:子数组A[1..n]由原来在A[1..n]中的元素组成,但已按序排列。注意到,子数组A[1..n]就是整个数组,我们推断出整个数组已排序。因此算法正确。

下面给出一个笔者自己认为易于理解的版本,可能会有欠妥之处,望读者指出。

**初始化:**在第一次迭代前只有一个元素,即是该子数组仅有一个元素,所以它是有序的。

**保持:**假设我们要插入A[j],为了将A[j]插入到正确的位置,我们需要与前面的元素(前面的元素是有序的,即A[1..j-1]有序)比较,然后后移元素,此时子数组A[1..j]是有序的。

终止:因为for循环退出时,j>A.length=n,此时我们将循环不变式的 j 换成n+1。其实可以理解为在保持中的第n次迭代成立,这样子数组A[1..n]由原来在A[1..n]的元素组成,并且是按序排列的。

2.2    分析算法

分析算法的结果意味着预测算法需要的资源。虽然有时我们主要关心像内存、通信宽带或计算机硬件这类资源,但是通常我们想度量的是计算时间。算法导论上面的章节绝大多数假定一种通用的单处理器计算模型——随机访问机(random-acess machine,RAM)作为实现技术,算法可以用计算机程序来实现。在RAM模型中,指令一条接一条地执行,没有并发操作。

插入排序算法的分析

一般来说,算法需要的时间与输入规模同步增长,所以通常把一个程序的运行时间描述成其输入规模的函数。

输入规模:主要依赖于研究的问题,对于排序算法或计算离散傅里叶变换的量度是输入的项数。

运行时间:一个算法在特定输入上的运行时间是指执行的基本操作数或步数。

比如对于排序算法来说,我们假定第i行代码的代价为ci,通过计算出程序中所有代码的代价总和得到一个关于输入规模n的函数。插入排序的最佳运行时间 (假定输入数组已排序) t = an+b 因此它是n的线性函数。插入排序的最坏运行时间(假定输入数组反向排序) t = an^2+bn+c ,因此它是n的二次函数。

最坏情况与平均情况分析

我们往往集中于只求最坏情况运行时间,即对于规模为n的任何输入,算法的最长运行时间,下面给出三点理由:

  • 一个算法的最坏情况运行时间给出了任何输入运行时间的一个上界。知道了这个上界,就能确保该算法绝不需要更长的时间。我们不必对运行时间做某种复杂的猜测并可以期望它不会变得更坏。
  • 对于某些算法,最坏情况经常出现。例如,当在数据库中检索一条特定的消息时,若该信息不在数据库中出现,则检索算法的最坏情况经常出现。在某些应用中,对缺失信息的检索可能是频繁的。
  • “平均情况”往往和最坏情况大致一样差。就对于插入排序来说,假定随机选取n个数,当插入 A[j] 时A[1..j-1]中一半比A[j]大,一般比A[j]小,所以在移动的时候的那个 t 比最坏运行时间的那个 t 减少一半,但最终导致平均情况运行时间像最坏情况的运行时间一样,也是输入规模的一个二次函数。

增长量级

在前面的运行时间的代价ci和总代价关于n的函数,实际上是我们做出的一种抽象。比如对于插入排序的最坏运行时间 t=an^2+bn+c,我们不仅忽略了实际的语句代价,而且也忽略了抽象代价ci。

现在我们做出一种更简化的抽象:即我们真正感兴趣的运行时间的增长率增长量级。所以我们只考虑最重要的项,比如排序算法中的an^2,因为当n很大时,低阶项相对来说不太重要。我们也忽略最重要项的常系数,因为对于大的输入,在确定计算效率时,常量因子不如增长率重要。所以对于插入排序来说,如果我们忽略低阶项和最重要的项的常系数时,只剩下最重要项的因子n^2。我们记插入排序具有最坏情况运行时间Θ(n^2)。如果一个算法的最坏情况运行时间具有比另一个算法更低的增长量级,那么我们通常认为前者比后者更有效。

 

代码交流 2021