news 2026/10/17 5:51:39

C语言打印杨辉三角形:数组、循环与格式化输出全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言打印杨辉三角形:数组、循环与格式化输出全解析

学 C 语言的人,迟早都要和“输出杨辉三角形”这道经典题目碰面。它经常以“打印杨辉三角的前 N 行”的形式出现在课本习题、期末上机题,甚至一些公司的笔试题里。题面只有一句话,看起来人畜无害,但真坐到编辑器前写的时候,你会发现它把 C 语言里几个最基础也最容易翻车的东西全串起来了:循环嵌套怎么写、数组怎么初始化、下标怎么不越界、输出格式怎么对齐。这篇文章就围绕 C 语言输出杨辉三角形这件事,把规律推导、两种主流实现、格式对齐技巧和常见翻车现场完整过一遍。不管你是刚学到数组的初学者,还是想看看有没有更优写法的老手,都能从里面找到对你有用的东西。

杨辉三角形本身不复杂:它是一个数字金字塔,每一行的首尾都是 1,中间每个数等于它左上方和右上方两个数之和。第 0 行是 1,第 1 行是 1 1,第 2 行是 1 2 1,第 3 行是 1 3 3 1,再往下就是 1 4 6 4 1。规律一句话就能说清,但“用 C 语言把它漂亮地打印出来”和“在纸上画出它”是完全两码事。纸上画画不用管内存,不用管下标,更不用管每一行前面要空几个空格才像三角形。一旦换成 C 语言,这些问题全都变成必须处理的细节。这也是为什么这道题能一直活跃在各种教材和考试里,因为它就是考察数组和循环基本功的试金石。

1. 杨辉三角的规律,动手前先拆干净

1.1 核心递推关系:每个数都是上面两个数之和

写代码之前,我习惯先把要实现的数学规律写在纸上。杨辉三角最核心的性质是:从第 2 行开始,中间任意一个数都等于它上一行同位置和前一个位置的数相加。

举个例子,第 3 行是1 3 3 1,中间两个 3 是怎么来的?第一个 3 等于第 2 行的1 + 2,第二个 3 等于第 2 行的2 + 1。放到数组里,如果我们用tri[i][j]表示第 i 行第 j 列的数,那么递推式就是:

tri[i][j] = tri[i-1][j-1] + tri[i-1][j]

这个式子看着简单,但它决定了你的核心循环怎么写:外层循环遍历每一行,内层循环遍历这一行中间的每个位置,然后套用这个公式。很多人一上来就写循环,写到一半卡住,就是因为没在纸上先把这条递推关系列出来。先花两分钟把公式写明白,后面写代码就是照着公式翻译的事。

1.2 边界条件:每一行的首尾永远是 1

递推式能处理中间的数,但首尾两个数是特殊情况。第 i 行的第 0 列永远是 1,第 i 行的第 i 列也永远是 1,它们没有“左上”或“右上”的邻居,不能套递推式。

所以标准的计算循环一定长这样:先把tri[i][0]和tri[i][i]都赋成 1,然后用内层循环从j = 1跑到j = i - 1,只处理中间部分。边界单独处理,中间用递推,这个思路在算法题里非常通用,不只是杨辉三角,任何有边界条件的矩阵填充问题都可以这么拆。

我见过不少初学者把内层循环写成for (j = 1; j <= i; j++),想着“干脆连最后一个也一起算了”。如果数组恰好初始化为 0,这样算出来的结果碰巧也是对的,因为tri[i-1][i]是 0,tri[i][i]等于tri[i-1][i-1] + 0,最终还是 1。但这种写法依赖“数组恰好是 0”,一旦数组没初始化,tri[i-1][i]就是随机垃圾值,结果直接乱掉。所以老老实实把边界单独处理,别图省事。

1.3 它和组合数是一回事,可以用来验算

杨辉三角还有一个隐藏身份:第 n 行其实就是二项式系数C(n, 0), C(n, 1), ..., C(n, n)。也就是说,第 4 行应该是C(4,0)=1, C(4,1)=4, C(4,2)=6, C(4,3)=4, C(4,4)=1,正好就是1 4 6 4 1。

