1. 题目是什么,为什么每个学C的人都绕不开它
杨辉三角形,也叫帕斯卡三角形,在C语言学习里几乎是“入门必写”的一道程序题。很多高校的C语言作业、期末上机题、甚至一些公司的笔试里都能看到它的影子:输入一个行数n,输出对应的杨辉三角。我见过太多刚开始学C的朋友,对着这个题目一脸懵——明明数学课上见过杨辉三角,但就是不知道从哪下手写代码。
其实杨辉三角形的输出,表面上是在考“循环嵌套”,本质上是在考三件事:数组的下标操作、递推关系的建模、格式化输出的控制。很多新手不是不会循环,而是不知道每一行的数是怎么“算”出来的,更不知道输出的时候怎么让三角形对齐不乱掉。这篇就把这个题目从头到尾拆开,从数学规律、代码实现、排版细节到常见翻车现场,一次说清楚。
先说清楚适合谁看:如果你刚学完C语言的数组和循环,想找一个经典题目练手,这篇很适合你;如果你已经会写了,但只知道背代码,不知道背后的优化思路,这篇也能帮你把层次往上提一提。题目不大,但能聊的细节其实很多。
1.1 杨辉三角形到底长什么样
先把图形拉出来看一眼。5行的杨辉三角形长这样:
1 1 1 1 2 1 1 3 3 1 1 4 6 4 1它的规律,说简单也简单:每行第一个和最后一个数字都是1;中间的数字,是上一行相邻两个数字之和。比如第三行的“2”,来自第二行的“1+1”;第四行的两个“3”,来自第三行的“1+2”和“2+1”;第五行的“6”,来自第四行的“3+3”。
这个规律在数学里叫“递推关系”,在C语言里就对应着一行赋值代码:
a[i][j] = a[i - 1][j - 1] + a[i - 1][j];看,规律本身就这么直白。所有的复杂度,其实都藏在“怎么把这句话放进循环里”以及“怎么把算好的数据摆得好看”上。
1.2 出题人真正想考你的三件事
很多学生以为这题考的是“会不会写杨辉三角”,其实考的是更底层的三种能力。
第一,下标与边界意识。递推公式里出现了a[i-1]、a[j-1]这种下标,意味着循环的时候必须注意边界。新手最容易犯的错就是数组越界,或者把边界条件写错,导致第一列、对角线上的1没有被正确赋值。这一块考察的是你对二维数组内存布局和下标范围的理解。
第二,数学规律到代码的映射能力。杨辉三角形的递推公式是一个很典型的“动态规划”雏形——当前结果依赖之前的结果。能把数学式子翻译成循环代码,就说明你具备了初步的建模能力。这道题的价值,不在于让你背一个算法,而在于让你亲身体验“规律→变量→循环→输出”的完整链条。
第三,格式化输出的控制能力。也算完了,循环也写完了,结果一运行,三角形歪七扭八。这是输出控制的问题。printf里%d、%2d、%-4d,还有前面补多少空格,都有讲究。很多人栽在最后这一步,代码逻辑全对,就是输出难看,但是这恰恰是考试中容易被扣分的地方。
2. 动手前先想清楚:三种不同的解法思路
遇到这题,先别急着敲键盘。把思路理清楚,后面代码写起来会顺畅得多。针对“输出杨辉三角形”,至少有三种常规路线,每一种的思考角度和适用场景都不同。
2.1 思路一:按定义直接走递推(二维数组法)
最直观的写法:开一个二维数组,逐行逐列地套用递推公式,把整个三角形算出来,算完再统一输出。
这种方式的好处是朴素、好理解、不容易错。二维数组天然对应“行”和“列”,下标a[i][j]直接表示“第i行第j列的数”,写起来一点都不绕。对于初学者,我强烈建议第一个版本就用二维数组写,先把规律跑通再说。
需要两个循环:外层循环控制行数i,从0到n-1;内层循环控制列数j,从0到i。每次进入内层循环先检查一下:如果j==0或者j==i,直接把a[i][j]赋成1;否则用递推公式。这个“先检查再赋值”的模式,就是边界条件的处理,少了它就翻车。
2.2 思路二:空间省着用(一维数组法)
二维数组存了整个n行,但输出的时候是一行一行按顺序来的。前面的行算完、输出完,其实就不再需要了。所以理论上可以只用一个一维数组,在算下一行时原地更新,这样空间复杂度从O(n²)降到O(n)。
这个思路理解起来略微烧脑,因为一维数组更新的时候有个大坑:必须从后往前更新。为什么?因为a[j]的新值依赖的是a[j-1]和a[j]的旧值。如果你从前往后更新,等你算a[j]的时候,a[j-1]已经被这一轮算出来的新值覆盖了,那拿到的就不是上一行的数了。
我从后往前更新,算第j列时,a[j-1]和a[j]都还是上一行的旧数据,可以安全地拿来做加法。这个细节,是二维数组法向一维数组法升级时最重要的思维拐点。
2.3 思路三:一行都不存(组合数公式法)
再看远一点。杨辉三角形的第n行第k个数,其实是一个组合数C(n-1, k-1)。于是可以完全不用数组,直接对每一行的每个位置套组合数公式,算一个输出一个。
组合数的计算有一个递推技巧,可以在O(n)时间内算出整行:C(n, k) = C(n, k-1) * (n - k + 1) / k。也就是说,每一行的第一个数是1,后面的每个数都能由前一个数乘一个系数得到。
这个思路在“只输出杨辉三角形某一行”的面试变种题里非常管用,因为它不占空间、算得快,还能顺带展示你对排列组合的理解。但如果是要输出整个三角形,每次都重新计算组合数,会有大量重复计算,性价比未必高,反而没有前两种思路直观。所以思路三更适合作为一种“进阶的补充理解”存在。
3. 从零到能跑:完整代码和格式化输出的坑
思路有了,下面直接进入实操环节。为了照顾大多数初学者的阅读习惯,这一节先给二维数组的完整可运行版本,再单独讲解对齐和空格这些输出细节。
3.1 可以直接抄作业的首版代码
#include <stdio.h> int main() { int n, i, j; int a[100][100] = {0}; printf("请输入行数:"); scanf("%d", &n); for (i = 0; i < n; i++) { a[i][0] = 1; // 每行第一个数为1 a[i][i] = 1; // 每行最后一个数为1 for (j = 1; j < i; j++) { a[i][j] = a[i - 1][j - 1] + a[i - 1][j]; } } // 输出上半部分(带空格版) for (i = 0; i < n; i++) { for (j = 0; j < n - i - 1; j++) { printf(" "); } for (j = 0; j <= i; j++) { printf("%4d", a[i][j]); } printf("\n"); } return 0; }这段代码应该能直接跑通。输入5,输出如下,注意因为我把每个数字占位设置成4个字符宽,所以数字之间是“被空格撑开”的,看起来比前面的示意图宽一点:
1 1 1 1 2 1 1 3 3 1 1 4 6 4 1每个数固定占4个字符宽度,这就是%4d的作用。如果你希望数字紧凑一点,可以改成%2d或%3d,但行数稍微多几行、数字超过两位数时,间距就会变形。这个问题在后面的3.3小节再细说。
3.2 对齐里藏着的门道:左对齐和右对齐
上面代码用的是%4d,属于“右对齐”。意思是每个数字在4个字符宽的格子内靠右显示。C语言里还有左对齐的写法:%-4d,数字靠左显示,右边补空格。
两种方式打印出来的形状会差很多。因为杨辉三角是等差的对称结构,用右对齐配合前面补空格,更容易拼出一个“顶点居中”的效果。用左对齐的话,数字和数字之间的间隔天然是均匀的,但左边的起始位置会随着行号变化,整体也能拼出三角形,只是视觉风格不同。
实际做题的时候,不要死记某个格式,要理解:%4d控制的是“每个数字占据的宽度”,前面补空格的循环控制的是“每行开头的缩进”。两者配合好了,三角形才不会歪。
3.3 层数一变就乱?三角形变形的真相
很多同学发现,输入5行没问题,输入10行、15行,三角形就开始“塌方”——中间的数字挤成一团,左右不再对称。原因在于:杨辉三角形的数字会随行数增大而快速变大。第17行的中间数字已经是几千上万,四位数、五位数了,而%4d只预留了4个字符宽度,放不下就自动拉宽,把后面数字全挤走。
这个问题没有完美的通用解法,只有相对稳妥的妥协方案。
- 如果题目限定了行数范围(比如n≤10),用
%4d就够了,两位数以内的数字刚好放下。 - 如果n可能到15以上,可以适当增大占位,比如
%6d、%8d,牺牲横向的空间换取不塌方。 - 还有一个思路:先用数学公式估算这一行最大数字的位数,再动态决定每个数字占几位。比如最大数字不超过6位数,就统一用
%7d。这个方法看起来高级,实际写起来也不难,就是在输出前先扫描一遍已经算好的数组,记录最大值,再算它有多少位。
我在实际写题时,最常用的就是“动态算位数”这个方案。做法是:在算完整个三角形后,先遍历二维数组找到最大元素,然后用一个临时变量不断除以10统计位数,最后输出时用%*d动态指定宽度。*在printf里表示“宽度由后面的参数指定”,很多人没用过,但其实非常好用:
int width = 6; // 假设最大数字是5位数,加一个空格分隔 printf("%*d", width, a[i][j]);这样无论n取多大,只要width足够,三角形永远是对称的,不会因为某个数字位数超了而变形。
4. 二刷才懂的优化:这里藏着C语言的精髓
把基础版跑通之后,别急着收工。杨辉三角形这个题目还有不少更值得琢磨的细节,这些细节才是让你从“会写”到“写得好”的关键。很多人学C,卡在“能运行”和“写得漂亮”之间,缺的就是这一层。
4.1 从后往前更新数组,为什么是关键
前面提到一维数组法时,我强调了“从后往前更新”。这里展开说说。
一维数组法的核心思路是:用一个长度为n的一维数组a,模拟滚动更新的过程。初始时a[0]=1。算下一行时,从最后一个位置开始往前算:a[j] = a[j-1] + a[j]。因为a[j-1]和a[j]此刻都还没被本轮更新覆盖,所以拿到的仍然是上一行的值,加出来的结果就是当前行的新值。
这个技巧,和“原地修改数组”“滑动窗口更新”这类进阶操作是同一个思路,在力扣或者面经里经常以“只用一个数组求杨辉三角下一行”的变种出现。理解了它,你以后遇到“滚动数组”一类的问题会轻松很多。
对比一下从前往后的错误版本:先更新a[1] = a[0] + a[1],此时a[0]没变,没事;再更新a[2] = a[1] + a[2],但这里的a[1]已经不再是上一行的1了,而是本轮刚算出的2。这样一来,a[2]就变成了2+1=3,而正确结果应该是1+1=2,直接算错。从后往前更新之所以安全,是因为更新时依赖的两个旧值都还没有被破坏。这个道理很朴素,但很多人第一次接触时都会绕进去。
4.2 单手算一行:递推公式避开全部数组
第二种优化,是“只输出第n行”的场景。比如面试里问你:不用数组,怎么打印杨辉三角形的第n行?这时候组合数递推公式就派上用场了。
第n行的组合数公式是:
C(n, k) = C(n, k-1) * (n - k + 1) / k这里的C(n, k)代表从n个元素里取k个的组合数。杨辉三角形的第n行第k个数,恰好就是C(n-1, k-1)。所以只要从C(n-1, 0)=1开始,不断套用上面的递推式,就能一行一行地推出这一行的所有数字,不需要任何数组。
写成C语言,大概长这样:
long long c = 1; printf("%lld ", c); for (int k = 1; k <= n; k++) { c = c * (n - k + 1) / k; printf("%lld ", c); }这里有个数学细节值得注意:组合数公式里是“先乘后除”,而且是整除,但中间结果可能会很大。所以一般要用long long来存,否则n稍大一点就会溢出。这个代码看起来简单,但能写出来的人,往往在数据类型的敏感性上比只背二维数组代码的人强不少。
4.3 内存能开多大,取决于你怎么存
用二维数组直接存n行,空间是O(n²)。n=10时无所谓,n=1000时,哪怕每个元素只占4字节,也需要约4MB内存,勉强还好;但如果你存的是long long,那就翻倍到8MB,再加上乱七八糟的冗余,内存就有点紧张了。所以在实际工程场景里,要么用一维滚动数组,要么干脆用组合数公式一行一行地边算边输出。
这也是为什么我建议初学者在学会二维数组版本后,一定要再写一遍一维数组版本。不是为了炫技,而是为了让你体会到:同一道题,换一个维度思考,内存开销能差一个数量级。这种“优化意识”,比记住某个具体题目重要得多。
5. 常见问题与排查技巧实录
这个题目虽然基础,但在我见过的作业和答疑里,翻车率一点都不低。下面挑几个最典型的现场,说说问题出在哪、怎么排查。
5.1 经典翻车现场:数组越界和扫描不到数据
有同学一上来就把数组定义成int a[n][n],然后用a[i-1][j]去访问,结果数组越界、程序直接崩溃。问题是出在“没有给下标0做边界保护”。
排查思路很简单:把数组打印出来看一遍。在算完所有行之后、输出之前,用printf("%d ", a[i][j])把整个二维数组从头到尾打印一遍,看哪些位置的值是垃圾值。如果是,就去查那两行边界判断写没写对:a[i][0] = 1和a[i][i] = 1是不是在每个外层循环里都被执行到了。
另外,局部数组最好初始化:int a[100][100] = {0}。这样就算你的逻辑里漏掉了某些位置,至少不会出现乱七八糟的垃圾值。很多奇奇怪怪的输出,都是因为没有初始化引起的。
5.2 排版对不齐,第一步先查占位符
“为什么我的三角形右边像楼梯,左边却歪了?”——这种问题,90%出在printf的格式控制上。你先检查一下:是%d还是%4d还是%-4d?如果是%d,没有任何占位宽度,数字位数不同的时候天然就会参差不齐。改成%4d再看。
第二步查前面补空格的循环次数。常见错误是左右各多补了一格或少补了一格。这里教一个笨办法:先用一个小行数(比如4行或5行),每行前面补2个空格,手动在纸上画一遍,看缩进量是怎么递减的,再对着代码改。看到缩进对了,再扩大到更大行数,如果又歪了,回到3.3小节的变形成因。
5.3 想抄作业结果抄了半份:只输出了半边三角
还有一种情况:输出只有左半边的1和数字,右半边全是空白或者没有。这多半是内层循环的终止条件写错了。正确的内层循环应该是j <= i,但有人写成了j < i,这样最后一列就被漏掉了,而那恰好是应该打印1的位置。
再加一个隐藏的坑:只输出上半部分时,有人在打印前先想到“杨辉三角是对称的,只算一半再镜像”,但镜像逻辑没写对,结果右边的数字全错了。我的建议是:初学者老老实实算完整再输出,等代码跑通、规律摸清了,再琢磨对称优化也不迟。
我给这个题目做个收尾总结,其实是给自己留个提醒:杨辉三角形这个题,看起来简单,但它把“递推、边界、格式化输出、空间优化”这几个C语言的关键点全串起来了。我自己的经验是,别满足于把答案背下来,试着改一改条件——比如“用一维数组实现”“只输出第n行”“不限制行数但是不能开二维数组”,每改一次,你都会对这道题有新的理解。这也是我为什么在讲完基础代码后,一定要把优化版本和常见翻车现场都摊开来讲的原因。希望你写这道题的时候,不只是交个作业,而是真正把它当成一块练手的磨刀石。