C语言打印沙漏:循环控制与格式化输出的经典算法解析
2026/9/17 7:39:27 网站建设 项目流程

1. 从一道经典题看C语言的控制逻辑与格式化输出

最近在整理一些C语言的入门练习题,发现“打印沙漏”这道题出现的频率相当高。无论是学校的课程作业,还是像PAT(浙江大学程序设计能力考试)这类机试的真题,它都算得上是常客。很多初学者乍一看题目描述,觉得不就是打印个对称图形嘛,但真正上手写代码时,却常常在控制行数、空格和符号的逻辑关系上绕晕,最后打印出来的要么是半个沙漏,要么就是形状歪七扭八。

这道题的价值,远不止于“打印一个图案”。它本质上是一个绝佳的综合性练习,能同时考察你对循环控制、数学计算和格式化输出的掌握程度。你需要先根据给定的符号总数,反推出能打印的最大沙漏的行数,然后分上下两部分,精确控制每一行打印的空格数和符号数。整个过程就像是在用代码“雕刻”一个图形,每一步的逻辑都必须清晰无误。今天,我就结合自己当年刷题和后来辅导学生的经验,把这道题的解题思路、代码实现,以及那些容易踩进去的“坑”,从头到尾捋一遍。无论你是正在备战考试,还是想巩固C语言基础,相信这篇内容都能给你带来实实在在的帮助。

2. 问题拆解:沙漏的数学规律与核心算法

在动手写代码之前,我们必须先把沙漏这个图形的数学规律吃透。一个对称的沙漏,可以看作是由一个顶点符号、以及向下和向上对称扩展的若干层组成。假设我们使用星号“*”来打印,一个最简单的沙漏就是一行只有一个星号。但题目通常要求沙漏的“腰部”也是对称的,即除了最中间一行只有一个符号外,上下部分都是对称的奇数行。

2.1 行数与符号总数的关系

这是解题的第一个关键点,也是后续所有计算的基础。我们设沙漏上半部分(包括中间行)的行数为H。那么,整个沙漏的总行数total_rows就是2 * H - 1。例如,H=3时,上半部分有3行(第1行5个*,第2行3个*,第3行1个*),下半部分是对称的2行(第4行3个*,第5行5个*),总行数为5。

接下来计算符号总数。沙漏的符号数量是一个等差数列求和问题。上半部分(从中间行向上)的符号数:中间行1个,上一行3个,再上一行5个……这是一个首项为1,公差为2的等差数列。上半部分H行的总符号数就是H * H(等差数列求和公式:S = n * a1 + n*(n-1)*d/2,这里a1=1, d=2, n=H,化简后为H*H)。因为沙漏上下对称,下半部分符号数与上半部分去掉中间一行后的数量相等,即(H-1)*(H-1)。所以,整个沙漏的符号总数total_chars = H * H * 2 - 1。这个-1是因为中间行被计算了两次。

2.2 根据给定符号数N求最大行数H

题目通常会给定一个整数N和一种符号,要求用掉尽可能多的符号来打印沙漏,最后还要输出剩下多少符号没用。所以,我们的算法是:找到满足total_chars <= N的最大H。换句话说,就是找到最大的H,使得H * H * 2 - 1 <= N

这个过程可以用一个简单的循环来实现:从H=1开始,计算need = 2*H*H - 1,如果need <= N,则H++继续尝试;一旦need > N,就跳出循环,此时的H-1就是我们能打印的最大沙漏的上半部分行数。剩下的符号数remain = N - (2*(H-1)*(H-1) - 1)

2.3 打印图形的双重循环逻辑

确定了行数H之后,打印就变成了一个标准的“行-列”控制问题。我们需要打印total_rows = 2*H - 1行。对于第i行(假设从0开始计数):

  1. 打印空格:空格的数量与当前行到中间行的“距离”有关。距离越远,空格越多。具体来说,空格数space = abs(i - (H - 1))。这个公式可以统一处理上半部分和下半部分。
  2. 打印符号:符号的数量是一个关于行序的奇数序列。第i行需要打印的符号数star = 2 * (H - 1 - space) - 1。推导一下:H-1-space表示当前行在它所属的半边(上或下)中是第几层(从0开始),每一层的符号数构成奇数序列 1, 3, 5...,所以符号数就是2 * 层数 + 1,但这里层数是从0开始的,所以是2 * (层数) + 1,而层数 = H-1-space,代入即可。

