算法入门章——引出贯穿《算法导论》全书的算法分析和设计框架

本文深入探讨了插入排序和合并排序两种经典排序算法。首先通过插入排序的实际情景和算法描述,逐步引导读者理解其工作原理,接着引入了算法设计与分析框架,并通过伪码详细展示了算法流程。随后,文章对算法正确性进行了严谨的数学证明,并对其时间复杂度进行了细致分析。最后,文章通过对比插入排序和合并排序,介绍了分治法思想及其应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

刚刚认真学习了第二章,习题还未做。现在趁热打铁,先来凭空总结和回忆一下整个过程。

本章主要线索:通过引入两个算法,从插入排序分析和设计排序算法,引出了整本书后续各章节的算法设计和分析的框架。这个框架归纳起来即是:

引入问题并以实际情景进行思考-->抽象出精确的算法描述(这里就包含了所用的数据结构)-->伪码表示-->证明算法的正确性-->算法分析-->算法设计。

本章先引入的是插入排序算法。

1.实际情景:

插入排序可以以这个实际情景来设想:有一堆牌拿在右手上,你左手开始是空的。每次从右手拿一张牌,与左手已有的排从右到左的进行比较,并插在正确的位置,则左手在每一次插入后均是排好序的。直到右手没有牌时,则终止,此时全部排序完成。

2.算法描述:

输入:含有n个待排序的数组A。(a[1]、a[2]、...、a[n]);