这个关系有两个实际用途。第一,程序写完以后,你可以拿任意一行去和组合数公式算出来的结果对照,快速确认程序对不对。第二,它解释了为什么三角形是对称的,因为C(n, k) = C(n, n-k)。我在调试的时候经常用这个对称性:打印出来以后,如果某一行左右不对称,那基本可以断定是递推式或者下标写错了。

2. 方案一:二维数组直球实现

2.1 完整代码与逐行解读

先上最直观的版本。用一个二维数组存整个三角形,先全部算完,再统一打印。

#include <stdio.h> #define ROWS 10 int main() { int tri[ROWS][ROWS] = {0}; // 计算阶段 for (int i = 0; i < ROWS; i++) { tri[i][0] = 1; tri[i][i] = 1; for (int j = 1; j < i; j++) { tri[i][j] = tri[i - 1][j - 1] + tri[i - 1][j]; } } // 打印阶段 for (int i = 0; i < ROWS; i++) { for (int k = 0; k < ROWS - i; k++) { printf(" "); } for (int j = 0; j <= i; j++) { printf("%4d", tri[i][j]); } printf("\n"); } return 0; }

这段代码里最值得讲的是两层循环的设计。计算阶段,外层i控制行号,从 0 到ROWS-1;内层j控制列号,从 1 到i-1。当i等于 0 或 1 时,内层循环一次都不执行,因为第 0 行和第 1 行全是 1,没有中间元素。这个自动跳过边界的写法很干净,不需要单独写if判断。

int tri[ROWS][ROWS] = {0};这一句很多人会忽略,但它非常关键。{0}会把整个二维数组全部初始化为 0。虽然我们计算时只用到部分格子,但如果你打印时下标没控制好,读到未初始化的格子,输出就是乱七八糟的随机数。先全部归零,等于给程序上了一道保险。

2.2 为什么不边算边打,要分开两个阶段

有些教材会把计算和打印放在同一个循环里:每算完一行就立刻打印一行。代码更短,对纯打印需求也完全够用。我早期也这么写,但后来在项目里需要把这个三角形数据二次利用时,发现“先算后打”的版本改起来容易得多——打印逻辑和计算逻辑互不干扰,想调整空格宽度、想改成居中对齐,只需要动打印循环,计算部分碰都不用碰。

另外一个实际原因是调试体验。如果边算边打,一旦输出错乱,你要同时怀疑计算和打印两个环节。分开以后,可以先不看打印格式,单独检查tri数组里的值对不对,确认数据正确再调格式。对初学者来说,“计算”和“展示”分离是一种特别值得养成的习惯,不只是杨辉三角,很多编程题都能用这个原则来降低调试难度。

3. 方案二:一维数组滚动更新

3.1 核心思路:只保留一行,原地覆盖

二维数组虽然直观,但空间是O(n^2)。如果只要求打印,不要求把整个三角形留到后面用,其实可以只用一个一维数组,一边算一边把它从“上一行”原地更新成“当前行”。这个技巧通常叫滚动数组,在动态规划题里也经常见到。

核心思想是这样的:数组tri在进入第 i 轮循环之前,存的是第i-1行的数据。我们要把它更新成第 i 行。更新的时候,新值等于“当前值”加上“左边的值”,而这两个值在更新之前都还属于上一行。问题是,如果你从左往右更新,左边的值先被改成了新一行的值,后面的计算就用错数据了。

3.2 完整代码:为什么必须倒着更新

#include <stdio.h> #define ROWS 10 int main() { int tri[ROWS] = {0}; for (int i = 0; i < ROWS; i++) { tri[i] = 1; for (int j = i - 1; j > 0; j--) { tri[j] = tri[j] + tri[j - 1]; } for (int k = 0; k < ROWS - i; k++) { printf(" "); } for (int j = 0; j <= i; j++) { printf("%4d", tri[j]); } printf("\n"); } return 0; }