注意:很多同学在这里喜欢写两个大循环,分别打印上半部分和下半部分。这当然可以,但代码会冗余。利用abs()函数和统一的公式,我们可以用一个循环搞定所有行的打印,代码更简洁,也不容易出错。这是处理对称图形打印的一个常用技巧。

3. 手把手实现:从伪代码到可运行的C程序

理解了核心算法,我们就可以开始编写代码了。我会按照“输入-处理-输出”的逻辑,一步步构建程序,并解释每一段代码的意图。

3.1 程序框架与变量定义

首先,包含必要的头文件,并定义主函数和变量。

#include <stdio.h> #include <math.h> // 为了使用abs()函数,需要包含math.h int main() { int N; // 给定的符号总数 char c; // 要打印的符号,如 '*' int H = 0; // 沙漏上半部分行数(包括中间行) int need; // 当前H需要的符号数 int remain; // 剩余符号数 int total_rows; // 沙漏总行数 int i, j; // 循环控制变量 // 输入部分 scanf("%d %c", &N, &c); // 注意:题目输入格式通常是“数字 符号”,例如“19 *”

这里有一个小细节:abs()函数虽然在stdio.h中可能也能用,但为了可移植性,最好包含math.h。另外,输入时%d%c之间有一个空格,这个空格很重要,它会吸收数字和字符之间的空白符(如空格、换行),确保正确读入字符。

3.2 计算最大行数H

接下来,我们通过循环来计算最大的H

// 计算能打印的最大沙漏的上半部分行数H while (1) { need = 2 * (H+1) * (H+1) - 1; // 计算如果上半部分行数为H+1时需要的总符号数 if (need > N) { break; // 如果需要的符号超过了N,则当前的H就是最大值 } H++; // 否则,可以尝试更大的H } // 处理N很小,连一个符号都打印不了的情况(理论上N>=1,但考虑健壮性) if (H == 0) { // 此时,沙漏不存在,用掉的符号数为0,剩余为N printf("0\n"); // 有些题目要求先输出用掉的符号数,这里假设输出剩余数,需根据题目调整 remain = N; } else { // 计算剩余符号数 remain = N - (2 * H * H - 1); total_rows = 2 * H - 1; }

这段代码的逻辑是:不断试探更大的H,直到其所需的符号数need超过给定的N。循环跳出时,H就是满足条件的最大值。need的计算公式里用了H+1,是为了在循环内判断“下一个”H是否可行,这样写逻辑更清晰。

3.3 打印沙漏图形

这是最核心的部分,我们将使用一个循环遍历所有行,并在每一行内先打印空格,再打印符号。

// 打印沙漏 for (i = 0; i < total_rows; i++) { // 计算当前行前面的空格数 int space_count = abs(i - (H - 1)); // 当前行与中间行的距离 // 计算当前行需要打印的符号数 int char_count = 2 * (H - 1 - space_count) + 1; // 打印空格 for (j = 0; j < space_count; j++) { printf(" "); } // 打印符号 for (j = 0; j < char_count; j++) { printf("%c", c); } // 每行结束后换行 printf("\n"); }

让我们验证一下公式:当i等于中间行索引(H-1)时,space_count = 0,char_count = 2*(H-1-0)+1 = 2*H-1,这正是中间行应该有的符号数(也是最多的一行)。当i为0或total_rows-1(首尾行)时,space_count = H-1char_count = 2*(H-1-(H-1))+1 = 1,符合沙漏顶部和底部只有一个符号的规律。

3.4 输出剩余符号数并完成程序

最后,按照题目要求输出剩余未使用的符号数量。

// 输出剩余符号数 printf("%d\n", remain); return 0; }

将以上所有代码段组合起来,就是一个完整的、可以处理标准“打印沙漏”问题的C语言程序。你可以尝试输入不同的Nc进行测试。

4. 深度优化与边界条件处理

一个能通过基础测试的程序只是开始。在实际的OJ(Online Judge)系统中,题目往往会设计一些边界情况来考验程序的健壮性。此外,代码本身也有优化空间。