输出:含有n个已排序的数组A。(a`[1]、a`[2]、...、a`[n]);

3.伪码表示:

INSERTION-SORT(A) //输入:待排序数组A

for j <-- 2 to length[A]

do key <-- A[j]

》对每一个待排序数组的元素,将其与已排序数组的元素从右向左的比较,找到合适位置时停止比较并插入

i <-- j-1

while(i > 0 and A[i] > key)//“从右向左比较,找到合适位置停止”包含两层意思:1.向左递进到起始位置则停止,2.找到合适位置则停止;所以这话不妨改为:“从右到左进行比较,到达起始位置或者找到合适位置时停止。(未到达且未找到时不停止)”

A[i+1] <-- A[i] /

i <-- i-1

A[i+1] <-- key

4.算法正确性证明:

先引入所谓"loop invariant'的概念:一般而言,用这个式子表示希望得到的结果,如果在循环的每一步,这个式子都是正确的,那么循环结束后,这个式子也正确,并得到了期望的结果.(有人建议译为循环不变性,因为这是种性质不一定是式子)

循环不变式证明遵循以下三步:1.初始化 2.保持 3.终止

先假设的循环不变式为:在每轮循环开始前,A[1]..A[j-1]是排好序的。

1.初始化:对于for循环就是在第一次测试之前,这里即是在j赋值为2并且在测试j<=length[A]之前。可以看到,这时j=2,此时A[1]是单个元素,是排好序的,满足。

2.保持:假设在某轮循环(j = k时)开始前,循环不变式成立,即A[1]..A[k-1]有序,则在下一轮循环(j = k+1)开始前,已经执行了循环部分,此时A[1]..A[k]成立,满足。

3.终止:j=n+1时,A[1]..A[j-1]即A[1]..A[n]有序,成立。

5.算法分析:

对于算法分析,需要一个模型,我们设计的是一种单处理器的随机存取机模型,指令一条条执行,没有并发。在算法分析中,实际要考虑的因素往往很多,而我们为了方便,只考虑大的因素,小的因素忽略掉。

现在开始分析。

先通过代码标注执行时间:

INSERTION-SORT(A)       costtimes

for j <-- 2 to length[A]c1n

do key <-- A[j]c2n-1

》对每一个待排序数组的元素,将其与已排序数组的元素从右向左的比较,找到合适位置时停止比较并插入

i <-- j-1 c4n-1

while(i > 0 and A[i] > key)c5求和(j=2..n)*t(j)//由于while测试次数不定,故设为t(j)

A[i+1] <-- A[i] c6求和(j=2..n)*(t(j)-1)

i <-- i-1                c7求和(j=2..n)*(t(j)-1)

A[i+1] <-- keyc8n-1

所以T(n)=c1n+c2(n-1)+c4(n-1)+c5(求和(j=2..n)*t(j))+c6(求和(j=2..n)*(t(j)-1))+c7(求和(j=2..n)*(t(j)-1))+c8(n-1)

最好情况:即当数组本身就有序,则A[i] <= key,只测试一次,所以t(j)=1,化简得T(n) = (c1+c2+c4+c5+c8)n-(c2+c4+c5+c8),所以是满足@(n)的(@表示sita)

最坏情况:即循环j-1次,亦即while处测试j次,此时T(n) = (...)n^2+(...)n-(...)

在实际的算法分析中,若平均情况不好估计,一般分析的是最坏情况, 因为最坏情况一般与平均情况一样差,比如这里平均情况是:对于一个插入元素A[j],有一半元素小于它一半大于它,那么t(j) = j/2,亦得一个二次函数,故运行效率相等。

但有时我们仍然对平均情况或期望感兴趣,那么我们可以根据后面会介绍的概率分析技术来进行分析。

下面我们可对执行时间进行简化,因为考虑大规模输入时,相对于增长率来说,系数是次要的,故这里可以改写成:@(n^2)。不解释了吧。

6.设计更好的算法

我们既然分析了算法,就应该找出时间耗费的地方,根据其他的技术或思想来设计更好的算法。开始吧。

我们知道插入排序使用的思想是增量式的设计,还有一种常见的思想则是分治法。

分治法的框架是:1.分解 2.解决3.合并

根据这个框架我们设计出合并排序算法,同样遵循步骤:

1.实际情景:略

2.算法描述:略

3.伪码表示:略

4.算法正确性证明:增量式设计的算法如插入排序使用的证明方法是循环不变式;而对于这种分治法则采用递归主定理来证明即可。

5.算法分析:(可以画递归树来分析)

执行时间写成一般表达式是:

T(n)= @(1)n<=c时

aT(n/b)+D(n)+C(n)否则

解释如下:当规模小于c时,可以直接解决,执行常量时间;否则分解为a个子问题,每个子问题大小是原问题的1/b,D(n)是分解所用时间,C(n)是合并所用时间。

针对这个特定问题,c=1,a=b=2,D(n) = @(n),C(n) = @(n)

不用主定理的话,可以画出递归树来看,可以得到T(n) = cn * (lg n +1)

故T(n) = @(nlgn)

以上就是我对第二章的主线和内容总结。

下面是实现:

插入排序:

#include <iostream>
using namespace std;
void InsertionSort(int A[], int n){ //输入含有n个数的数组A
	int length_A = n;
	for(int j=1; j<=length_A-1; j++){ //从右边挨个取数进行迭代比较
		int key = A[j];
		int i = j-1;
		while(i>-1 && A[i]>key){
			A[i+1] = A[i];
			i = i-1;
		}
		A[i+1] = key;
	}
}
//测试
int main(){
	int A[]={10,9,4,7,8,3,1};
	InsertionSort(A, 7);
	for(int i=0; i<7; i++)
		cout<<A[i]<<" ";
	return 0;
}

合并排序:

#include <iostream>
using namespace std;
#define SENTINEL 10000; //用一个比所有数字都大的值作为哨兵
void Merge(int* A, int p, int q, int r){
	int n1 = q-p+1;
	int n2 = r-q;
	int *L = new int[n1+1];
	int *R = new int[n2+1];
	for(int i=0; i<=n1-1; i++)
		L[i] = A[p+i-1];
	for(int j=0; j<=n2-1; j++)
		R[j] = A[q+j];
	L[n1] = SENTINEL;
	R[n2] = SENTINEL;
	int i = 0;
	int j = 0;
	for(int k=p-1; k<=r-1; k++){
		if(L[i]<=R[j]){
			A[k] = L[i];
			i++;
		}else{
			A[k] = R[j];
			j++;
		}
	}
	delete[] L;
	delete[] R;
}
void MergeSort(int* A, int p,int r){
	if(p<r){
		int q = (p+r)/2;
		MergeSort(A, p, q);
		MergeSort(A, q+1, r);
		Merge(A, p, q, r);
	}
}
int main(){
	int A[]={10,9,4,7,8,3,1,15,12};
	int length_A = 9;;
	MergeSort(A, 1, length_A);
	for(int i=0; i<length_A; i++)
		cout<<A[i]<<" ";
	return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值