关键在for (int j = i - 1; j > 0; j--)这一句,必须从右往左更新。我们模拟一下第 3 轮循环:进入循环前,数组里存的是第 2 行1 2 1(后面全是 0)。先把tri[3]设成 1,也就是新行最后一个数。然后从j = 2开始倒着更新:tri[2] = tri[2] + tri[1] = 1 + 2 = 3,此时用到的是上一行的tri[1],还没被改过。接着tri[1] = tri[1] + tri[0] = 2 + 1 = 3,同样用的是旧值。一轮跑完,数组变成1 3 3 1,正好是第 3 行。

如果改成从左往右更新,第 2 轮循环就有问题了。tri[1] = 1 + 1 = 2把原来的 1 覆盖成 2,等到算tri[2]时,左边已经变成新行的 2,而不是上一行的 1,算出来的就多了。所以这个倒序更新不是风格问题,是正确性问题。务必记住:原地滚动更新,方向错了数据一定错。

3.3 两种方案怎么选

对比项二维数组一维数组滚动
空间复杂度O(n^2)O(n)
代码可读性高,tri[i][j]直观稍绕,要理解倒序覆盖
数据保留所有行都能后查只能拿到当前行
推荐场景学习、后续要复用整张表只要求打印、追求精简

我的建议很直接:如果你是初学者,第一次写这道题,先用二维数组,把逻辑看懂、写对,不要上来就挑战一维版本。一维滚动的核心价值是省内存,但代价是牺牲直观性。等你二维版本能闭着眼写出来,再去看一维版本,理解倒序更新背后的原因,那时候它对你才真正有意义。

4. 输出格式:让三角形真的像三角形

4.1 前置空格怎么算

很多人的程序数据算得全对,但打印出来是一堆左对齐的数字,完全看不出三角形形状。问题出在每行开头的空格数量上。

如果用printf("%4d", ...)打印每个数,每个数固定占 4 个字符宽,第 i 行有i+1个数,那一行总共占(i+1) * 4个字符。最后一行有ROWS个数,总宽度是ROWS * 4。想让整个三角形居中,每一行前面补的空格数应该是总宽度减去本行宽度的差值的一半,也就是:

offset = (ROWS - i - 1) * 4 / 2 = (ROWS - i - 1) * 2

所以代码里通常会写for (int k = 0; k < ROWS - i; k++) printf(" ")。严格来说,ROWS - i比标准居中的ROWS - i - 1多了 1,也就是整体右移了一个空格。但很多教材都这么写,因为视觉效果差别不大,代码短一行。我自己写的时候习惯用ROWS - i - 1,让三角形更居中一点。这个细节不影响正确性,纯粹是观感问题,你选哪种都行,但要清楚两者差在哪里。

4.2 数字变多:固定宽度救不了你

%4d在ROWS不大于 13 左右的时候效果不错。一旦行数上去,数字位数超过 4,比如第 15 行中间的数已经到 3432,第 20 行中间的数高达 92378,固定 4 位宽就会把对齐全部打乱。这时候需要根据最大的那个数动态计算宽度。

#include <stdio.h> #define ROWS 15 int main() { int tri[ROWS][ROWS] = {0}; int maxVal = 1; for (int i = 0; i < ROWS; i++) { tri[i][0] = 1; tri[i][i] = 1; for (int j = 1; j < i; j++) { tri[i][j] = tri[i - 1][j - 1] + tri[i - 1][j]; if (tri[i][j] > maxVal) { maxVal = tri[i][j]; } } } int width = 1; while (maxVal >= 10) { maxVal /= 10; width++; } width++; // 数字之间至少留一个空格 for (int i = 0; i < ROWS; i++) { int offset = (ROWS - i - 1) * width / 2; for (int k = 0; k < offset; k++) { printf(" "); } for (int j = 0; j <= i; j++) { printf("%*d", width, tri[i][j]); } printf("\n"); } return 0; }

