缘起
学习C语言过程中遇到了一道要求打印菱形的习题,受同学启发尝试使用格式控制字符来实现,其中经历的过程颇为有趣,特此记录。
题目如下
由键盘输入正数n,要求输出 2 × n + 1 2×n+1 2×n+1行的菱形图案。要求菱形顶部无空行,菱形左边紧靠屏幕左侧。
[第一组自测数据]
键盘输入:3↙
正确输出:
*
***
*****
*******
*****
***
*
[第二组自测数据]
键盘输入:2↙
正确输出:
*
***
*****
***
*
“笨”方法实现
步骤简述
- 分别对菱形的上半部分和下半部分进行处理,此处简述以上半部分为例。
- 循环n+1次,循环序数i即为行数-1。
- 每行输出 输入数 − 行数 + 1 输入数-行数+1 输入数−行数+1个空格。
- 输出 行数 ∗ 2 − 1 行数*2-1 行数∗2−1个星号
- 接着输出下一行,直至循环结束。
- 下半部分同理,只不过循环序数为递减。
代码实现
为方便后续调试,此处将主体部分代码放在了loop()
函数当中。
#include<stdio.h>
/*方法一:通过循环输出空格实现格式控制*/
int loop(){
int n;
n=59;
//scanf("%d",&n);
for(int i=0;i<=n;i++){
for(int k=1;k<n+1-i;k++){
printf(" ");
}
for(int j=0;j<=i;j++){
printf("*");
}
for(int j=1;j<=i;j++){
printf("*");
}
printf("\n");
}
for(int i=n-1;i>=0;i--){
for(int k=n+1-i;k>1;k--){
printf(" ");
}
for(int j=0;j<=i;j++){
printf("*");
}
for(int j=1;j<=i;j++){
printf("*");
}
printf("\n");
}
}
此段代码经过测试,可以正常运行。
纵观全局,可以看到代码结构虽然较为的清晰,但是for循环两层嵌套之下还有好几个并列。
另一种方法
前提铺垫
在C语言中,我们可以通过格式控制字符使得计算机自动在我们输入的内容前补空格或者补其它字符,方便对齐和美观,那么这个特性是否能应用于此程序当中呢?研究过程中陆陆续续出现了诸多问题,这些问题的解决我想也十分有助于我更加深入地了解C语言的原理和语法知识,因此有必要记录下来。
一般的格式控制字符
郭老师曾说过:一切问题都是从特殊抽象为一般来研究。
那么,我们就拿一个特殊来看看,第一行过于特殊,可能难以抽象推广,我们不妨取第二行,此处假定如下:
变量含义 | 变量名 | 变量值 |
---|---|---|
循环序数 | i | 1 |
用户输入数 | a | 3 |
那么我们应当按照如下语句输出
printf("%5c","***");
经测试可以实现我们需要的效果,即该行前面有两个空格,然后接着三个星号。
推广
格式字符以字符串的形式存在,上一步当中%5c
的5
是一个每行都不同的数值,需要动态修改,如何实现呢?
试错1
我首先想到了字符串的本质是字符数组,因此能不能用以下的方式实现呢?
char str[]={'%',a+i+1+48,'c','\0'};
printf(str,'***');
这里说明一下:通过+48
这一个运算将数值型转换为对应数字的char型
此段代码有一个重大缺陷:星号*
的数量无法控制
试错2
为了控制星号的数量,我试图在数组中加入星号,即
char str[]={'%',a+i+1+48,'c','*','*','*',\0};
数组末尾的三个星号最后通过循环的形式,每次循环里面在数组末尾加一个,加到数组的末尾去,最后再把结束符\0
加上。既然都已经循环了,那为什么不直接打印呢?想到这,我放弃了原来的想法,没有继续实现下去,现在看来也未尝不可。
试错3
于是乎,我写了个循环打印的代码
char str[]={'%',a+i+1+48,'c','\0'};
for(int j=0;j<i;j++){
printf(str,'*');
}
刚开始看上去感觉没什么问题,一运行问题就来了。
似乎每个星号前面都有空格
——那是因为格式字符对每个星号都生效了。
优化1
为了解决这个问题,我又想到了一种思路,令格式字符只控制第一个星号的输出,后面再无脑循环输出 要求数量 − 1 要求数量-1 要求数量−1的星号。实现大致如下:
char str[]={'%',a-i+1+48,'c','\0'};
printf(str,'*');
for(int j=0;j<i-1;j++){
printf("*");
}
正当我以为大功告成之时,我就开始进行效率测试,试图对比两种方法的运行时间,为了突出差异,我把自定义的输入数设为了一个较大的数50
,结果问题就来了。
每一行星号都齐刷刷地贴在左侧边缘,无法形成菱形。
我通过Debug试图查看格式控制字符串str变量的值,发现其值如下:%:c
熟悉的ASCII码的读者应该已经能看出问题了,当数值为两位数时通过+48
的方法是无法直接将整型转换为对应的字符型的。
优化2
虽然作业要求输入的都是个位数,自动评判系统的示例数据也都是个位数,就这么提交上去亦不报错,但我想,编写一个程序不仅需要可行性还需要保证其健壮性。
为了应对更多的情况,我最终还是引入了<string.h>这个头文件为我提供一些便利。
实现思路为: 通过string.h当中的strcat函数拼合“%”、“a-i+1”、“c”这几个字符串构成完整的格式控制字符串。
新的问题随之产生,a-i+1这个数字怎么转换成字符或者字符串的形式呢?
习惯了在其他更为高级的语言当中可以直接转换(例如Python的str()
),C语言中就摸不着头脑了。
优化3
经过查阅资料,我们可以使用sprintf
这个函数间接实现格式转换,至于为什么是间接呢?sprintf本意应该是将原本printf的输出内容放入指定的变量中,可实现的功能可不仅仅是格式转换这么简单。
于是乎
他来了
代码附上
char str[]="%";
int num=a+1-i;
char num_str[15];
char end[]="c\0";
sprintf(num_str,"%d",num);
strcat(str, num_str);
strcat(str, end);
printf(str,'*');
int j;
for(j=1;j<2*i+1;j++){
printf("%c",'*');
}
printf("\n");
至此,核心算法可以说是大功告成了。
细节完善
关于行数的控制、循环变量的设定等等各种细枝末节的调试过程此处就不再赘述了。
咱们直接
上!代!码!
#include<stdio.h>
#include<string.h>
/*方法二:利用格式控制字符进行输出*/
int loop(){
int a;
scanf("%d",&a);
for(int i=0;i<=a;i++){
char str[]="%";
int num=a+1-i;
char num_str[15];
char end[]="c\0";
sprintf(num_str,"%d",num);
strcat(str, num_str);
strcat(str, end);
printf(str,'*');
int j;
for(j=1;j<2*i+1;j++){
printf("%c",'*');
}
printf("\n");
}
for(int i=a-1;i>=0;i--){
char str[]="%";
int num=a+1-i;
char num_str[15];
char end[]="c\0";
sprintf(num_str,"%d",num);
strcat(str, num_str);
strcat(str, end);
printf(str,'*');
int j;
for(j=1;j<2*i+1;j++){
printf("%c",'*');
}
printf("\n");
}
}
此段代码经过测试,可以正常运行。
效率对比
测试过程
为方便对比,上面的两种方法我都封装为了函数,而在main函数中循环调用100次上述代码,每次调用固定a的值为59,以扩大差异。
为什么是59?
经过测试可知,以测试用的计算机命令行窗口默认大小,输入60或者更多星号就会自动换行,影像观看效果。
最终测试结果如下(单位为秒,所计为运行100次所用的总时长)
方法一 | 方法二 |
119 | 132 |
可知方法二效率更低一下
为排除偶然性,每次运行100趟,总共分别运行了3次结论均一致(相应数据当时没有留存)
效率低原因分析
目前根据个人浅薄的知识猜测,导致效率低下的主要症结应该在于字符串的处理过程比较耗时。
写在最后
小结
虽然方法二效率更低,但是也不失为一种新的思路,而且在这样的小程序当中效率的差异并不会体现得很明显。
探索方法二的过程让我收获了丰富的程序调试经验,让我学会了从极少的提示信息中快速地找到问题所在,也让我对C语言的原理有了更为深入地了解。
程序调试过程当中每一步的解决方案现在想来或许不是最佳,原因推测或许也有偏差,但在当时编写程序的过程中,想到就去做了,最终也就做成现在这样子。
程序还有许多需要优化的地方,有待于进一步发掘、优化,说不定优化完成后方法二能够赶超方法一呢?
特别说明
- 本文中除代码末尾标注的两段代码以外,其余所有代码均未经测试验证,可能有细微错误,仅用于表示大致思路。
- 本文内容为事后复盘回忆而来,可能与实际过程有细微的出入。
- 转载请注明出处。
最后的最后
想必还有其它各种方法,欢迎读者朋友各抒己见,若有错误欢迎批评指正。
彩蛋
以下是使用ChatGPT写的代码(经过一定的人为调整以便于程序测试)
#include <stdio.h>
int loop() {
int n=59;
for (int i = 0; i < 2 * n + 1; i++) {
int spaces = (i <= n) ? n - i : i - n;
int stars = 2 * n + 1 - 2 * spaces;
for (int j = 0; j < spaces; j++) {
printf(" ");
}
for (int j = 0; j < stars; j++) {
printf("*");
}
printf("\n");
}
}
经测试,其100次运行总时间为121
秒
其思路与本文法一接近,效率亦比较接近,关键是代码长度更短,逻辑也更为清晰,可读性更强。
结论是:还得是人工智能_
来对比一下代码长度(包含测试用的一些代码和函数间的空行)
方法一:42
方法二:48
ChatGPT:26