《算法导论》笔记——第2章 算法入门

第2章 算法入门

插入排序:对数组A进行插入排序
	Insertion-sort(A)
	1 for j<--2 to length[A]
	2		do key <-- A[j]  //<--代表赋值
	3			*Insert A[j] into the sorted sequence A[1..j-1]
	4			i <-- j-1
	5		while A[i] > key and i > 0
	6			do A[i+1] <-- A[i]
	7					i <-- i-1
	8		A[i+1] <-- key

循环不变式与插入算法的正确性:
循环不变式用来帮助我们理解算法的正确性,必须证明它的三个性质:
1 初始化:循环不变式在第一轮迭代之前是正确的;
2 保持:如果它在某一轮迭代开始前是正确的,那么在下一轮开始前也应该保持正确;也就是某一轮的迭代是成立的。
3 终止:当循环结束时,不变式给了我们一个有用的性质。它有助于表明算法是正确的。

对于插入算法而言,
初始化:在第一轮迭代之前,j = 2,已排好序的元素是A[1…j-1]即A[1],是只有一个元素的子数组,所以是已排序的,初始化性质是正确的。
保持:在某一轮迭代时,对某个A[j]元素来说,它先用变量key来保存它的值,然后key和已排好序的元素从后往前逐一比较,已排好序中的元素只要比key大的都往后移一位,直至找到小于等于key的第一个值,然后将key插入,在某一轮的迭代中,保持性质也是成立的。
终止:在终止的时候,j=length[A]+1,此时已经退出了循环,而此时的已排好的序列A[1…j-1]为A[ 1…length[A] ],这个子数组就是整个数组,即整个数组排序完毕。所以终止性质也成立,说明算法是正确的。

插入排序算法的分析:(n=length[A])
INSERTION-SORT ----------------------cost-----------------times
1 for j<–2 to length[A] -------------------c1---------------------n(循环头始终比循环体多一次)
2 do key<–A[j] ----------------------------c2---------------------n-1
3 *Insert A[j] into the sorted
sequence A[1…j-1]
4 i <-- j-1 -----------------------------------c4---------------------n-1
5 while A[i] > key and i >0 -------------c5---------------Σtj (j=2…n)
6 do A[i+1] <-- A[i] -----------------------c6---------------Σ(tj - 1) j=2…n
7 i <-- i-1 -----------------------------------c7---------------Σ(tj - 1) j=2…n
8 A[i+1] <-- key ---------------------------c8---------------------n-1

总运行时间T(n) = c1n + c2(n-1) + c4(n-1) + c5Σti + c6Σ(ti-1) +c7(ti-1) + c8(n-1)
如果输入的数组是已经排好序时:(不用进入while循环,只需n-1次判断)
T(n) = c1n + c2(n-1) + c4(n-1) + c5(n-1) + c8(n-1)
= (c1 + c2 + c4 + c5 + c8)n - (c2 + c4 + c5 + c8)
设 a = c1 + c2 + c4 + c5 +c8, b = -(c2 + c4 + c5 + c8)
T(n) = an + b,T(n)此时是一个线性函数。
如果输入的数组是逆序时:
A[j]=key与A[1…j-1]各个元素进行比较,循环头判断 j 次 (始终比循环体多一次以退出循环),
Σtj = Σ j (j = 2…n) = (2 + n) (n - 1) / 2
Σ(tj-1)= Σ( j-1) = (1+n-1)(n-1) / 2 = n(n-1) / 2
T(n) = c1n + c2(n-1) + c4(n-1) + c5(2 + n)(n - 1) / 2 + c6(n - 1)n /2 + c7(n - 1)n / 2 + c8(n - 1)
= (c5/2 + c6/2 + c7/2)n^2 + (c1 + c2 + c5/2 - c6/2 - c7/2 + c8)n -(c2 + c4 + c5 + c8)
设 a = c5/2 + c6/2 + c7/2, b = c1 + c2 + c5/2 - c6/2 - c7/2 +c8, c = -(c2 + c4 + c5 + c8)
T(n) = an^2 + bn + c,T(n)此时是关于n的二次函数。

算法设计:
插入排序使用的是增量法,即不断将元素插入到子数组[1…j-1]中,不断增加子数组的长度,直至
子数组等于整个数组的大小。

