简介:本文面向C语言初学者,精讲四道高频入门题:用双栈完成表达式求值、以枚举加拆位统计数位和、借助嵌套循环与printf格式控制打印乘法表、将二分查找模板适配降序数组。读者可以掌握栈的应用、整数拆位、格式化输出与二分变体的完整推导过程,并沉淀出一套分析问题的通用方法论。
刚接触C语言的大一新生,常常上课听得懂语法,一到独立写题就无从下手。本文挑选四道最具代表性的入门题——表达式求值、数位求和、九九乘法表、二分查找,逐一拆解思路、给出可落地的代码框架,并解释每一步“为什么这么写”。读完之后,你收获的不只是四道题的答案,而是一套可迁移的分析方法。
栈(Stack)是一种“后进先出”(LIFO)的线性数据结构:只允许在一端(栈顶)插入(push)和删除(pop)元素。可以把它想象成一摞盘子——放盘子放在最上面,拿盘子也从最上面拿。栈在表达式求值、函数调用、括号匹配等场景中都是核心工具。如果对栈还不熟悉,建议先查阅任意一本经典的数据结构教材或权威的算法学习资料,把 push、pop、top 三个基本操作弄清楚,再回到本题,会顺畅得多。
题目给的输入通常是一个表达式字符串,而不是现成的数字。很多新手卡在“我不会输入”。思路其实很直接:
数字字符转数值的通用技巧是利用字符编码的连续性:c - '0' 即得到该字符对应的 0~9 数值。整数部分按“高位累乘”的方式构造,例如 “123” 依次执行 val = val*10 + 1、val = val*10 + 2、val = val*10 + 3。小数部分则每多一位就把除数扩大 10 倍。
生活中最常见的是中缀表达式,如 (1 + 1) * 2,运算符写在两个操作数中间;而后缀表达式(也称逆波兰式)把运算符放在操作数之后,如 1 1 + 2 *,两者完全等价。
后缀表达式最大的特点是:不需要括号。因为扫描到运算符时,它作用的对象就是“最近出现的两个操作数”,含义天然无歧义。这一特性使得后缀表达式非常适合用栈求值,也解释了为什么编译器内部常把中缀转成后缀再处理。
要在中缀表达式上直接求值,经典做法是“运算符优先级 + 双栈”:
#),并把它的优先级设为最低,目的是让扫描结束时能强制清空符号栈、让开头能统一处理;这里有一个细节必须强调:对于同优先级的连续运算(如 8-3-2 或 8/4/2),要按“栈顶优先级 ≥ 当前优先级就先算”处理,才能保证左结合的正确性,否则 8-3-2 会被错误地算成 8-(3-2)=7。
整个算法的直觉是:优先级高的运算“等不起”,必须先算;优先级低的运算先“压栈等待”,等右边的操作数齐了再回头算。理解了这一点,中缀转后缀和直接求值其实是同一套逻辑的两种输出形式。
第二题要求统计某范围内满足“各位数字之和等于 15”的三位数个数。思路分两步:枚举和拆位。
数据范围只有三位数,直接从 100 枚举到 x,对每个数逐一判断即可。范围小的时候,“暴力枚举”不是偷懒,而是最稳妥的策略——代码短、不易错、边界好验证。
判断“各位数字之和”需要把每一位取出来。三位数可以直接硬拆:
int ans = 0;for (int i = 100; i <= x; ++i) {// i % 10 -> 个位// i / 10 % 10 -> 十位(先除以10去掉个位,再对10取余)// i / 100 -> 百位(三位数直接除以100即可)if (i % 10 + i / 10 % 10 + i / 100 == 15)ans++; // 满足条件,答案加一}
这里依赖两个整型运算的性质:% 是取余;/ 在两个整数之间是整除(直接舍去小数部分),所以完全不用担心出现浮点数。
如果想取第 j 位(从 0 开始计数,0 是个位),通用写法是:
int digit = i / (int)pow(10, j) % 10;
原理:先除以 10 的 j 次方,把第 j 位“降”到个位,再对 10 取余取出它。注意幂函数返回浮点数,需要强制转换成整型,避免浮点误差带来的边界问题;更稳妥的做法是自己写循环累乘 10 来代替。
打印乘法表的关键是先观察图形特征:
for (int i = 1; i <= 9; ++i) { // 枚举第一个乘数(行号)for (int j = 1; j < i; ++j)printf(" "); // 补齐被跳过算式的占位空格for (int j = i; j <= 9; ++j) {printf("%dx%d=%2d ", i, j, i * j);}printf("\n"); // 每行结束换行}
对齐是本题最容易丢分的地方。printf 的格式控制可以帮我们省去手写对齐逻辑:
%2d:输出整数,宽度不足两位时用空格在左侧补位;%02d:宽度不足两位时用 0 补位;这类“算宽度”的题,建议先在纸上画出前两三行的样子,数清楚每个字段占几列,再动笔写代码,比反复提交试错高效得多。
二分查找利用数组的有序性,每次把搜索区间缩小一半,时间复杂度为 O(log n)。标准模板针对升序数组:
int lo = 0, hi = n - 1;while (lo <= hi) {int mid = lo + (hi - lo) / 2; // 防溢出写法if (a[mid] == target) return mid;else if (a[mid] < target) lo = mid + 1; // 升序:目标在右半边else hi = mid - 1;}return -1;
本题数组是降序排列,此时“比中间元素大”意味着目标在右半边,比较方向恰好相反:
if (a[mid] == target) return mid;else if (a[mid] > target) lo = mid + 1; // 降序:大的在左,目标在右else hi = mid - 1;
只需交换两个分支的移动方向,其余结构不变。这个练习的价值在于提醒我们:模板不是背下来的,而是理解“每一步为什么往这个方向收缩区间”之后推出来的。
lo + (hi - lo) / 2,避免 lo 与 hi 直接相加导致整型溢出;另外,不少语言的标准库都提供了现成的有序查找接口,工程中可以直接使用;但作为初学者,手写一遍二分是理解“有序性如何被利用”的必经之路。
把这四道题放在一起看,能提炼出几条对初学者普遍适用的经验:
编程入门阶段,“卡住”是常态。把每一道卡住的题彻底讲清楚——为什么这样想、为什么这样写、换个条件还成立吗——比囫囵吞枣刷十道题更有价值。坚持这种复盘方式,语法会内化成直觉,题目会变成套路,入门期自然会平稳度过。