首页 / 资讯中心 / 文章详情

C语言进阶必刷:循环、数组、指针、递归与文件操作综合练习

C语言进阶必刷:循环、数组、指针、递归与文件操作综合练习 ★ FEATURED ARTICLE
先解释一下这组题的设计逻辑51到60正好卡在C语言学习的分水岭上。前面的题目帮你把语法点逐个认了个脸熟到这组题循环要玩出花数组要当工具使指针和字符串开始真正产生配合递归和文件操作也要上手。我把这十道题按知识点重新排了序不是为了凑题量而是让每道题都踩在前后题的思路上练完一遍你会明显感觉到写代码的时候脑子里有结构了。1. 循环嵌套与数字规律第51、52题的思路拆解1.1 第51题输出左下三角九九乘法表题目要求其实很朴素用C语言在终端输出一个规范的九九乘法表。别小看这道题它考察的循环嵌套、循环边界控制、格式化输出几乎是后续所有复杂循环结构的地基。#include stdio.h int main(void) { for (int i 1; i 9; i) { for (int j 1; j i; j) { printf(%d*%d%-4d, j, i, i * j); } putchar(\n); } return 0; }这里有两处细节最容易被新手忽略。第一个是内层循环的边界j i它决定了我们输出的是左下三角而不是完整的9x9矩阵。如果你写成j 9输出的就是完整方形布局完全不对。第二个是%-4d里的负号它的作用是左对齐并占用4个字符宽度。很多初学者用\t制表符对齐效果通常不理想因为每项内容的宽度不同制表位会错位。实测下来%-4d是最省事、最稳定的对齐方式。输出效果1*11 1*22 2*24 1*33 2*36 3*39 ... 1*99 2*918 3*927 ...如果你手头用的是Windows下的老式控制台可能字宽显示会有细微差异遇到这种情况不用纠结优先保证逻辑正确。另外putchar(\n)和printf(\n)在这道题里效果一样但putchar更轻量也顺便帮你复习一下字符输出函数。1.2 第52题找出1000以内的所有完数完数是个数学概念指一个数恰好等于它的真因子之和。最经典的例子是6因为6 1 2 3而28 1 2 4 7 14。题目让你找出1000以内所有完数这道题的价值不在于完数本身而在于你怎么高效地求因子和这直接关系到你对整除、循环范围、数字对称性的理解。#include stdio.h int main(void) { int n; for (n 2; n 1000; n) { int sum 1; // 1 一定是真因子 for (int i 2; i * i n; i) { if (n % i 0) { sum i; if (i ! n / i) { // 避免因子重复累加 sum n / i; } } } if (sum n) { printf(%d\n, n); } } return 0; }这里有个关键优化判断因子时循环范围只需要到i * i n。为什么因为因子总是成对出现的比如28的因子2对应14一旦你找到了小于等于根号28的那个因子另一个因子必然可以通过n / i算出来。如果写成i n / 2虽然也能得到正确答案但内层循环次数多了一倍多这就是能算出结果和算得漂亮之间的差别。我在实际练习中发现很多人第一版代码会写成这样for (i 1; i n; i) { if (n % i 0) sum i; }这段代码逻辑完全正确但显然不够聪明。如果你想在面试或者平时练习中体现出对数学的敏感度对称因子法一定得掌握。再注意一个细节if (i ! n / i)这行是防止n本身是某个数的平方比如36因子6被累加两次的情况。对于完数的题来说n36不是完数但养成这个判断习惯以后很多题都用得上。运行结果1000以内只有3个完数6、28、496。2. 数组的三种典型场景排序、查找与进制转换2.1 第53题冒泡排序的两种写法与提前终止冒泡排序是C语言初学者第一次系统性接触算法的入口网上各种教程多到爆炸但很多都停留在能跑就行的层面缺少对过程本身的解释。题目要求很简单写一个函数对一个整型数组进行升序排序。#include stdio.h void bubble_sort(int a[], int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (a[j] a[j 1]) { int tmp a[j]; a[j] a[j 1]; a[j 1] tmp; swapped 1; } } if (!swapped) { break; } } } int main(void) { int a[] {5, 2, 9, 1, 7}; int n sizeof(a) / sizeof(a[0]); bubble_sort(a, n); for (int i 0; i n; i) { printf(%d , a[i]); } putchar(\n); return 0; }两个容易写错的地方第一内层循环j n - 1 - i里的减i很多人会漏。每完成一轮外循环数组中最大的数就已经被冒到最后面了下一轮完全没必要再去碰已经固定好的位置。不减i也能运行但白白多做了无意义的比较。第二swapped标志位是为了处理已经有序的数组如果某一轮没有任何交换说明整个数组已经排好直接跳出外层循环。比如输入是{1, 2, 3, 4, 5}第一轮下来没有交换立刻结束省掉剩下所有轮次。如果你想加深理解建议自己加一行printf把每轮排序后的数组打印出来观察5, 2, 9, 1, 7是怎么一步步变成1, 2, 5, 7, 9的。看到过程比看结论重要得多。2.2 第54题5x5矩阵鞍点查找的行列思维鞍点问题是个经典二维数组题在一个5x5矩阵中如果一个元素满足在它所在的行上是最大值在它所在的列上是最小值就称它为鞍点。题目要求输出所有鞍点的位置和值如果没有鞍点则输出提示。这道题真正的考点在于你得先把行最大和列最小这两件事拆开处理。新手常见的思路错误是边扫描边比较结果把自己绕晕了。正确的做法是分两步走第一步固定一行先找到这行最大元素所在的列号第二步检查这一列上所有元素是否都大于等于这个元素如果是它就是鞍点。#include stdio.h #define ROW 5 #define COL 5 int main(void) { int a[ROW][COL]; printf(请输入5行5列的矩阵\n); for (int i 0; i ROW; i) { for (int j 0; j COL; j) { scanf(%d, a[i][j]); } } int found 0; for (int i 0; i ROW; i) { // 第一步找出第 i 行最大元素所在的列 int col 0; for (int j 1; j COL; j) { if (a[i][j] a[i][col]) { col j; } } // 第二步检查该列上每个元素是否都不小于它 int is_min 1; for (int r 0; r ROW; r) { if (a[r][col] a[i][col]) { is_min 0; break; } } if (is_min) { printf(鞍点a[%d][%d] %d\n, i, col, a[i][col]); found 1; } } if (!found) { printf(不存在鞍点\n); } return 0; }我提醒一个很容易忽略的场景如果某一行有两个并列的最大值上面的写法只检查了第一个最大值的位置。严格说所有等于行最大值的元素都应该被验证是否是列最小。但这道题通常不考这个边界如果你想做的严谨可以把第一步改成遍历该行所有列对每个等于最大值的列都做一次列检查。这个扩展留给你自己尝试。另外#define ROW 5 COL 5这种宏定义方式值得养成习惯以后如果题目变成8x8矩阵你只改宏就行不用动逻辑代码。别忘了scanf的返回值最好检查一下写成if (scanf(...) ! 25) { ... }能拦截掉输入格式错误的情况。2.3 第55题十进制转任意进制的除基取余进制转换是计算机组成原理里的基础话题也是C语言练习里一道很综合的题。题目要求输入一个十进制正整数输出它对应的二进制、八进制和十六进制。核心算法是除基取余逆序排列不断用目标进制数去除原数取余数直到商为0然后把所有余数倒着写出来。#include stdio.h void convert(int n, int base) { char digits[] 0123456789ABCDEF; char result[32]; int i 0; if (n 0) { putchar(0); putchar(\n); return; } while (n 0) { result[i] digits[n % base]; n / base; } while (i 0) { putchar(result[--i]); } putchar(\n); } int main(void) { int n; printf(请输入一个十进制正整数); if (scanf(%d, n) ! 1) { printf(输入格式错误\n); return 1; } printf(二进制); convert(n, 2); printf(八进制); convert(n, 8); printf(十六进制); convert(n, 16); return 0; }这里最值得学习的点是用digits数组一次性解决了10和16的表示问题。十进制里余数最大是9但十六进制里余数能到15这个数组让你的convert函数在2到16之间任意进制通用。我用到的技巧本质上就是拿数组做映射表以后做字符转换类题目会经常碰到。输出时要注意顺序因为先算出来的余数是结果的最后一位所以存储时按顺序放输出时要从后往前。我用了两个while循环来分别完成存储和输出你也可以用一个定长数组加一个计数器来实现。顺手提一个实际应用嵌入式开发里经常要调试寄存器的值二进制和十六进制是常用表达方式能快速手算转换绝对是个加分技能。3. 字符串、指针与递归越过语法坎的三道题3.1 第56题字符串逆序的双指针写法字符串逆序题目要求不能额外开一个数组直接在原字符数组上完成逆序。朴素的做法是拿两个下标一个从开头走一个从末尾走交换头尾对应的字符直到两个下标相遇。#include stdio.h #include string.h void reverse_string(char s[]) { int i 0; int j strlen(s) - 1; while (i j) { char tmp s[i]; s[i] s[j]; s[j] tmp; i; j--; } } int main(void) { char s[100]; printf(请输入一个字符串); fgets(s, sizeof(s), stdin); s[strcspn(s, \n)] \0; // 去掉 fgets 读入的换行符 reverse_string(s); printf(逆序结果%s\n, s); return 0; }这段代码有三个隐蔽的坑。首先是strlen(s) - 1数组里最后一个字符是\0它不算在字符串有效内容里所以下标要从strlen(s) - 1开始否则会把结束符换到开头去打印结果会变得莫名其妙。其次是fgets会连换行符一起读进数组比如你输入hello再按回车数组里实际是h e l l o \n \0不处理这个换行符的话逆序结果会变成\nolleh。我用strcspn(s, \n)来定位换行符的位置并替换成\0这是目前比较安全的标准做法。第三while (i j)的终止条件意味着当字符串长度是偶数时两个下标会交错后退出长度是奇数时中间那个字符原地不动逻辑上都没问题。老教材里常用gets我明确建议不要用。gets不检查缓冲区边界输入一长串立刻栈溢出C11标准里已经把它删了。凡是劝你用gets的教程基本可以关掉了。3.2 第57题用函数封装两个变量的交换这道题表面上是写一个交换函数实际上考的是C语言最核心的值传递概念。函数要修改外部变量的值必须拿变量的地址。#include stdio.h void swap(int *a, int *b) { int tmp *a; *a *b; *b tmp; } int main(void) { int x 5; int y 10; printf(交换前x %d, y %d\n, x, y); swap(x, y); printf(交换后x %d, y %d\n, x, y); return 0; }如果你写成void swap(int a, int b)函数内部确实交换了但那是形参的交换main里的x和y纹丝不动。原因在于C语言默认是值传递函数拿到的是实参的一份拷贝。要想让函数修改实参只能传递实参的地址也就是指针。这正好呼应了指针语法里最常见的应用场景。我见过一些初学者把交换逻辑写在main函数里直接tmp x; x y; y tmp;这当然能实现但题目考察的核心就是你是否理解为什么要用指针。以后写链表的节点交换、树的旋转操作本质上都离不开这种传址思路。如果你希望代码更现代一点可以考虑用一个临时变量做交换之外的方法比如用加减法或异或运算来交换但那些方法你不需要记——临时变量法最简单、最不容易出错。面试场合用临时变量反而最稳妥。3.3 第58题递归实现斐波那契数列及优化方向斐波那契数列的定义是第1项和第2项都是1从第3项起每一项等于前两项之和。题目要求用递归实现第n项的求解并输出前n项。#include stdio.h int fib(int n) { if (n 1 || n 2) { return 1; } return fib(n - 1) fib(n - 2); } int main(void) { int n; printf(请输入项数); if (scanf(%d, n) ! 1) { printf(输入格式错误\n); return 1; } for (int i 1; i n; i) { printf(%d , fib(i)); } putchar(\n); return 0; }递归函数永远只需要考虑两件事终止条件和递归公式。这道题的终止条件是n等于1或2时返回1递归公式是fib(n-1) fib(n-2)代码本身干净利落。但我必须说这种写法性能极差求第40项就已经明显卡顿。原因很好理解fib(5)要算fib(4)和fib(3)而fib(4)又要算fib(3)和fib(2)同一个子问题被反复计算计算量呈指数级增长。为了让你理解优化的方向我给一个迭代版本int fib_iterative(int n) { if (n 1 || n 2) { return 1; } int a 1, b 1; for (int i 3; i n; i) { int next a b; a b; b next; } return b; }这个版本用滚动数组的思路只用两个变量就能完成计算时间复杂度从O(2^n)降到了O(n)。我在练习题答案里通常会把两种写法都给出因为递归版本用来理解递归的本质是函数自己调用自己迭代版本用来理解递归不一定是最优解复杂度过高时必须换思路。递归省事但费时间迭代省时间但费脑子这就是典型的时间复杂度权衡。以后再见到递归第一反应不应该是好简洁好喜欢而是这个递归有没有重复计算。4. 日期逻辑与文件操作贴近真实应用的关卡4.1 第59题输入日期计算它是这一年的第几天题目要求输入年、月、日判断这一天是那一年的第几天。这道题综合了数组、循环、分支、函数封装非常贴近实际业务逻辑。#include stdio.h int is_leap(int year) { return (year % 4 0 year % 100 ! 0) || (year % 400 0); } int main(void) { int year, month, day; printf(请输入年、月、日); if (scanf(%d %d %d, year, month, day) ! 3) { printf(输入格式错误\n); return 1; } int days[] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int total 0; for (int i 1; i month; i) { total days[i]; } total day; if (is_leap(year) month 2) { total 1; } printf(这一天是%d年的第%d天\n, year, total); return 0; }判断闰年的规则绝大多数人只记得四年一闰忘掉了百年不闰四百年再闰。上面is_leap的写法是标准答案能被4整除且不能被100整除或者能被400整除。这道题的关键是把每个月的天数预置进数组下标1到12分别对应一月到十二月下标0没用随便填个0占位。这样写比用switch-case逐个月累加清晰得多也更容易维护。什么时候需要加天数不是所有闰年都加而是闰年的2月之后才加。如果你输入的是2月28日总天数里不需要加因为2月28天已经包含在days[2]里了如果是3月1日那2月的天数应该是29所以要额外补上1天。这就是month 2这个判断条件的意义。再补一句易错点scanf的返回值和输入缓冲区的坑。如果你输入时误打了2024 3 1xscanf读到x时会失败day的值可能保持上一次的值。判断返回值不等于3时直接退出程序是最简单的防御性写法。4.2 第60题统计文本文件中各字母出现次数前59题都在和内存里的数据打交道第60题首次把文件系统拉进来。题目要求读入一个文本文件统计26个英文字母各自出现的次数忽略大小写并把结果打印到屏幕上。#include stdio.h #include ctype.h int main(void) { const char *filename hello.txt; FILE *fp fopen(filename, r); if (fp NULL) { perror(文件打开失败); return 1; } int count[26] {0}; int ch; while ((ch fgetc(fp)) ! EOF) { if (isalpha(ch)) { ch tolower(ch); count[ch - a]; } } fclose(fp); printf(字母出现次数统计\n); for (int i 0; i 26; i) { if (count[i] 0) { printf(%c: %d\n, a i, count[i]); } } return 0; }文件这块有三个容易出错的地方我挨个讲。第一fopen返回的指针必须检查是否为NULL。文件不存在、路径写错、没有读取权限都会导致返回NULL。这里我用perror输出具体的错误原因比简单地打印打不开更有用。第二fgetc的返回值类型是int不是char。因为EOF-1不是一个合法的字符值如果你把返回值存成char某些字符可能会和EOF混淆导致循环提前结束或者死循环。我见过不少人在这个细节上栽过跟头。第三tolower和isalpha这两个函数声明在ctype.h里。它们接收和返回的都是int所以把ch传给它们没问题。ch - a这种写法利用的是字符编码中字母的连续性在ASCII环境下完全正确。我在本地建了一个hello.txt内容写的是英文短句跑完程序后输出类似a: 12 e: 18 h: 3 ...如果你手头没有合适的测试文件可以直接用程序配合fprintf自己生成一个或者用重定向输入。这道题练完你对文件通道的打开、读取、处理、关闭这个完整流程就有肌肉记忆了。5. 从51到60这一阶段练完该怎么继续十道题过完你不妨自己复盘一下哪类题卡壳了如果是九九乘法表和完数这类循环题卡壳说明你对循环边界的控制还不熟练建议不要急着往后刷把50题之前的循环题再动手敲一遍。如果冒泡和鞍点卡壳那是数组下标和两层循环配合的问题可以自己手动模拟几遍小数组的执行过程比如拿张纸把每一步i、j的值和数组里所有的变化写出来。如果指针交换题卡壳说明你对地址和值的关系还不够清晰这是C语言最核心的坎必须反复磨。递归题卡壳是正常的所有人都觉得递归绕但只要你记住终止条件递归公式这个万能模板多看几道题就能适应。文件题卡壳大概率是环境问题可能是文件路径写错也可能是文本文件的编码不对先检查这两项。这一阶段练完下一个阶段该碰什么我个人建议三件事一是结构体把若干数据打包成一种新类型这是C语言走向项目开发的必经之路二是动态内存分配malloc和free会打开你对堆内存的理解三是链表把指针和结构体结合起来你会突然明白指针为什么是C语言的灵魂。当然函数指针、位运算、多文件编译这些深度内容也都是好方向每一样都比空刷题有意义。最后分享一个我自己的练习习惯每做完一组题我会把里面最让自己卡壳的题目抽出来隔两周再做一遍并且故意换一种解法。比如冒泡排序第一遍用标志位优化第二遍我可能改成从后往前冒泡进制转换第一遍用数组存余数第二遍改成递归输出。用不同的方法解同一道题往往比你做十道新题更能暴露问题。编程能力的提升不是靠看答案解析堆出来的而是靠一遍遍亲手验证、出错、修正堆积出来的。
阅读完成 · 觉得有帮助?
咨询建站