第二章 算法入门

 

循环不变式 loop invariants

We use loop invariants to help us understand why an algorithm is correct. We must show three things about a loop invariant:

·         Initialization: It is true prior to the first iteration of the loop.

·         Maintenance: If it is true before an iteration of the loop, it remains true before the next iteration.

·         Termination: When the loop terminates, the invariant gives us a useful property that helps show that the algorithm is correct.

我们使用循环不变式来帮助我们为什么一个算法是正确的,我们必须证明它的三个性质,才能说明它的正确性。

 

初始化:在第一次迭代之前,循环不变式用该是正确的。

保持:如果它在一次迭代之前是正确的,那么在下次迭代之前它必须任然是正确的。

终止:当循环结束时,不变式提供一个有用的性质,它有助于保证算法是正确的。

 

循环不变式与归纳法的区别

The third property is perhaps the most important one, since we are using the loop invariant to show correctness. It also differs from the usual use of mathematical induction, in which the inductive step is used infinitely; here, we stop the "induction" when the loop terminates.

第三个性质(终止)大概是最重要的,归纳法中的步骤是无穷的使用,而循环不变式会终止。

 

证明插入排序的正确性

INSERTION-SORT(A)

for j ← 2 to length[A]

2       do keyA[j]

3          Insert A[j] into the sorted sequence A[1 ‥ j - 1].

4          ij - 1

5          while i > 0 and A[i] > key

6              do A[i + 1] ← A[i]

7                 ii - 1

8          A[i + 1] ← key

 

初始化:在第一次迭代之前,只有一个需要排序的对象,显然是排好序的。

保持:这里只证明外层循环式正确的,比如:如果它在3次迭代之前是正确的,那么代码38把第三个数字插入到第i+1的位置,而这个位置前面的数都比它小,后面得数都比它大,所以在下次迭代之前它必须任然是正确的。

终止:j现在是A中对象的数目n+1,那么就有1……n是排好序的,这个性质说明A已经排好序了

 

 

算法分析

往往只考虑最坏时间,因为最好情况不会出现;平均情况往往和最坏情况差不多;概率分析技术可以确定一个算法的期望运行时间。

 

插入排序算法分析

//插入排序是增量法

BOOL InsertSort(INT *pArray, INT ilength)

{

         //算法把从0开始的数组排序

 

         for( INT j = 1;    j <     ilength; j++ )           

         {

                   INT key = pArray[ j ];         //需要比较的关键字

                   INT i = j - 1;                          //关键字上一个指针或迭代器

                   while( i >= 0 && pArray[ i ] < key )   //到达容器开始和当前元素比关键字大

                   {

                            pArray[ i + 1 ] = pArray[ i ];

                            i--;

                   }

                   pArray[ i + 1 ] = key;          //把关键字放到合适的位置,前j个对象都按<排序

         }

         return true;

}

语句

cost          

Times

for( INT j = 1;   j <     ilength; j++ )  

C1

N

INT key = pArray[ j ];

C2

n-1

INT i = j - 1;     

C3

n-1

while( i >= 0 && pArray[ i ] < key )

C4

假设执行tj

tjj=2……n

pArray[ i + 1 ] = pArray[ i ];

C5

(tj-1)j=2……n

i--;

C6

(tj-1)j=2……n

pArray[ i + 1 ] = key;        

C7

n-1

计算运行总时间

 

T(n) = c1*n  + c2*(n-1)  + c3*(n-1) + c4*(tjj=2……n) + c5*((tj-1)j=2……n) + c6 *((tj-1)j=2……n) + c7*(n-1)

 

当序列已经排好序时

while( i >= 0 && pArray[ i ] < key )会执行n-1次,所以最好时间性能为

T(N)  =  c1*n  + c2*(n-1)  + c3*(n-1) + c4*(n-1)  + c7*(n-1)

      =  (c1+c2+c3+c4+c7)n –(2+c3+c4+c7)

n的一个线性函数。

 

如果序列逆序排列,发生最坏情况

由求和公式

tjj=1……n= n(n+1)/2 可知最坏情况是一个关于n的二次函数

 

算法设计

There are many ways to design algorithms. Insertion sort uses an incremental approach: having sorted the subarray A[1 j - 1], we insert the single element A[j] into its proper place, yielding the sorted subarray A[1 j].

