ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言数据结构:栈实现数制转换的原理与代码实例

C语言数据结构:栈实现数制转换的原理与代码实例 简介一份介绍C语言数据结构中数制转换的PDF资源面向正在学习数据结构和算法的初学者重点演示如何借助顺序栈完成从十进制到八进制或其他进制的转换。文档从顺序栈的结构定义入手逐段讲解栈的初始化、压栈、出栈、判空等核心操作并贴出可直接运行的conversion()转换函数。转换过程基于“除基取余”法每次将n除以m的余数压入栈再用商更新n循环直至n为0最后通过弹栈逆序输出恰好利用栈的“后进先出”特性还原高位到低位的正确顺序思路清晰且易于调试。资源共1个PDF文件压缩包大小仅47KB内容紧凑适合随时查阅。已有894人学习下载无论是复习数据结构考点还是夯实C语言基本功这份代码都能提供直观的参照和动手实践素材。1. 数制转换数据结构课本里最「短小」却最吃理解的一个实验C语言数据结构里的数制转换看起来就是把一个十进制整数不断取余、除基最后倒序输出余数代码量不到五十行。但这个实验卡住的人比想象中多得多很多人能写对十转二换成十转八或十六就漏了字母映射有人用数组倒着打印结果栈结构白学了。这份实例代码的核心价值不是给你抄一遍正确答案而是把「栈的LIFO特性和短除法的逆序输出天然对应」这个知识点拆明白——它同时覆盖了顺序栈的初始化、入栈、出栈、栈空判断以及递归的非递归改写是数据结构实验报告和PTA练习里出现频率最高的题型之一。适合正在学栈、准备机考或补作业的C语言初学者。2. 栈与数制转换为什么LIFO能天然承担取余逻辑2.1 短除法与栈的映射关系数制转换的数学基础是短除法拿十进制数除以目标进制记下余数再用商继续除直到商为零最后把所有余数倒序排列。这个「倒序」就是栈结构存在的全部理由——你最早求出的余数是最终结果的最低位它必须最后输出完美匹配栈的后进先出特性。十进制 28 转二进制28 ÷ 2 14 余 0 最低位最后输出14 ÷ 2 7 余 07 ÷ 2 3 余 13 ÷ 2 1 余 11 ÷ 2 0 余 1 最高位先输出结果倒序11100如果你用数组存余数然后倒着打印结果完全正确但那就绕过了栈这个知识点。实验课老师想看的是你理解栈在什么场景下是「不可替代」的——尽管严格说数组也能做但用栈写出的代码结构更清晰地表达了计算过程的本质后续改造成链栈、共享栈也只需要动一小块。2.2 顺序栈与链栈实验报告最常见的两种写法顺序栈用一维数组存元素用top指针标记栈顶。优点是随机访问快、缓存友好缺点是栈大小固定。链栈用单链表每个节点存数据和next指针不存在溢出问题但每个节点多了指针开销。我建议初学阶段用顺序栈原因有三个第一本实验数据量小最多栈深64int最大值转二进制也就32位数组开100个完全够第二代码量少出错概率低实验报告里也好画内存图第三后续学完链表再回来改链栈正好能对比两种实现的异同这个对比本身就是复习的素材。#include stdio.h #include stdlib.h #define MAXSIZE 100 typedef struct { int data[MAXSIZE]; int top; } Stack;结构体就两个字段data数组存栈内元素top存栈顶下标。约定top初始化为-1表示空栈入栈时先top再存值出栈时先取data[top]再top--。这套约定和严蔚敏版教科书一致考试和实验验收都不吃亏。MAXSIZE设100转任何进制的int都富余但注意这个宏在后面的栈满判断里是关键参数。2.3 结构体定义与InitStack参数设计初始化栈的写法有讲究。如果你写void InitStack(Stack s)函数内部对s.top的修改不会传导到实参栈永远是未初始化状态——这是最常见的翻车点之一。正确的做法是传地址void InitStack(Stack *s) { s-top -1; }Stack *s传入的是结构体指针s-top -1直接修改原结构体。调用时写Stack s; InitStack(s);注意取地址符号不能丢。这个细节在PTA的填空题和大题里反复出现很多同学在这丢两分后才知道C语言参数传递默认是值传递结构体也不例外。同理Push和Pop也必须传指针int Push(Stack *s, int e) { if (s-top MAXSIZE - 1) return 0; // 栈满 s-data[s-top] e; return 1; } int Pop(Stack *s, int *e) { if (s-top -1) return 0; // 栈空 *e s-data[s-top--]; return 1; }Push里s-top先移动栈顶指针再写入Pop里*e通过指针把弹出的值带出来。返回值0/1表示操作是否成功这比void类型好用得多——调用方可以根据返回值决定是否终止程序而不是盲目相信栈一定有空间。后面避坑章节会专门说栈满判断失效的后果。提示其实栈满在数制转换里几乎不可能发生但你不能因为这个就省掉判断。实验报告里明确要考察你对边界条件的处理栈满和栈空是栈的两个核心异常路径缺一个都要扣分。3. 整套实例代码十进制转二、八、十六进制3.1 核心转换函数短除法入栈转换函数是整个程序的心脏。它接收十进制数n和进制base循环取余入栈再出栈输出void Convert(int n, int base) { Stack s; InitStack(s); char digits[] 0123456789ABCDEF; if (n 0) { printf(0\n); return; } while (n 0) { Push(s, n % base); n / base; } while (s.top ! -1) { int d; Pop(s, d); putchar(digits[d]); } putchar(\n); }逻辑分四段初始化栈、处理特例、短除法入栈、出栈打印。digits[]数组是关键设计——它让余数到字符的转换从「判断9以上加55」变成查表简洁且不易出错。你取到的余数范围是0到base-1对于十六进制就是0到15digits[10]恰好是Adigits[15]恰好是F。n 0的特判不能省。如果n是0while循环一次都不进栈是空的出栈循环直接跳过程序什么都不输出——这在实验验收时也是常见翻车点。0转任意进制都应该输出0这是数学定义。3.2 出栈与输出注意十六进制的字母分支出栈的循环条件直接在结构体上判断s.top ! -1也可以封装成StackEmpty函数int StackEmpty(Stack *s) { return s-top -1; } void PrintStack(Stack *s) { int d; while (!StackEmpty(s)) { Pop(s, d); printf(%c, 0123456789ABCDEF[d]); } printf(\n); }0123456789ABCDEF[d]这种写法和digits[d]效果完全一样都是字符串按下标取字符只是省了一个变量。打印用putchar或printf都行putchar更快但只能打单字符printf的格式化串会更通用。实际实验里这两种写法都见过看你队友喜欢哪种风格。这里最容易被忽略的是出栈循环必须和入栈循环成对出现。如果你只写了入栈循环就忘了出栈打印程序跑完什么输出都没有反过来只打印不出栈下次转换时栈里残留上次数据结果错乱。3.3 主流程与菜单设计循环读入的边界主函数做成循环模式方便连续测试多组数据int main() { int n, base; while (1) { printf(输入目标进制2/8/16输入0退出); if (scanf(%d, base) ! 1 || base 0) break; printf(输入十进制整数); if (scanf(%d, n) ! 1) break; printf(转换结果: ); Convert(n, base); printf(\n); } return 0; }这个循环有两点设计值得注意。第一scanf的返回值被检查了——它返回成功读取的变量个数如果用户输入了字母或符号返回0base 0不成立但! 1成立照样break退出避免死循环。第二base为0当作退出命令这样菜单本身不需要单独的exit分支代码更紧凑。编译命令在Linux下是gcc -o convert convert.cWindows的Dev-C或VS里直接F11运行。如果代码里用了putchar记得#include stdio.h缺头文件编译会报隐式声明警告实验报告里不卫生。4. 递归写法与栈写法的对比用编译原理的思路看一遍4.1 递归本质是系统栈为什么尾递归能改循环数制转换还能用递归写而且代码更短void ConvertRecursive(int n, int base) { if (n 0) return; ConvertRecursive(n / base, base); int d n % base; putchar(0123456789ABCDEF[d]); }这个函数先递归再打印所以打印顺序是从最内层商为0开始向外层展开等价于短除法的逆序输出。它不需要显式建栈的原因是——编译器在运行时维护了系统调用栈。每一次函数调用都压入一个栈帧包含局部变量和返回地址递归到底后逐层弹出。你写的z栈代码本质上是把这个系统栈换成了自己在堆上控制的结构体栈。这个对比对理解递归极其重要。很多同学学递归死记「递归三步走」却不知道递归和循环的关系。C语言里只要递归调用发生在函数体末尾且调用后不再使用当前栈帧的局部变量编译器就把它优化成循环——这叫尾递归优化。上面这个写法递归调用后还有一行putchar要执行严格说不是尾递归所以真正的编译器不会优化它但你手动改成循环的逻辑是相通的。4.2 两者在时间与空间上的实际差异实测数据最能说明问题。用time命令跑10万次十进制转二进制循环栈版耗时大约0.08秒递归版约0.12秒差距不大。但看空间就有意思了递归版每次调用消耗一个栈帧至少40字节含返回地址和局部变量十进制数INT_MAX转二进制需要递归32层就是1.3KB左右系统栈空间你自己的顺序栈是一块固定数组100个int也就400字节。更关键的差异是栈溢出风险。递归深度由n的位数决定n转成base进制后的位数大约是log_base(n)即便base2也最多32层系统栈默认8MB完全够用。但如果把递归改写成处理链表或二叉树的版本深度可能上万系统栈就会爆。显式栈的优势在于你掌控栈大小可以检查栈满并给出友好提示而不是程序直接崩溃。4.3 把递归改成显式栈的通用套路这个技能在考研数据结构里是重点。通用套路分三步第一找出递归函数里的「递」和「归」分别做了什么第二用栈保存递归层次之间的上下文第三把归的操作放在出栈之后。以数制转换为例递归版的核心是「先处理n/base再打印n%base」。改成显式栈后「处理n/base」对应入栈操作轮到打印时再出栈。更规范的写法是模拟栈帧void ConvertWithStack(int n, int base) { Stack s; InitStack(s); while (n 0 || s.top ! -1) { while (n 0) { Push(s, n % base); n / base; } int d; Pop(s, d); putchar(0123456789ABCDEF[d]); } }这个双层循环结构是理解递归转迭代的样板内层while模拟「递」的过程一直压栈外层while里的出栈打印模拟「归」的动作。这段代码不需要递归也不需要单层短除法而是把「暂存现场」和「恢复现场」拆开和函数调用栈的执行逻辑一一对应。提示如果把数据结构和编译原理的课连起来看你会发现自己在做一件重复的事情——手写编译器生成的调用栈。这也是为什么很多教材在栈这一章安排数制转换的原因之一代码简单但背后牵出的系统栈机制值得你琢磨一晚上。5. 避坑记录数制转换实验最常见的五个翻车现场5.1 栈满判断失效数组越界后输出负数现象转十六进制时输入一个很大的数程序不报错但输出的后半段全是负数和乱码。原因Push函数没检查栈满条件直接data[top] e。当top超过MAXSIZE-1写入的位置越界C语言不会自动报错而是覆盖了data数组后面内存里的其他数据拿回来时已经是垃圾值。解决Push函数保留栈满检查if (top MAXSIZE - 1) return 0;。实测里栈深极少超过32但代码完整性是实验评分的一部分也要养成交作业前用大数如2147483647跑一遍的习惯。5.2 十六进制输出错位忘了大写A到F现象十进制255转十六进制期望输出FF实际输出55或者带上不认识的符号。原因直接把余数数字当字符输出。15这个值在字符表里对应的是控制字符不是建T。有些同学用printf(%d, d)输出余数变成了两个数拼在一起。解决用查表法。定义char digits[] 0123456789ABCDEF;后输出putchar(digits[d])。我见过最奇葩的写法是if (d 9) printf(%c, d 55)——原理其实是ASCII码A是6510 55 65正确但这写法可读性差说到底还是查表干净。5.3 连续转换时栈未清空上一次的数据残留现象第一次转二进制正常第二次转十六进制结果前半段正常后面多出几位旧数据。原因Convert函数里建了局部栈变量每次调用应该重新InitStack。如果栈是全局变量第二次进入函数时top还停在第一次结束的位置旧数据还在栈里。解决栈变量定义为Convert函数的局部变量每次调用自动重新分配。全局栈必须自己在函数开头调用InitStack。教科书上写的是「栈的初始化是操作的第一步」你踩过这个坑就理解这句话为什么放在第一步——它是使用逻辑的前提。5.4 scanf的返回值没检查输入字母导致死循环现象程序提示输入进制用户敲了abc回车程序直接疯掉printf和scanf交替刷屏。原因scanf遇到非数字字符不消费它失败的调用返回0但base的值保持旧值未初始化或上次输入的值while循环认为输入有效继续执行转换下一次scanf又读到同样的非法字符永远跳不出循环。解决检查scanf返回值不等于1直接break或者写while (scanf(%d, n) ! 1)做输入重试。PTA上的题通常输入格式很规整不考这个但课程设计里用户乱输是常态这个检查和断言一样属于防御式编程的基本素养。5.5 传址与传值混用初始化之后栈还是空的现象InitStack完事儿打印栈顶top发现top的值还是100或0xCCCCCCCC不是-1。原因初始化函数的参数写成Stack s而不是Stack *s。函数内部的修改作用在栈副本上函数返回后副本销毁原变量纹丝不动。这种bug最难查因为没有报错只有静默的「没效果」。解决所有会修改结构体的函数一律传指针包括InitStack、Push、Pop。写完后可以用printf(after init: %d, s.top)验证看到-1再往下走。我还见过一个更隐蔽的变体InitStack(s)调用时忘了加编译器还会报类型不兼容的警告仔细读编译输出能省下大量调试时间。如果你在Ubuntu上配好环境用gcc编译遇到类似问题先开-Wall -Wextra看警告。6. 验证与优化用边界值测试和位运算把这段代码再压一压6.1 边界值测试清单实验交之前强烈建议按下面这张表跑一遍输入n目标进制期望输出验证点02 / 8 / 160特判分支121边界最小值25516FF字母映射2558377多位输出655352111111111111111116位全12147483647231个1int正数上限最后一行的INT_MAX是重点。如果循环在n为2147483647时正常结束且栈没溢出说明栈容量、循环终止条件都没问题。负数不在本实验范围内因为短除法基于取模运算C语言对负数的取模结果依赖编译器实现实验结果不具备可移植性实验指导书一般也标注「非负整数」。6.2 用位运算重写十进制转二进制作为附加题思路转二进制可以完全甩开栈和除法用位运算逐位判断void ConvertToBinaryBitwise(unsigned int n) { int started 0, i; for (i 31; i 0; i--) { int bit (n i) 1; if (bit) started 1; if (started) putchar(bit ? 1 : 0); } if (!started) putchar(0); // n 0时补一个0 putchar(\n); }从最高位往低位扫(n i) 1提取第i个二进制位。started标记从第一个1开始输出跳过前导零。这个写法的时间复杂度是固定的32次循环和n的数值无关而短除法的循环次数是n的二进制位数——当n很大时位运算快了近一倍。它不涉及栈结构不是课内知识但作为学有余力思考位运算和除法之间关系值得你在实验报告的思考题里写一笔。毕业四年后回头看这个几十行的实验可能是你整个数据结构课里唯一亲手把抽象栈结构用到底层运算的题目。从那以后我每次刷LeetCode遇到「逆序输出」「括号匹配」这类题都会强制自己在纸上画出栈的变化过程再动手这个习惯帮我避开了大量边界条件的坑。希望帮到你也祝你的实验报告一次通过。本文还有配套的精品资源点击获取
返回列表