分治法:
将原问题划分成n个规模较小而结构与原问题相似的子问题,递归地解决这些子问题,然后再合并其结果,就得到原问题的解。分治模式在每一层递归都有三个步骤:
1.分解:将原问题分解成一系列的子问题;
2.解决:递归地解决各个子问题,若子问题足够小,则直接求解;
3.合并:将子问题的结果合并成原问题的解。
合并排序完全依照分治模式:
1.分解:将n个元素分成2个各含n/2个元素的子序列;
2.解决:用合并排序法对两个子序列递归地排序,只有一个元素时,则视为有序(即子问题足够小,直接求解)
3.合并:合并两个已排序的子序列以得到排好序的数组

对于将原问题分解,分解不止分解一次,分解到子问题足够小可以直接解决为止。合并排序最关键的步骤时合并,假设子数组A[p…q]和A[q+1…r]都排好序,将它们合并成一个已排好序的子数组A[p…r]。
为了避免检查每个子数组为空,所以在每一个要合并的子数组后加一个”哨兵位“,它包含一个特殊的值(比如-1)。此处为了简化代码,利用∞来作为哨兵值。

MERGE(A, p, q, r)
1 n1 <-- q-p+1 //q-p+1是数组A[p..q]的长度
2 n2 <-- r-q //r-q是数组A[q+1..r]的长度
3 create arrays L[1..n1+1] and R[1..n2+1]
4 for i <--1 to n1
5 		do L[i] <-- A[p+i-1]
6 for j <--1 to n2
7		do R[j] <-- A[q+j]
8 L[n1+1] <-- ∞
9 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 

循环不变式与排序算法的正确性:
循环不变式是上述的12-17行,我们要证明它的三个性质
1.初始化:在for循环的第一轮迭代开始之前,k=p,子数组A[p…k-1]为空,即k-1-p+1=k-p=0,
L[i]和R[j]的元素都在各自的数组中。
2.保持:在某一轮的迭代中,假设L[i]<=R[j],将较小的元素L[i]复制到A中,此时子数组由A[p…k-1]变成了A[p…k],元素个数从k-p变成k-p+1。增加k和i(i=i+1)的值,会为下一轮重新建立循环不变式的值。如果这次由L[i] > R[j],则16-17行就会执行适当的操作,以使循环不变式保持成立。
3.终止:结束循环时,k=r+1,子数组A[p…k-1]为A[p…r],包含p-r+1个在L[i]或R[j]中的元素,并且排好序。而A[p…r]即为原数组,说明已经将原数组排好序了。

现在,可以将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)

分治法分析:
设T(n)为一个规模为n的问题的运行时间。如果问题足够小,如n<=c(c为一个常量),则得到它的直接解的时间为Θ(1)。假设我们把问题分解成a个子问题,每一个大小是原问题的1/b。(在合并排序中,a=b=2,但在许多分治法中,a≠b),如果分解问题和合并问题的时间个为D(n)和C(n),则递归式为:T(n) = {Θ(1), 如果n<=c;aT(n/b) + D(n) + C(n),否则}

合并排序算法的分析:
最坏情况下合并排序n个数的运行时间:
分解:这一步计算子数组的中间位置,D(n) = Θ(1)。
解决:递归地解两个规模为n/2的子问题,时间是2T(n/2)
合并:在n个元素的数组上,MERGE过程的运行时间是Θ(n)

由于D(n) = Θ(1),C(n) = Θ(n),所以D(n) + C(n) = Θ(n)
所以上述的递归式可以写为:
T(n) = {Θ(1),如果n<=c;2T(n/2) + Θ(n),否则}

实际上,合并排序最坏情况的运行时间T(n)= Θ(nlgn)。
T(n) = {c,如果n=1;2T(n/2) + cn,如果n>1},其中常量c代表规模为1的问题所需的时间,也是在“解决”和“合并”步骤中处理每个数组元素所需的时间。运用递归树方法求递归式,可以知道树的层数有lgn+1(n为每层的结点数),每一层的代价为cn,所以整棵树的代价是cn(lgn+1) = cnlgn + cn,忽略低阶项和常量c,即得到结果 Θ(nlgn)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值