一题多做
从不同立场、不同视角看问题,会得到不同认识;用不同分析方法、不同设计方案,会得到不同代码。从提高编程能力角度讲,对于同一个题目,尝试几种不同的设计思路,往往比漫不经心地做几个题目来得有效。下面用许多教程中都使用的题目“打印杨辉三角形”为例,介绍有关的分析、设计思路。
[例题]输出下述的“杨辉三角形”的前若干行(不超过15行)
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
……
[分析1]
*可以使用二维数组存储三角形中的数据;
*每一行的数据个数比上一行多一个;
*每一行最前面的的数据是1,最后面的数据(在对角线上)也是1;
*其它数据是上一行左上方和正上方数据之和;
*……
在分析过程中,我们可以列出许多我们能观察到的东西,但并非列出的东西在设计中有用,我们应该抓住一些最基本的、最直观的东西,发现规律、指导设计。比如有些人还观察到:
*将每一列视为数列,第一列衡为1,第二列相邻数据值差1,第三列相邻数据值差为2、3、4、…
如何在分析过程中,抓住最重要最本质的东西,往往需要理论与实践相结合。
#include <stdio.h>
int Yang[20][20];
/*存储杨辉三角形的二维数组,因为二维数组不容易在函数间传递,所以定义为全局形式。但从软件工程的角度上认识,这并不是一种良好的程序设计风格,有能力的话该当不使用全局变量或全局数组*/
void printit(int line); /*函数原型,输出生成的数据*/
void triangle(int line) /* 生成共line行的杨辉三角形*/
{
int i,j;
for(i=0; i<line; i++) /*逐行*/
{
for(j=0; j<=i; j++) /*第i行有i+1个元素*/
{
if(j==0 || j==i) /*位置在第0列或在对角线上*/
{
Yang[i][j]=1; /*每一行的第0个元素和第i个元素值为1*/
}
else
{
Yang[i][j] = Yang[i-1][j-1] + Yang[i-1][j];
/*其它元素的值是上一行左上和正上元素值之和*/
}
}/*for(j)*/
} /*for(i)*/
} /*End of void triangle(int line) */
/* 主函数 */
int main( )
{
int line;
printf(“Please input the number of lines”);
scanf(“%d”,&line); /*运行时获取要打印的行数*/
triangle(line); /*生成杨辉三角形*/
printit(line); /*打印生成的三角形*/
system(“pause”);
return 0;
}
void printit(int line) /* 打印杨辉三角形 形参line : 行数*/
{
int i,j;
for(i=0; i<line; i++) /*打印 共line行 */
{
printf(“/n”); /*换行控制*/
for(j=0; j<=i; j++) /*每一行打印i+1个元素值*/
{
printf(“%5d”,Yang[i][j]);
} /* for(j)*/
} /*for(i)*/
printf(“/n”);
} /*End of void printit(int line)*/
[评价1 ]
*二维数组的存储利用率不高,刚刚超过50%,数组的右上部没有数据
*按外部标识符形式在各函数间传递二维数组信息,无法达到函数通用、复用
[分析2]压缩掉所有无用的存储空间,将二维的三角阵改成一维数组
1 |
1 |
1 |
1 |
2 |
1 |
1 |
3 |
3 |
1 |
1 |
6 |
4 |
4 |
1 |
1 |
… |
第0行 |
第1行 |
第2行 |
第3行 |
第4行 |
k |
如何构造一维数组中的元素下标k与二维数组中行i、列j之间的关系成为关键。初等代数的知识足以帮助我们解决这个问题:三角形第i行之上有i行(从第0行至第i-1行),元素的总个数为
1+2+3+…+i = i*(i+1)/2
而第i行第j列左有j个元素,则二维数组中第i行第j列元素在一维数组中的位置
k = i*(i+1)/2+j
在这类问题里,我们将讨论中的二维数组称为数据的逻辑结构,而把对应的一维数组称为数据的物理结构,则i、j称为逻辑位置,k称为物理位置。(请读者自行尝试一下,反向导出已知一维数组中物理位置为k的元素在二维数组中的逻辑位置i、j是什么)。
#include <stdio.h>
#define N 20
void printit(int arr[],int line);
/* 生成杨辉三角形的一维数组,Yang[ ] 一维存储数组,line 行数*/
void triangle(int Yang[], int line)
{
int i,j;
for(i=0; i<line; i++)
{
for(j=0; j<=i; j++)
{
Yang[i*(i+1)/2+j] = ((j==0 || j==i))
? 1
: Yang[(i-1)*i/2+j-1]+Yang[(i-1)*i/2+j];
/*利用逻辑位置计算物理位置,并利用条件表达式简化了if语句*/
}
}
}
/* 主函数,构造一维的存储数组,
int main()
{
int Yang[(N*N+N)/2]; /*定义一维物理结构的数组*/
int line; /*要打印的二维数组行数*/
printf(“Please input the number of lines(<%d): “,N);
scanf(“%d”,&line);
triangle(Yang, line); /*生成数组*/
printit(Yang, line); /*打印三角形*/
system(“pause”);
return 0;
}
/* 打印三角形,数据在一维数组Yang中,共line行 */
void printit(int Yang[], int line)
{
int i,j;
for(i=0; i<line; i++)
{
printf(“/n”);
for(j=0; j<=i; j++)
{
printf(“%5d”,Yang[i*(i+1)/2+j]); /*用逻辑位置计算物理位置*/
}
}
printf(“/n”);
}
[评价2]
*典型的用时间换空间,用逻辑位置计算物理位置不那么直观,但存储空间利用率是100%
*过份追求代码的简洁,可读性下降
但是读者有兴趣的话,这种空间位置的变换,,还是应该多做练习,有利于提高空间结构的认识,比如说前面提到的由一维数组的下标k求对应的二维数组下标i和j;又如三角阵为对角线右上部分,如何变换成一维数组?
[分析3]
还可以进一步提高存储空间的利用率:每一行数据的计算只依赖上一行的数据,因此在计算中只需保留最新生成的一行供下一行计算即可,而下一行直接利用同一块存储空间。
1 |
1 |
1 |
1 |
下一行 |
第i行 |
。。。。。。 |
从后向前计算 |
#include <stdio.h>
void printit(int arr[],int line);
/*计算第i行数据,Yang为一维(一行)数组*/
void triangle(int Yang[], int i)
{
int j;
Yang[i] = 1; /*第i行的最后一个元素位置为i (对角线)*/
for(j=i-1; j>0; j–) /*从后向前依次计算,第0个数据不必重算*/
{
Yang[j] = Yang[j-1]+Yang[j]; /*=左为第i行数据,=右实际是上一行数据*/
}
}
/*主函数,存储空间满足最后一行的需要即可*/
int main()
{
int Yang[20];
int line;
printf(“Please input the number of lines(<%d): “, 20);
scanf(“%d”,&line);
printit(Yang, line); /*将每一行数据的生成放在打印过程中控制*/
system(“pause”);
return 0;
}
/*打印共line行的三角形*/
void printit(int Yang[], int line)
{
int i,j;
for(i=0; i<line; i++) /*逐行生成并打印数据*/
{
printf(“/n”);
triangle(Yang,i); /*生成第i行数据*/
for(j=0; j<=i; j++) /*打印第i行数据*/
{
printf(“%5d”,Yang[j]);
}
}
printf(“/n”);
}
[评价3]
这是典型的递推—迭代法,从时间效果上看,用上一行的数据递推出新一行的数据;从存储空间上,重复使用(迭代)同一组空间(关于递推和迭代的准确意义,笔者不做深入讨论)。
[分析4]
事实上许多读者都知道杨辉三角形的任一行数据是k次二项式公式展开后的各项系数,如
(a+b)4 = 1*a4 + 4*a3*b + 6*a2*b2 + 4*a*b3 + 1*b4
特别的,k=0时
(a+b)0 = 1
而这些系数还可以用组合数CKi表示:
CKi = K!/(i!*(K-i)!)
K是二项式的冖次,也是杨辉三角形的第K行;
i是展开式的项次,也是杨辉三角形第K行的项次(K和i都从0开始,从而可见语言中数组元素的下标从0开始,在数学上是正确的)。
组合公式的计算中如果直接使用阶乘则存在着大量冗余计算,并且先计算K!可能值要溢出(超出数据类型的表示范围)。以C72的计算为例,导出一个计算量小的过程:
C72 =7!/(2!*(7-2)!)=(7*6*5*4*3*2*1)/((2*1)*(5*4*3*2*1))
=(7*6)/(2*1)=(7/2)*(6/1)
可以看出:
*计算步数可控制在2次(2和7-2两个数中的较小数)
*每一步计算值不会象阶乘那样增长很快
*注意整数除法带来的误差
[设计4]
#include <stdio.h>
long Com(int m,int n); /*组合公式计算*/
void printit(int line)
{
int i,j;
for(i=0; i< line; i++)
{
printf(“/n”);
for(j=0; j<=i; j++)
printf(“%5d”,Com(i,j)); /*直接输出组合数*/
}
}
int main()
{
int line;
printf(“Please input the number of lines: “);
scanf(“%d”,&line);
printit(line);
printf(“/n”);
system(“pause”);
return 0;
}
long Com(int m, int n)
{
int i, min;
double p=1; /*不用整数类型,避免整除的误差*/
min = (n < (m-n)) ? n : m-n;
if(min == 0 )
return 1; /* Cm0 或 Cmm */
n = min; /*使用形参变量作为工作变量不是好习惯*/
for(i=0; i<min; i++)
{
p = p * m / n ;
m–;
n–;
}
return (long)p;
}
[评价4]这是典型的解析法,从时间效果上看,由于存在着大量的重复运算(如同一行数据是对称的,不必重复计算,等等),效率极低;从存储角度上看,没有使用数组,节省了运行空间—-又是一个以时间换空间的例子。
[提示]1.我们调整一下运算顺序:(7/2)*(6/1)=(7/1)*(6/2)=7/1*6/2,会发现即使使用整数运算也不会出现整除误差(这涉及到一个代数原理:连续的n个整数积一定能被n整除)
2.当计算出C72(=(7*6)/(2*1)), C73(=(7*6*5)/(3*2*1))应该怎样计算,能否利用已知结果进行计算,作为本题目的扩展性问题,请读者自行考虑。
一般情况下,程序员应当做出选择,将时间—空间性能控制在自己能够掌控的范围内,—-读者应当在学习了《数据结构》之后进行更深入的讨论。