printf("%*d", width, tri[i][j])是 C 语言里一个很实用的格式控制技巧,*表示宽度由后面的参数动态传入。计算最大值的位数时,我先把maxVal不断除以 10 来数位数,然后加 1 作为数字间的最小间隔。前置空格用(ROWS - i - 1) * width / 2,相当于把固定宽度版本里的 4 替换成动态宽度。这样不管行数怎么变,三角形都能保持居中对齐。

5. 常见问题与调试实录

我把这些年看到、遇到过的典型问题整理成一张速查表,每个都对应一个具体的翻车现场。

现象大概率原因处理办法
打印出一堆垃圾值数组没初始化,或忘了给行首行尾赋 1定义时写= {0},显式赋tri[i][0]和tri[i][i]
每行最后一个数字不对内层循环写成j <= i,越界读了垃圾值内层改成j < i,边界单独处理
行数少一行或多一行外层循环边界写成<= ROWS统一用i < ROWS,内层用j <= i
数字突然变成负数int溢出换long long,或限制行数
一维版本中间数字偏大内层正序更新,左边被提前覆盖内层倒序for (j = i - 1; j > 0; j--)
三角形越来越歪固定%4d撑不住更多位数用%*d动态计算宽度

5.1 垃圾值和越界,是同一个坑的两面

输出出现垃圾值,十有八九和数组初始化有关。C 语言里局部数组如果不初始化,里面是内存残留的随机数。尤其用二维数组时,很多人记得给计算用的格子赋值,却忘了打印时会读到没赋值的区域。或者内层循环边界写宽了,j跑到i,就会去读tri[i-1][i],这一格在上一行根本不存在,读出来就是野值。解决办法很简单:声明时加= {0},循环边界用j < i。这两步做到,垃圾值问题基本消失。

5.2 溢出问题:杨辉三角是“数字炸弹”

杨辉三角的数值增长比想象中快得多。到第 35 行时,中间的数已经超过 21 亿,直接爆掉 32 位int的上限,表现出来就是负数或者奇怪的数字。所以如果你的程序允许用户输入比较大的行数,最好把数组类型换成long long。但要注意,long long也不是无限的,到第 68 行左右同样会溢出。更讲究的做法是用大数库,或者只打印不存储、用组合数公式按行生成,但这已经超出基础题的范围了,知道有这么回事即可。

5.3 一维版本最常见的“看起来很美”错误

一维版本调错方向的时候,输出特别有迷惑性。前两三行看起来完全正常,因为行数太小,错误还没积累起来。到第 4 行、第 5 行才突然冒出个偏大的数,比如把 4 算成 5、把 10 算成 15。我第一次踩这个坑时,盯着输出看了半天,一度怀疑是递推公式记错了。后来手动在纸上推了一遍才发现,是更新方向反了。所以调一维版本时,别只看前几行,至少看到第 5 行以后,再用组合数公式验算一次中间值,这样最容易暴露问题。

6. 扩展玩法:从“会写”到“能灵活用”

6.1 支持用户输入行数

把#define ROWS 10改成用户输入,代码需要调整。二维数组不行,因为 C 语言里数组大小必须是编译期常量。有两个方向:一是用 C99 的变长数组int tri[rows][rows],语法上可行,但行数太大时可能爆栈;二是用动态内存分配,这也是实际项目里更通用的做法。

#include <stdio.h> #include <stdlib.h> int main() { int rows; printf("请输入杨辉三角的行数: "); scanf("%d", &rows); if (rows <= 0) { printf("行数必须大于 0\n"); return 1; } int **tri = (int **)malloc(rows * sizeof(int *)); if (tri == NULL) { return 1; } for (int i = 0; i < rows; i++) { tri[i] = (int *)malloc((i + 1) * sizeof(int)); tri[i][0] = 1; for (int j = 1; j < i; j++) { tri[i][j] = tri[i - 1][j - 1] + tri[i - 1][j]; } tri[i][i] = 1; } for (int i = 0; i < rows; i++) { int offset = (rows - i - 1) * 2; for (int k = 0; k < offset; k++) { printf(" "); } for (int j = 0; j <= i; j++) { printf("%4d", tri[i][j]); } printf("\n"); } for (int i = 0; i < rows; i++) { free(tri[i]); } free(tri); return 0; }