In this section, we examine an alternative design approach, known as "divide-and-conquer." We shall use divide-and-conquer to design a sorting algorithm whose worst-case running time is much less than that of insertion sort. One advantage of divide-and-conquer algorithms is that their running times are often easily determined using techniques that will be introduced in Chapter 4.

插入排序是增量法,下面介绍分治法,它的优点是可以用第四章的技术很容易的确定运行时间。

 

2.3.1 The divide-and-conquer approach 分治法

Many useful algorithms are recursive in structure: to solve a given problem, they call themselves recursively one or more times to deal with closely related subproblems. These algorithms typically follow a divide-and-conquer approach: they break the problem into several subproblems that are similar to the original problem but smaller in size, solve the subproblems recursively, and then combine these solutions to create a solution to the original problem.

The divide-and-conquer paradigm involves three steps at each level of the recursion:

·         Divide the problem into a number of subproblems.

·         Conquer the subproblems by solving them recursively. If the subproblem sizes are small enough, however, just solve the subproblems in a straightforward manner.

·         Combine the solutions to the subproblems into the solution for the original problem.

The merge sort algorithm closely follows the divide-and-conquer paradigm. Intuitively, it operates as follows.

·         Divide: Divide the n-element sequence to be sorted into two subsequences of n/2 elements each.

·         Conquer: Sort the two subsequences recursively using merge sort.

·         Combine: Merge the two sorted subsequences to produce the sorted answer.

很多算法在结构上是递归的,这些算法采用分治策略,把问题分成n个小问题,这些小问题又非常的相似,递归的解决这些问题,然后合并其结果就能得到原问题的解。

分治模式在每一层递归上都有三个步骤:

分解divide:将原问题分解成一系列子问题;

解决conquer:递归的解各个子问题,直到子问题小到可以直接得到解。

合并combine:将子问题的结果合并成原问题的解。

 

合并排序就是分治法:

 

Merge是解决合并子问题解的算法

 

MERGE(A, p, q, r) 数组ap<=q<r,是下标
n1  q - p + 1 
 2  n2  r - q
 3  create arrays L[1  n1 + 1] and R[1  n2 + 1]
for i  1 to n1
 5       do L[i]  A[p + i - 1]
for j  1 to n2
 7       do R[j]  A[q + j] //复制
L[n1 + 1]  
R[n2 + 1]  
10  i  1
11  j  1
12  for k  p to r
13       do if L[i]  R[j]
14             then A[k]  L[i]
15                  i  i + 1
16             else A[k]  R[j]
17                  j  j + 1

 

把数组分成两个小数组,然后在两个小数组中找出小的放入a中(1217行)

 

MERGE-SORT才是合并排序算法,它先不断的分解问题规模,直到问题可以直接求解,然后合并(MERGE)子问题的解

MERGE-SORT(A, p, r)
1 if p < r
2   then q  (p + r)/2
3        MERGE-SORT(A, p, q)
4        MERGE-SORT(A, q + 1, r)
5        MERGE(A, p, q, r)

 

这里再用循环不变式证明合并排序中的的合并算法1217行是正确的

初始化:通过MERGE-SORT算法,合并算法是要把得到解得子问题合并,所以pq是排好序的,q+1r是排好序的,在一开始k=pA中没有元素,状态显然正确,i=j=1,没动过LR,所以LR的状态也是正确的

保持:在一次执行完后A中放了k个排序的数,状态都正常,这时L[i]R[j]开始比较,如果L[i]比较小,把L[i]放入A中第k+1位置,i也增加1,现在状态也都正常。

结束:k等于r+1,那么A中有r个数排好序了,A在这次merger中正是要排序r个数,所以状态正确。

 

所以合并排序是正确的。

分治法分析

当一个算法包含对自己的递归调用时,其运行时间可以用一个递归方程来表示T(N) =

1.O(1) 如果n<=C,规模小于c,直接得到解

2. aT(n/b) + D(n) + C(n)

a为分解为多少个问题 ,b是表示每一个问题为原问题的大小的1/bDn)分解该问题时间,Cn)为合并该问题的时间。

 

现在分析最坏运行时间:

分解:只是计算序列中间位置,D(N)=O(1)

解决:递归的解两个规模为n/2的子问题,时间为2T(n/2)

合并:在一个包含n个元素的子数组上,merge函数最坏时间为C(n)=O(n)

所以根据“主定理”可以知道T=nlgn(第四章)

根据构造递归树也可以求出这个解,lgn为树高,树的每一层的代价为n

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值