算法分析与设计及案例习题解析.docx
《算法分析与设计及案例习题解析.docx》由会员分享,可在线阅读,更多相关《算法分析与设计及案例习题解析.docx(36页珍藏版)》请在冰豆网上搜索。
![算法分析与设计及案例习题解析.docx](https://file1.bdocx.com/fileroot1/2023-1/27/ac1ce1bf-13dc-4edf-97ff-941da8bbf613/ac1ce1bf-13dc-4edf-97ff-941da8bbf6131.gif)
算法分析与设计及案例习题解析
习题解析
第1章
1.解析:
算法主要是指求解问题的方法。
计算机中的算法是求解问题的方法在计算机上的实现。
2.解析:
算法的五大特征是确定性、有穷性、输入、输出和可行性。
3.解析:
计算
的算法,其中n是正整数。
可以取循环变量i的值从1开始,算i的平方,取平方值最接近且小于或者等于n的i即可。
4.解析:
可以使用反证法,设i=gcd(m,n)=gcd(n,mmodn),则设m=a*i,n=b*i,且a与b互质,这时mmodn=(a-x*b)*i,只需要证明b和a-x*b互质,假设二者不互质,可以推出a与b不互质,因此可以得到证明。
5.解析:
自然语言描述:
十进制整数转换为二进制整数采用“除2取余,逆序排列”法。
具体做法是:
用2整除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为0时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。
流程图:
如图*.1
图*.1十进制整数转换成二进制整数流程图
6.解析:
a.如果线性表是数组,则可以进行随机查找。
由于有序,因此可以进行折半查找,这样可以在最少的比较次数下完成查找。
b.如果线性表是链表,虽然有序,则只能进行顺序查找,从链表头部开始进行比较,当发现当前节点的值大于待查找元素值,则查找失败。
7.解析:
本题主要是举例让大家了解算法的精确性。
过程中不能有含糊不清或者二义性的步骤。
大家根据可行的方式总结一下阅读一本书的过程即可。
8.解析:
数据结构中介绍的字典是一种抽象数据结构,由一组键值对组成,各个键值对的键各不相同,程序可以将新的键值对添加到字典中,或者基于键进行查找、更新或删除等操作。
由于本题已知元素唯一,因此大家可以据此建立一个自己的字典结构。
实现字典的方法有很多种:
∙最简单的就是使用链表或数组,但是这种方式只适用于元素个数不多的情况下;
∙要兼顾高效和简单性,可以使用哈希表;
∙如果追求更为稳定的性能特征,并且希望高效地实现排序操作的话,则可以使用更为复杂的平衡树。
在字典之上的主要操作可以有:
创建操作,添加操作,删除操作,查找操作,以及必要的字典维护操作。
第2章
1.解析:
根据本章所述,递归算法和非递归算法的数学分析方法分为5个步骤。
2.解析:
本题相当于对多项式找“主项”,也就是在除去常系数外,影响函数值递增速度最快的项。
a)
b)
c)
,c为常数
d)
e)
3.解析:
本题中如果手套分左右手,则最优情况选2只,最差情况选12只。
本题中如果手套不分左右手,则最优情况仍然选2只,最差情况选4只。
从本题的初衷推测设置题目应该是分左右手的手套,在考虑颜色的情况下,选择一双进行匹配。
4.解析:
本题的一般解法可以使用高等数学中求二者比值的极限来确定结果。
a)相同b)第一个小c)二者相同d)第一个大e)二者相同f)第一个小
5.解析:
a)
b)
c)
d)
6.解析:
参见本章例2.7。
第3章
1.解析:
蛮力法主要依靠问题的定义,采用简单直接的求解方法。
由此决定了蛮力法是解决问题的最简单也是最普遍的算法,同时其经常作为简单问题求解的方法和衡量其他算法的依据。
2.解析:
2,6,1,4,5,3,2
选择排序:
|2614532i=0:
min最后得2,交换二者。
1|624532i=1:
min最后得2,交换二者。
12|64532i=2:
min最后得6,交换二者。
122|4536i=3:
min最后得5,交换二者。
1223|546i=4:
min最后得5,交换二者
12234|56i=5:
min最后得5。
122345|6结束。
冒泡排序:
2614532
214532|6i=0:
最大值6就位。
12432|56i=1:
第二大值5就位。
1232|456i=2:
第三大值4就位。
122|3456i=3:
第四大值3就位。
12|23456i=4:
第五大值2就位。
1|223456i=5:
第六大值2就位,剩余的1也就位,排序结束。
3.解析:
选择排序不稳定,如3.1.1节例子:
4,4,2。
冒泡排序稳定。
4.解析:
如2题例子,到i=4时就没有发生交换的活动了。
这时可以在冒泡排序的交换部分加入一个布尔变量,如本次循环中没有发生交换,则以后的扫描就可以不进行。
5.解析:
如果n个点共线,则其最近对只需要考察相邻的点对,因此在对点进行按行坐标排序后,只需要两两计算相邻两点距离,就可以找到最近对。
6.解析:
所有的过程与寻找二维空间中的最近点对类似,只是计算距离的公式变为:
sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y)+(p1.z-p2.z)*(p1.z-p2.z)
使用循环计算任意两个点之间的距离,然后记录最小值即可。
类似的,如果推广到n维空间,需要考虑空间中任意两点的距离计算公式,同样计算每两个点之间的距离,并记录最小距离即可。
7.解析:
a)线段的凸包为本身,极点为两个端点。
b)正方形的凸包为本身,极点为四个顶点。
c)正方形的边界不是凸包,凸包为正方形(包括边界和内部)。
d)直线的凸包为本身,没有极点。
8.解析:
哈密顿回路的穷举查找算法,首选选择起点(图中任意一个点开始),之后不断寻找下一个点,下一个点应该满足:
1)不在已经走过的点中;
2)与上一个点有直连路径。
如果这样能找到回到起点的路径,并且遍历所有点,则为一条哈密顿回路。
然后依次进行下一个可行点的选择。
9.解析:
生成给定元素的一个排列,通过连续比较它们之间的元素,检查它们是否符合排序的要求。
如果符合就停止,否则重新生成新的排列。
最差情况生成排列的个数是
,每趟连续元素比较次数为n-1次。
所以效率类型为
。
第4章
1.解析:
假定把16枚硬币上网例子看作一个大的问题。
(1)把这一问题分成两个小问题,随机选择8个硬币作为第一组称为A组,剩下的8个硬币作为第二组称为B组;这样,就把16个硬币的问题分成两个8个银币的问题来解决;
(2)判断A组和B组中是否有伪币,可以使用仪器比较A组硬币和B组硬币的重量;假如两组硬币重量相等,则可以判断伪币不存在;假如两组硬币重量不相等,则存在伪币,并且可以判断它位于较轻的那一组硬币中;
(3)假设B是轻的那一组,因此再把它分成两组,每组有4个硬币,称其中一组为B1,另一组为B2,比较这两组,肯定有一组轻一些,假设B1轻,则伪币在B1中,再将B1分为两组,每组有两个硬币,称其中一组为B1a,另一组为B1b。
比较这两组,可以得到一个较轻的组,由于这个组织有两个硬币,因此不必再细分。
比较组中两个硬币的重量,可以立即知道哪个硬币轻一些,轻币就是要找的伪币;
最终,比较次数为4次。
2.解析:
逆序对是指在序列{a0,a1,a2...an}中,若aij),则(ai,aj)上一对逆序对。
而逆序数是指序列中逆序对的个数。
例如:
123是顺序,则逆序数是0;132中(2,3)满足逆序对的条件,所以逆序数只有1;321中(1,2)(1,3)(2,3)满足逆序对,所以逆序是3。
由定义不能想象,序列n的逆序数范围在[0,n*(n-1)/2],其中顺序时逆序数为0,完全逆序时逆序数是n*(n-1)/2。
对于一个数组s将其分为2个部分s1和s2,求s1和s2的逆序对个数,再求s1和s2合并后逆序对的个数:
这个过程与merge排序的过程是一样的,可以使用merge排序求得。
代码如下:
//a为字符数组,len为字符数组的长度
intnumber=0;//number表示逆序对的个数
voidcopy(char*dest,char*src,intl,intr)
{
while(l<=r)
{
dest[l]=src[l]; l++;
}
}
voidmergeSort(char*a,intsize)
{
char*b=(char*)malloc(sizeof(char)*size);
mergePass(a,b,0,size-1);
free(b);
}
voidmergePass(char*a,char*b,intl,intr)
{
intm;
if(l{
m=(l+r)/2;
mergePass(a,b,l,m);
mergePass(a,b,m+1,r);
merge(a,b,l,m,r);
copy(a,b,l,r);
}
}
voidmerge(char*a,char*b,intl,intm,intr)
{
inti=l,j=m+1;
while(i<=m&&j<=r)
{
if(a[i]<=a[j]) b[l++]=a[i++];
else
{
b[l++]=a[j++];
number+=m-i+1;
}
}
while(i<=m) b[l++]=a[i++];
while(j<=r) b[l++]=a[j++];
}
3.解析:
当序列A[1..n]中的元素的个数n=2时,通过直接比较即可找出序列的第2大元素。
当n>2时,先求出序列A[1..n-1]中的第1大元素x1和第2大元素x2;然后,通过2次比较即可在三个元素x1,x2和A[n]中找出第2大元素,该元素即为A[1..n]中的第2大元素。
SecondElement(A[low..high],max1,max2)
{//假设主程序中调用该过程条件为high-low>=2
if(high-low==2)
{if(A[low]else{max2=A[high];max1=A[low];}
}
else
{SecondElement(A[low..high],x1,x2);
if(x1<=A[n]){max2=max1;max1=A[n];}
else
if(x2>=A[n]){max2=x2;max1=x1;}
else{max2=A[n];max1=x1;}
}
}
该算法的时间复杂度满足如下递归方程:
T(n)=T(n-1)+2;T
(2)=1。
解得T(n)=2n-3。
4.解析:
如果在算法SELECT执行中每次标准元素都恰好选取的是序列中的真正中值元素,那算法的行为与二分查找算法类似。
最坏情况下,每次在原序列的一半上进行查找。
此时如果标准元素得到时间为T(n);如果标准元素得到时间为T
(1),则算法的最坏情况时间与二分检索一样,为T(logn)。
5.解析:
对于两棵二叉树T1和T2,若其根结点值相同,且其左右子树分别对应相同,则T1=T2,否则T1≠T2。
其描述如下:
BooleanBTEQUAL(BTT1,BTT2)
{
if(T1==NULL&&T2==NULL)returnTrue ;//均为空树
if(T1&&T2&&T1.data==T2.data&&BTEQUAL(T1.lchild,T2.lchild)&&BTEQUAL(T1.lchild,T2.lchild))returnTrue;
returnFalse;
}
6.解析:
快速分类算法是根据分治策略设计出来的算法。
其关键步骤就是“划分”:
根据某个元素v为标准,将序列中的元素重新整理,使得整理后的序列中v之前的元素都不大于v,而v之后的元素都不小于v。
此时,元素v即找到了其最终的位置。
要得到序列的排序结果,再只需对v之前的元素和v之后的元素分别排序即可,这可通过递归处理来完成。
7.解析:
当序列中的元素都相同时,每次执行算法SPLIT时,仅出现一次元素交换,即将序列的第一个元素与最后一个元素交换,且划分元素的新位置为该序列的最后一个位置。
因此,在元素均相同的数组A[1..n]上,算法QUICKSORT的执行特点为:
每次划分后剩下A[1..n-2]未处理,…,第n-1此划分后剩下A[1](已有序)。
在这类实例上,算法的执行时间为:
(n-1)+(n-2)+…+2+1=(n2),属于最坏的情况。
此外,算法最后所得结果中各元素顺序如下:
A[2],A[3],A[4],…,A[n],A[1](这里,A[i]表示输入时的第i个元素,i=1,2,…,n)。
8.解析:
算法QUICKSORT所需要的工作空间是指其递归执行时所需要的栈空间,其大小与递归调用的深度成比例。
考虑算法在n个元素上执行时的递归调用树,树最高可为T(n),最矮可为T(logn)。
所以,算法所需的工作空间在T(logn)到T(n)之间变化。
第5章
1.解析:
0个元素的幂集为空集;
1个元素的幂集为空集和本身;(可以看做空集并空集加入元素的集合)
2个元素的幂集为1个元素的幂集和该幂集中每个集合加入新元素的集合的并集;
类似的继续下去可以生成n个元素的幂集。
2.解析:
插入排序:
2,6,1,4,5,3,2
2|614532i=1:
A[1]插入
26|14532i=2:
A[2]插入
126|4532i=3:
A[3]插入
1246|532i=4:
A[4]插入
12456|32i=5:
A[5]插入
123456|2i=6:
A[6]插入
1223456|结束
3.解析:
对照给出的算法,进行链表插入排序。
这时,每次进行插入时均需要从链表头部节点开始,寻找插入位置。
因此要设置相邻的两个指针,用于寻找插入位置。
具体请大家自己完成。
4.解析:
折半插入排序:
//输入:
待排序数组A[0..n-1]
//输出:
排好序后的数组A[0..n-1]
voidISort(&A[0..n-1])
{
/*v用来保存待插入元素;s,t,k分别是插入位置寻找时使用的指针,s指向头,t指向尾,k指向中间。
*/
for(i=1;i<=n-1;i++)
{
v=A[i];
s=0;
t=i;
k=
;
while(t>=s&&v<>A[k])
{
if(v<>A[k])
{
s=k+1;
k=
;
};
else
{
t=k-1;
k=
;
};
};
if(A[k]for(j=i-1;j>=k;j--)
A[j+1]=A[j];
A[k]=v;
};
};
最差效率为:
。
5.解析:
用减一法进行拓扑排序,每次删除入度为0的结点即可,所有可能情况共有7种:
d-a-b-c-g-e-f;d-a-b-c-g-f-e;d-a-b-g-c-e-f;
d-a-b-g-c-f-e;d-a-c-b-g-e-f;d-a-c-b-g-f-e;
d-a-b-g-e-c-f。
同样,可以用深度优先遍历时,出栈顺序的逆序作为拓扑排序。
6.解析:
字典序列算法是一种非递归算法。
而它正是STL中Next_permutation的实现算法。
它的整体思想是让排列成为可递推的数列,也就是说从前一状态的排列,可以推出一种新的状态,直到最终状态。
比如说,最初状态是12345,最终状态是54321。
其实我觉得这跟我们手动做全排列是一样的。
首先是12345,然后12354,然后12435,12453....逐渐地从后往前递增。
看看算法描述:
首先,将待排序列变成有序(升序)序列。
然后,从后向前寻找,找到相邻的两个元素,Ti如果没有找到,则说明整个序列已经是降序排列了,也就是说到达最终状态54321了。
此时,全排列结束。
接着,如果没有结束,从后向前找到第一个元素Tk,使得Tk>Ti(很多时候k=j),找到它,交换Ti跟Tk,并且将Tj到Tn(Tn是最后一个元素)的子序列进行倒置操作。
输出此序列。
并回到第二步继续寻找ij。
例如839647521是数字1~9的一个排列。
从它生成下一个排列的步骤如下:
自右至左找出排列中第一个比右边数字小的数字4839647521
在该数字后的数字中找出比4大的数中最小的一个5839647521
将5与4交换839657421
将7421倒转839651247
所以839647521的下一个排列是839651247。
839651247的下一个排列是839651274。
以下是C语言的代码:
#include
#defineMAX1000
intn=4;
intset[MAX]={1,2,3,4};
intflag=0;
//swapaandb
voidswap(int*a,int*b)
{
inttemp=*a;
*a=*b;
*b=temp;
}
//reversearangeofaset
voidreverse(intstart,intend)
{
intindex_r=0;
intnew_end=end-start;
for(index_r=0;index_r<=new_end/2;index_r++)
swap(&set[index_r+start],&set[new_end-index_r+start]);
}
voidset_print()
{
intindex;
for(index=0;indexprintf("%d",set[index]);
printf("\n");
}
//findoutallofthepermutationinthesetwithfirstelement1ton.
voidfind_next()
{
set_print();
intindex_i,index_j,index_k;
flag=0;
for(index_i=n-2;index_i>0;index_i--)
if(set[index_i]{
index_j=index_i+1;
flag=1;
break;
}
if(flag==0)
return;
for(index_k=n-1;index_k>0;index_k--)
if(set[index_k]>set[index_i])
break;
swap(&set[index_i],&set[index_k]);
reverse(index_j,n-1);
find_next();
}
intmain()
{
intcount=1;
inti=n-1;
while(i>=0)
{
set[0]=count++;
find_next();
swap(&set[0],&set[i]);
reverse(1,n-1);
i--;
}
return0;
}
7.解析:
说到汉诺塔问题,首先想到的是最经典的递归解法。
在求格雷码的方法中,提到可以观察格雷码每一次改变的位数和汉诺塔每次移动的盘子的编号,从而产生一种不需要递归和堆栈的汉诺塔解法。
在生成格雷码的算法中,依次改变的位数是最低位和从右往左数第一个1所在位的左一位,对应汉诺塔的盘子就是最小的盘子和中间某个盘子。
最小的盘子有两种可能的移动方案,其他的盘子只有一种可能。
对于最小盘子移动到的柱子的解决方法是,根据观察,当盘子总数是奇数时,最小盘子的位置依次是“3->2->1->3->2->1...”;当总数是偶数时,这个顺序是“2->3->1->2->3->1...”。
据此从格雷码到汉诺塔的一种对应解决方案就产生了。
如下是非递归方法的代码。
intmain()
{
chardigit[MAX];
intposition[MAX];
inti,j;
for(i=0;idigit[i]='0';
position[i]=1;
}
inteven=YES;
intcircle[3];
circle[2]=1;
if(n%2==0){
circle[0]=2;
circle[1]=3;
}
else{
circle[0]=3;
circle[1]=2;
}
i=0;
intm=0;
while(LOOP){
m++;
if(m>20)
break;
if(even){
cout<"<position[0]=circle[i];
i=(++i)%3;
FLIP_DIGIT(digit[0]);
}
else{
for(j=0;j;
if(j==n-1){
break;
}
FLIP_DIGIT(digit[j+1]);
cout<"<<6-position[j+1]-position[0]<position[j+1]=6-position[j+1]-position[0];
}
FLIP(even);
}
cout<<"========================="<system("pause");
return0;
}
8.解析:
n
m
52
34
26
68
13
136
6
272
+136
3
544
1
1088
+544+136
1768
=1088+544+136
9.解析:
顺序查找的平均效率约为:
。
最高效的排序算法其效率为:
。
因此,如果只做一次查找,不需要进行排序后再折半查找。