4.1 边界条件与特殊输入

  1. N很小的情况:如果N小于7(H=2时需要7个符号),我们的循环计算出的H可能为1(因为H=1时需要1个符号,H=2时需要7个符号,当N=3时,needfor H=2 为7>3,所以H=1)。这是正确的。但如果N=0呢?按照我们的while循环,need初始计算H+1=1时的值为2*1*1-1=1,大于0,所以H不会自增,保持为0。在打印环节,total_rows = 2*0-1 = -1,这会导致循环出现问题。因此,更健壮的写法是在计算完H后,立即判断if (H == 0),如果成立,则只输出剩余符号数(就是N),并且不进行任何打印操作。这符合题目“尽可能多用”的逻辑——当连一个符号都用不掉时,就不打印。

  2. 输入格式陷阱:题目输入有时是N c,中间可能有多个空格或直接是换行。使用scanf(“%d %c”, &N, &c);中的空格可以处理这种情况。但更安全的方法是使用scanf(“%d”, &N); getchar(); scanf(“%c”, &c);来显式吸收掉数字后的空白符。不过对于大多数OJ,第一种写法已经足够。

  3. 符号类型:题目给定的符号可能是任意字符,比如*#@等。我们的代码使用%c读入并存储到char c中,是通用的。

4.2 算法效率与代码优化

我们的算法时间复杂度是 O(H + total_rows²),因为计算H的循环是 O(H),打印图形是 O(total_rows * max_char_per_row),而max_char_per_rowH成正比,所以大致是 O(H²)。对于本题,N通常不超过1000,H最大也就约等于sqrt(N/2),约22,所以效率完全不是问题。但我们可以让代码更优雅:

  • 避免使用math.h:如果环境限制不允许链接数学库,我们可以自己实现一个求绝对值的宏或函数,或者用条件判断来代替abs(i - (H-1))。例如,在打印循环内部判断if (i < H)则为上半部分逻辑,else为下半部分逻辑。这样就不需要math.h了。
  • 预计算与循环展开:对于每一行的空格数和符号数,我们可以在打印循环外先计算好并存储到数组里,然后再用循环打印。这样逻辑更清晰,但会占用少量额外空间。对于教学和清晰度而言,在循环内直接计算是更好的选择。
  • 使用putchar代替printf:在内部打印大量相同字符的循环中,使用putchar(c)printf(“%c”, c)效率稍高,因为printf需要解析格式字符串。但在这种小规模输出中,差异可以忽略不计。

4.3 一个优化后的、不使用math.h的版本示例

#include <stdio.h> int main() { int N, H = 0, remain, total_rows; char c; scanf("%d %c", &N, &c); // 计算最大H while (2 * (H+1) * (H+1) - 1 <= N) { H++; } if (H == 0) { printf("0\n"); return 0; } total_rows = 2 * H - 1; remain = N - (2 * H * H - 1); // 打印上半部分(包括中间行) for (int i = 0; i < H; i++) { // 空格数 = H - i - 1 // 符号数 = 2 * i + 1 for (int j = 0; j < H - i - 1; j++) putchar(' '); for (int j = 0; j < 2 * i + 1; j++) putchar(c); putchar('\n'); } // 打印下半部分(不包括中间行) for (int i = H - 2; i >= 0; i--) { // 空格数 = H - i - 1 // 符号数 = 2 * i + 1 for (int j = 0; j < H - i - 1; j++) putchar(' '); for (int j = 0; j < 2 * i + 1; j++) putchar(c); putchar('\n'); } printf("%d\n", remain); return 0; }

这个版本将打印分成了上下两个独立的循环,避免了使用abs()函数和复杂的统一公式,逻辑上更直观,也更容易被初学者理解和调试。它清晰地展示了沙漏的对称性:上半部分从第0行到第H-1行,符号数递增(1, 3, 5…);下半部分从第H-2行回溯到第0行,符号数递减。

5. 常见错误排查与调试心得

即便思路清晰,在实现过程中也难免会遇到各种问题。下面我总结几个最常见的错误点和调试技巧。

5.1 图形打印错位或不对称

这是最常见的问题,现象通常是沙漏歪了,或者上下部分对不齐。

  • 根因:绝大多数情况下,问题出在空格数量的计算上。要么是公式推导错误,要么是循环的边界条件没控制好(例如,for (j = 0; j <= space_count; j++)多打了一个空格)。
  • 调试方法:不要只看星号,把空格也可视化。一个简单的技巧是,在调试阶段,先用一个可见字符(比如点.)代替空格打印出来。这样你就能清晰地看到每一行前面到底有多少个“空格”,以及它们是否对称。
    // 调试时,将打印空格的循环改为: for (j = 0; j < space_count; j++) { printf("."); // 用点代替空格,便于观察 }
    运行程序,你会看到由点构成的左边界是否是一条直线。如果不是,说明你的space_count计算有误。

5.2 符号总数计算不正确,导致剩余数错误

