考试通知
C语言经典题61-70精讲:数组、递归、指针、结构体与链表实战 手上一套一百道的C语言经典题库不少朋友刷到61到70这个区间就卡住了。前面六十题基本属于“语法热身”分支、循环、数组的读写占了一大半到了61到70这批题局面忽然变了递归、指针、结构体、链表开始轮番上阵。这个节点刷没刷透直接影响后面学队列、二叉树和基础算法时能不能看懂代码。这里我把61-70的题目重新整理成数组与字符串、递归、指针与结构体、链表四个小块每道题都给了可以直接编译运行的答案代码并且把我这些年讲C语言时学生们踩得最狠的坑标了出来。如果你正学到函数和结构体对“传值和传地址”的关系还没完全理顺那这篇应该能帮你把概念落到实处。1. 这批题目卡在哪个阶段从“会写句子”到“会搭结构”61-70这批题不同于前面单纯练语法它们的核心是把多个知识点组合起来用。数组题不只要会下标访问还得处理边界递归题不只要会写出函数还得意识到“出口”和“调用栈”这两件事结构体题开始模拟真实业务里的“一条记录”链表题则要求你手动管理动态内存。如果把这十道题做完你对C语言的理解会从“能编译通过”变成“知道这段程序跑起来后内存里是什么样”。我把这十道题目按知识点的难度做了一个表格方便你先建立整体印象题号核心主题主要知识点建议完成时间61数组元素原地逆置下标运算、交换法15分钟62一行字符的分类统计getchar读入、字符区间判断15分钟63一维数组冒泡排序双重循环、边界控制25分钟64递归求n的阶乘递归出口、递推关系15分钟65递归求斐波那契数列第n项重复子问题、记忆化20分钟66汉诺塔移动步骤输出递归分治思想30分钟67用指针交换两个变量地址传递与值传递对比15分钟68结构体数组存放学生成绩结构体定义、字符输入20分钟69结构体数组按成绩排序整块交换、排序应用25分钟70单链表建立与输出malloc动态分配、链式访问30分钟从这个表能看出来61-70的题目并不是突然变难而是在反复强调“程序结构”这四个字。数组和字符串练的是下标和边界递归练的是自顶向下拆解问题结构体练的是把相关数据封装成整体链表练的是用指针把零散的结点串起来。每道题都值得亲手敲一遍光看答案代码效果会打一半折扣。2. 数组与字符串三连第61-63题先把基础手感捡回来很多人刚进入这组题时最大的感觉是“题目我能看懂但一写代码就出错”。这很正常因为数组题真正考验的不是你有没有背住语法而是你是否理解数组在内存里是一段连续空间以及下标为什么必须从0开始。2.1 第61题数组原地逆置别急着开新数组题目从键盘输入10个整数把它们逆序重新存放再输出。这道题有两类做法。一类是开一个新数组倒着拷贝回去另一类是原地逆置直接在一个数组内交换首尾元素。我推荐你做第二种因为它对空间更友好而且能逼你把下标关系想清楚。#include stdio.h int main() { int a[10]; int i, temp; printf(请输入10个整数); for (i 0; i 10; i) { scanf(%d, a[i]); } for (i 0; i 10 / 2; i) { temp a[i]; a[i] a[9 - i]; a[9 - i] temp; } printf(逆序后的数组为); for (i 0; i 10; i) { printf(%d , a[i]); } printf(\n); return 0; }运行效果是这样的输入1 2 3 4 5 6 7 8 9 10输出10 9 8 7 6 5 4 3 2 1。这道题的易错点集中在循环次数上。如果写成for (i 0; i 10; i)那么循环到中间时前半段和后半段会互相交换两次相当于又交换回去结果数组根本没变。正确写法是循环10 / 2次也就是5次。我第一次教这个知识点的时候有个学生把循环写成了i 10 / 2结果是下标越界成了未定义行为程序运行结果时对时错排查半天才发现边界多跑了一次。记住一句话两端向中间靠拢中间位置不需要处理。2.2 第62题字符计数最容易错的是“回车”题目输入一行字符分别统计英文字母、空格、数字和其他字符的个数。这道题要用getchar()循环读取每次读一个字符直到读入换行符才结束。字符判断直接用ASCII区间比较代码非常直白#include stdio.h int main() { char c; int letters 0, spaces 0, digits 0, others 0; printf(请输入一行字符); while ((c getchar()) ! \n) { if ((c a c z) || (c A c Z)) { letters; } else if (c ) { spaces; } else if (c 0 c 9) { digits; } else { others; } } printf(英文字母%d\n, letters); printf(空格%d\n, spaces); printf(数字%d\n, digits); printf(其他字符%d\n, others); return 0; }输入hello 123 world!输出是“英文字母10空格2数字3其他字符1”。这道题有两点值得讲透。第一很多人会把结束判断写成c ! EOF结果程序永远停不下来因为从键盘输入的一行结束后是\n不是EOF。除非你在终端里按CtrlZ或CtrlD否则EOF不会出现。第二如果你在这道题之前用过scanf比如先输入了一个数字再运行这个getchar()循环那么scanf留在输入缓冲区里的换行符会被getchar()第一次就读走程序直接结束。解决办法是在scanf之后补一个getchar()清掉残留回车或者统一用fgets读整行再做字符遍历。这个小坑几乎每隔几届学生就会踩一次实属经典。2.3 第63题冒泡排序的边界条件再掰扯一遍题目输入10个整数按从小到大排序后输出。排序算法有很多种60-70这个阶段最合适的就是冒泡排序。它的思路就是每轮把相邻两个元素中较大的往后挪一轮结束后当前范围里最大的元素会沉到末尾。#include stdio.h int main() { int a[10]; int i, j, temp; printf(请输入10个整数); for (i 0; i 10; i) { scanf(%d, a[i]); } for (i 0; i 9; i) { for (j 0; j 9 - i; j) { if (a[j] a[j 1]) { temp a[j]; a[j] a[j 1]; a[j 1] temp; } } } printf(排序后的数组为); for (i 0; i 10; i) { printf(%d , a[i]); } printf(\n); return 0; }排序结果就不单独演示了输入数字后按升序输出即可。这道题真正要理解的是两层循环的边界外层i 9因为10个数最多需要9轮内层9 - i因为每一轮已经确定好了一个末尾元素下一轮就不用再碰它。不少初学者会把内层写成j 9或者j 9 - i前者多做了无意义的比较后者则可能在j 1时访问到下标10越界。如果你想让代码更健壮一点可以加入一个“本轮是否有交换”的标志变量如果某一轮完全没有发生交换说明数组已经有序提前退出外层循环这在数据基本有序时能省下一大半时间。3. 递归三题第64-66题的“出口”到底该写在哪递归是很多人的第一道坎。其实递归就两个要素出口和递推式。出口决定递归什么时候停下来递推式决定怎么把大问题拆成小问题。三题放在一起做能让你对这两个要素形成肌肉记忆。3.1 第64题用递归求阶乘先想清n1这一刀题目用递归方法求n的阶乘n从键盘输入。阶乘的递推式是n! n * (n-1)!出口是0! 1或1! 1。写代码时出口必须写在函数开头。#include stdio.h long long factorial(int n) { if (n 1) { return 1; } return n * factorial(n - 1); } int main() { int n; printf(请输入n); scanf(%d, n); printf(%d! %lld\n, n, factorial(n)); return 0; }输入5输出120。这里特别强调一句出口尽量写成n 1不要只写n 1。因为如果调用时n是0factorial(0)会因为0 1恒为假而一直递归下去直到栈溢出。负数同理。虽然实际输入可能都是正整数但函数设计时把边界情况考虑全是一种专业习惯。另外一个实用建议阶乘增长极快long long也只能存到20!左右再往上就会溢出。如果想扩大范围可以用double换来精度损失或者手工写大数乘法但那已经超过本组题的范围了。如果只是练习递归n控制在20以内就够了。3.2 第65题斐波那契数列的重复计算与优化意识题目用递归求斐波那契数列第n项n不超过40。斐波那契数列从第1项开始是1、1、2、3、5、8...递推式是fib(n) fib(n-1) fib(n-2)出口是第1项和第2项都返回1。#include stdio.h long long fib(int n) { if (n 2) { return 1; } return fib(n - 1) fib(n - 2); } int main() { int n; printf(请输入n); scanf(%d, n); printf(第%d项 %lld\n, n, fib(n)); return 0; }输入10输出55。这道题如果只做到“能跑”还不够因为里面的重复计算非常严重。你想想fib(5)要调用fib(4)和fib(3)而fib(4)又去调用fib(3)和fib(2)这里的fib(3)被算了两次。越往上被重复调用的次数越膨胀。我实测过n到40时普通递归已经明显发卡等到n到45程序基本就卡在原地了。解法其实很简单用一个数组把已经算过的结果缓存起来下次再遇到直接返回。这种思想叫记忆化也可以直接用循环从第3项往后推。我不要求初学者立刻掌握动态规划但至少要知道“递归也有性能问题”这为后面学算法优化埋个伏笔。3.3 第66题汉诺塔递归三步走题目有三根柱子A、B、CA上有n个盘子要求把所有盘子移动到C上每次只能移动一个盘子且大盘不能压在小盘上打印移动步骤。汉诺塔的递归思路特别简单。你不需要盯着细节模拟每一步只需要想三件事把上面的n-1个盘子从A借助C移到B把第n个盘子从A直接移到C把B上的n-1个盘子借助A移到C写成代码就是#include stdio.h void hanoi(int n, char a, char b, char c) { if (n 1) { printf(%c - %c\n, a, c); return; } hanoi(n - 1, a, c, b); printf(%c - %c\n, a, c); hanoi(n - 1, b, a, c); } int main() { int n; printf(请输入盘子数); scanf(%d, n); hanoi(n, A, B, C); return 0; }输入3输出A - C A - B C - B A - C B - A B - C A - C很多初学者被汉诺塔吓住是因为老想着“计算机到底怎么一步一步走的”。其实递归的价值恰恰是不需要你手动模拟每一步。你只要确保“把n-1个盘子移走”这个子问题能被正确解决剩下的自然就对了。验证时可以先从n1试再n2再n3逐步增加。顺带说一句汉诺塔的移动次数是2的n次方减1。所以n别输入太大到20就有上百万次移动屏幕刷到停不下来。做题的时候用3到5个盘子验结果就足够了。4. 指针与结构体实战第67-69题把数据“挂”起来从结构体开始C语言的数据组织能力跃升了一个档次。一个结构体变量可以同时携带多个字段这就像现实中的一条记录。第67题先补一下指针传参的底子后面两道题才能顺下去。4.1 第67题值传递和地址传递的差别在这道题里最明显题目编写一个函数交换两个变量的值并在main函数里输出交换前后的结果。很多初学者刚看到这道题会想这有什么难的直接写一个swap函数把两个变量传进去不就行了。但运行之后发现main里的a和b根本没变。问题出在C语言默认是值传递。#include stdio.h void swap(int *p, int *q) { int temp; temp *p; *p *q; *q temp; } int main() { int a 5, b 10; printf(交换前a %d, b %d\n, a, b); swap(a, b); printf(交换后a %d, b %d\n, a, b); return 0; }输出交换前a 5, b 10 交换后a 10, b 5关键点在于swap(a, b)传入的不是a和b的值而是它们的地址。函数内部*p和*q是直接通过地址访问main里的变量所以修改能生效。如果写成void swap(int a, int b)函数内部交换的只是形参的一份拷贝函数一结束拷贝就没了。这个理解不牢固的话后面学链表操作时会出现更大的问题你往链表头插一个新结点如果只传头指针的值而不是传头指针的地址插入操作对main里的链表完全没影响。4.2 第68题结构体数组里的平均分和及格线扫描题目从键盘输入5个学生的学号、姓名、成绩计算平均成绩并输出所有不及格学生的学号、姓名和成绩。这道题把结构体数组和循环结合起来了。先定义结构体类型再声明一个长度为5的结构体数组#include stdio.h #define N 5 struct Student { int id; char name[20]; float score; }; int main() { struct Student stu[N]; int i; float sum 0.0, avg; printf(请输入%d个学生的学号、姓名、成绩\n, N); for (i 0; i N; i) { scanf(%d %s %f, stu[i].id, stu[i].name, stu[i].score); sum stu[i].score; } avg sum / N; printf(平均分%.2f\n, avg); printf(不及格学生名单\n); for (i 0; i N; i) { if (stu[i].score 60) { printf(%d %s %.2f\n, stu[i].id, stu[i].name, stu[i].score); } } return 0; }输入样例前三行示意1001 Alice 88 1002 Bob 57 1003 Carol 72 1004 David 43 1005 Eve 91输出会显示平均分和不及格名单。一个小到容易被忽视、但面试时经常被问的知识点stu[i].name是字符数组名本身已经代表地址所以scanf里不需要再加。但stu[i].id和stu[i].score是普通变量必须加。你可以在代码里故意写一次stu[i].name会发现编译器报警告程序运行十有八九会崩。这种“一执行就段错误”的体验多半就是从这里来的。4.3 第69题结构体排序交换的是整块还是只换指针题目把第68题中的5个学生按成绩从高到低排序输出排序后的完整信息。排序逻辑和第63题的冒泡排序几乎一样区别在于比较时用score交换时要把整个结构体变量一起交换。#include stdio.h struct Student { int id; char name[20]; float score; }; int main() { struct Student stu[5], tmp; int i, j; printf(请输入5个学生的学号、姓名、成绩\n); for (i 0; i 5; i) { scanf(%d %s %f, stu[i].id, stu[i].name, stu[i].score); } for (i 0; i 4; i) { for (j 0; j 4 - i; j) { if (stu[j].score stu[j 1].score) { tmp stu[j]; stu[j] stu[j 1]; stu[j 1] tmp; } } } printf(按成绩从高到低排序\n); for (i 0; i 5; i) { printf(%d %s %.2f\n, stu[i].id, stu[i].name, stu[i].score); } return 0; }这里最关键的写法是tmp stu[j]。结构体变量可以直接整体赋值这是C语言的合法操作。有些初学者犯的典型错误是只交换了score字段没交换id和name结果排序后每个分数对应着别人的学号和名字数据完全错位。顺带可以想一想如果学生结构体里再增加一个长度为100的字符串字段每次交换整个结构体的开销就不小了。更好的做法是建一个指针数组只交换指针不移动结构体本身。这个优化你现在不用急着改但知道有这回事以后学链表和索引排序时会更容易理解。5. 第70题链表建立与输出的完整复盘第70题很多初学者不敢动手因为它第一次把“动态内存”和“指针链”放到一起。其实拆开看只有四个动作创建结点、链接结点、遍历结点、释放结点。5.1 建表和输出的核心代码题目从键盘输入n再输入n个整数建立一个单链表结点按输入顺序排列最后从头到尾输出链表中的数据。#include stdio.h #include stdlib.h struct Node { int data; struct Node *next; }; int main() { struct Node *head NULL; struct Node *tail NULL; struct Node *p; int n, val, i; printf(请输入结点个数); scanf(%d, n); for (i 0; i n; i) { scanf(%d, val); p (struct Node *)malloc(sizeof(struct Node)); p-data val; p-next NULL; if (head NULL) { head p; } else { tail-next p; } tail p; } printf(链表内容为); p head; while (p ! NULL) { printf(%d , p-data); p p-next; } printf(\n); p head; while (p ! NULL) { struct Node *q p; p p-next; free(q); } return 0; }输入4 10 20 30 40输出链表内容为10 20 30 40这段代码里tail指针很关键。第一次创建结点时头和尾都指向它之后每次新结点都挂在tail-next上然后更新tail。如果没有维护tail每次插入都要从头遍历找到最后一个结点链表一长效率就很差。5.2 三个到处都在踩的深坑第一忘了给新结点的next初始化为NULL。malloc分配出来的内存是不清零的如果你直接p-data val就给p-next赋值那么next可能指向一个随机的内存地址。遍历链表时循环根本无法正确判断“链表结束”轻则多打印一堆垃圾重则程序崩溃。正确的习惯是malloc之后立刻p-next NULL不管在哪个平台都这么做。第二输出链表时把头指针搞丢。有些同学喜欢直接用head指针遍历边遍历边往后挪结果输出结束后head已经指向了链表的尾部后面想再次遍历或者释放链表都不可能。一定要再定义一个临时指针p head让head始终指向链表第一个结点。第三释放链表时没有保存下一个结点的地址。标准做法是先用临时变量保存当前结点q p再把p移到下一个结点p p-next最后free(q)。如果先free当前结点再取p-next就会访问已被释放的内存属于典型的use-after-free问题。链表越大这类错误越隐蔽因为很多情况下程序看起来还能“正常运行”只是偶尔出错。5.3 调试链表题目的一套笨但有效的办法我自己的习惯是链表题写完后先不用急着看输出结果而是把代码中三处关键节点打印出来对比第一处是插入每个结点后打印head-data和tail-data确认头尾是否按预期移动第二处是遍历循环开始时打印当前结点的地址和值确认链是否串对了第三处是释放循环里打印即将释放的地址。这三处信息一出来问题基本立刻现形比反反复复读代码有效得多。另一个建议是画图。链表题不画图硬看代码是效率最低的学法。纸上画几个方框代表结点用箭头把next连起来然后把代码里每一行操作对应到箭头上你会发现原来晦涩的指针操作瞬间清晰。教了这么多年凡是觉得链表难的同学几乎都是因为不肯动手画图。写这批题的时候我最大的体会就是61到70真正拦人的不是某个语法点而是“你对内存有没有直觉”。数组题拦的是下标边界递归题拦的是出口意识结构体题拦的是数据组织思路链表题拦的是内存管理细节。把这些边界、出口、组织、释放一个个踩过又填平后面再去看队列、二叉树就不会再有“每个字都认识但读完还是不知道在干嘛”的空虚感了。