注意这里每一行只分配i+1个元素,不浪费多余空间,因为第 i 行本来就只需要这么多。同时记得最后用free释放,先释放每一行,再释放指针数组,顺序不能反。

6.2 封装成函数,方便反复调用

如果不想每次都写一长串main,可以把核心逻辑封装成函数。我平时练习时习惯写成一个printYanghui(int rows),用一维滚动数组实现,内存省,函数短,放进通用工具库也不占地方。

#include <stdio.h> void printYanghui(int rows) { long long tri[68] = {0}; if (rows <= 0 || rows > 68) { printf("行数超出可打印范围\n"); return; } for (int i = 0; i < rows; i++) { tri[i] = 1; for (int j = i - 1; j > 0; j--) { tri[j] += tri[j - 1]; } for (int k = 0; k < rows - i; k++) { printf(" "); } for (int j = 0; j <= i; j++) { printf("%4lld", tri[j]); } printf("\n"); } } int main() { printYanghui(10); return 0; }

这个版本把行数上限限制在 68,是为了保证long long不会溢出。函数里先做参数检查再干活,这个习惯很值得养成,别等到数组越界或者数据溢出才发现问题。

我在实际练习里最深的体会是:杨辉三角这道题,网上答案随手就能搜到,但复制粘贴十遍,不如自己从头写一遍。先写二维数组版本,打印出来对着组合数公式验算;再改成动态宽度,把对齐调漂亮;最后挑战一维滚动数组,理解倒序更新的必要性。每跨一步,你对数组下标、循环边界和内存的理解都会扎实一层。还有一个调试小技巧:写完之后故意把某个边界条件改错,比如把j < i改成j <= i,观察输出变化。这种“故意弄坏再修好”的练习方式,比单纯刷题更能帮你记住每个细节为什么存在。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 5:51:35

Unity卡通渲染实战:从光照模型到完整Shader框架

1. 从“纸片人”到“立体感”&#xff1a;卡通渲染的视觉核心到底在哪很多人第一次尝试在Unity里复刻卡通渲染&#xff0c;都会掉进同一个坑&#xff1a;把模型往场景里一放&#xff0c;加个方向光&#xff0c;然后开始调Ramp贴图&#xff0c;结果发现角色要么像糊了一层泥&…

作者头像 李华
网站建设 2026/10/17 5:51:15

PacketTracer智慧城市设备接入OneNET云平台:从HTTP上云到远程控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 5:51:10

双节锂电池智能均衡方案:MP2642GR-Z与STM32F042K6协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 5:50:56

HttpClient消息管道探秘:用DelegatingHandler统一处理日志、认证与重试

我最早接触DelegatingHandler&#xff0c;是在排查一组接口超时问题的时候。日志打了一堆&#xff0c;状态码看着也正常&#xff0c;可就是有几个请求慢得离谱&#xff0c;最后定位到是某个调用链路上重试逻辑写得太分散&#xff0c;每个请求都要手工处理认证头、日志、超时控制…

作者头像 李华
网站建设 2026/10/17 5:50:50

智慧蜂业无人机集群协同测试体系:架构、指标与故障复盘

第一次在蜂场试飞三机编队&#xff0c;原本排队出勤的蜜蜂像被捅了马蜂窝一样炸开了。那一刻我意识到&#xff0c;智慧蜂业无人机集群协同测试&#xff0c;绝对不是在田里飞一圈看它稳不稳那么简单。我们后来花了大半年&#xff0c;围绕一个智慧蜂业示范项目&#xff0c;把集群…

作者头像 李华
网站建设 2026/10/17 5:50:03

计算机组成原理中的MIPS:指令格式、数据通路与流水线入门

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华