这个问题在提交到OJ时,可能表现为“答案错误”,而不是“格式错误”。

  • 根因:行数H的计算逻辑有误。可能是在while循环中,判断条件need <= N写成了need < N,导致H少算了1;或者是在计算剩余数remain时,公式2*H*H-1写错了。
  • 调试方法:在程序开头计算完H后,立刻打印出Hneed(当前H所需符号)和remain的值。然后用手算几个简单的例子进行验证。例如,输入N=7,c=‘*’,正确的H应该是2(因为2*2*2-1=7),remain=0。输入N=8,正确的H仍然是2(因为2*3*3-1=17>8),remain=1

5.3 最后一行的多余空格或换行符

有些OJ对输出格式要求极其严格,不允许在最后一行符号后面有多余的空格,或者要求最后一行输出后必须有换行。

  • 根因:在打印符号的循环后,不小心多打了一个空格,或者printf(“\n”)的位置放错了。
  • 解决方案:我们的代码在每行打印完符号后直接换行,符号后面没有空格,这是符合要求的。但要特别注意,不要在打印符号的循环内,在printf(“%c”, c)后面加空格。确保你的代码和上面示例一致。

5.4 内存或时间超限

对于本题,几乎不可能发生。但如果你的代码用了非常复杂的结构(比如二维数组先存储再打印),或者有死循环,则有可能。

  • 根因while循环计算H时,如果条件永远成立,会导致死循环。例如,忘记更新循环变量H
  • 检查点:确保你的while循环内有让条件可能变为假的语句(如H++)。

6. 举一反三:从沙漏到其他图形打印问题

掌握了“打印沙漏”的精髓,你就拥有了解决一大类“字符画”或“格式化输出图形”问题的钥匙。这类问题的核心模式都是:找出图形与行号之间的数学关系(等差数列、等比数列、对称性等),然后用双重循环控制行和列的输出

6.1 打印菱形

菱形可以看作是两个等腰三角形(一正一反)的组合,也可以看作是一个“胖版”的沙漏(中间一行最长,向上下对称减少)。其解题思路与沙漏几乎一模一样。假设菱形高度为2n-1行,那么第i行(0起始)的空格数就是abs(n-1 - i),符号数就是2*(n-1 - space) + 1。看,公式是不是和沙漏的完全一致?唯一的区别是,菱形中间一行的符号数可能和沙漏要求的不同(沙漏中间行可能要求是1个,而菱形中间行通常最长),这只需要调整n的定义或计算公式中的常数即可。

6.2 打印空心图形

例如空心沙漏或空心菱形。思路是:在打印每一行时,判断当前打印的位置是否是图形的“边界”。如果是边界,打印符号;否则打印空格。这就需要我们知道当前行的起始符号位置和结束符号位置。对于空心沙漏,第i行需要打印符号的位置只有第一个和最后一个(对于非首尾行),或者全部(对于首尾行)。这需要更精细的条件判断。

6.3 更复杂的数字矩阵或蛇形填数

这类问题不再是打印固定字符,而是按照某种规律(螺旋形、蛇形、回形)填入递增的数字。其核心思想依然是通过循环和条件判断来控制“行走”的轨迹(方向:上、下、左、右),并标记哪些位置已经访问过。这可以看作是图形打印问题的“动态”升级版。

6.4 通用解题框架

面对任何图形打印题,我建议的思考步骤是:

  1. 观察:将图形放在坐标轴中,行索引为i,列索引为j。观察i与需要打印的“有效内容”(符号或数字)的起始列start_j、结束列end_j之间的关系。
  2. 建模:尝试用数学公式(通常涉及iabs()min()max()等)来描述start_j(i)end_j(i)
  3. 实现:外层循环for i遍历所有行,内层循环for j遍历该行所有列。在内层循环中,判断if (j >= start_j && j <= end_j)则打印内容,否则打印背景(通常是空格)。
  4. 调试:用小规模数据(如3行、5行)验证,并使用替换法(如用.代替空格)使输出可视化。

通过“打印沙漏”这道题,我们深入练习了循环控制、条件判断和格式化输出,这是C语言编程最基础的功力。更重要的是,我们学习了一种将具体问题抽象为数学模型,再用代码精确描述的思维方法。这种能力,是解决后续更复杂算法问题的重要基石。下次再遇到“打印钻石”、“打印W形”之类的题目时,不妨先拿出纸笔,画一画,找找规律,你会发现它们都是“沙漏”的变种而已。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询