ARTICLE DETAIL

资讯详情

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

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

C语言数据结构:栈实现数制转换的原理与完整代码详解 简介C语言数据结构中数制转换实例代码是一份面向初学者的PDF文档聚焦于用顺序栈实现十进制到其他进制如八进制的转换帮助理解教材中的伪代码实际如何落地。压缩包共1个文件为47KB的PDF内容精炼便于随时查阅。目前已有894人学习下载。文档以严蔚敏《数据结构》中的数制转换算法为基础完整给出栈结构定义、初始化、压栈、出栈、判空等关键操作并重点剖析conversion()函数的执行流程通过反复“取余入栈、商数更新”收集低位数字再借助栈的后进先出特性逆序输出从而得到正确的目标进制表示。代码注释详尽逻辑清晰将理论讲授与编程实践紧密结合适合C语言初学者、数据结构学习者参考也方便教师作为课堂案例使用。通过研读这份PDF读者可具体掌握栈在“逆序处理”中的应用并能够便捷地修改进制参数以扩展到十六进制等场景。1. C语言数据结构中数制转换一个栈的经典应用到底能走多远在 C 语言和数据结构课程的作业与面试题里“数制转换”几乎是绕不开的一道坎。它的典型形态是给定一个十进制整数输出它对应的二进制、八进制或十六进制表示。很多刚学完栈的人第一反应是“这不就是不断的除法和取余吗用数组也能做”但真写出来就会发现余数的产生顺序和打印顺序是反的而栈的后进先出特性恰好把这件事完美矫正过来。这篇笔记会把“十进制转任意进制”这条线完整走一遍先讲清楚为什么非要用栈再落到可复现的完整 C 代码、参数设计和踩坑记录最后给出扩展到任意进制和长整数场景的写法。新手可以照步骤抄作业熟手可以直接跳到避坑部分看边界。2. 数制转换为什么必须用栈从短除法的输出顺序说起2.1 短除法的本质余数是倒着读的任意十进制整数 N 转换成 base 进制标准做法是短除法用 N 不断除以 base记下余数再用商继续除直到商为 0。比如把 13 转成二进制13 / 2 6 余 1 6 / 2 3 余 0 3 / 2 1 余 1 1 / 2 0 余 1余数产生顺序是 1、0、1、1读出来是 1011但正确的二进制结果恰好相反是 1101。这说明余数序列天然是逆序的。你要是硬用数组存余数最后必须把数组倒过来输出多写一轮循环不说还会引入一个“到底逆序到哪个下标为止”的边界问题。数制转换的教材解法之所以清一色指向栈就是因为栈的 pop 顺序天然把这个问题消解掉了——先产生的余数后出去后产生的余数先出去弹栈本身就是逆序输出。2.2 顺序栈和链栈怎么选这次的答案是顺序栈而且理由很硬既然要用栈就面临选型问题顺序栈数组实现还是链栈链表实现很多教材两个都讲学生反而卡在这里。数制转换这个场景的特殊性在于栈的最大深度是确定的一个 int 范围内的数转成二进制最多也就 32 层转成八进制最多 11 层左右。栈深有上限且不大顺序栈的固定数组完全够用而且顺序栈的缓存命中率高、实现代码短、压栈弹栈就是两次数组赋值性能开销可以忽略。链栈的优势在“栈深度不可预知、频繁插入删除”的场景比如表达式求值里嵌套层数可能很深或者运行时动态增长的括号匹配。数制转换不值得为它去写 malloc 和 free一个定长数组加一个 top 指针就是全部家当。我一般会这样定义顺序栈#include stdio.h #include stdlib.h #define MAX_STACK_SIZE 64 // 覆盖 int 的 32 位二进制 符号位留余量 typedef struct { int data[MAX_STACK_SIZE]; int top; // -1 表示空栈 } SeqStack;这里的 MAX_STACK_SIZE 取 64 而不是 32不是拍脑袋。int 在 32 位平台上占 32 位加上负号占一位理论上输出字符个数不超过 33但你做任意进制扩展时base 越小字符越长64 这个值既覆盖了 int也顺带覆盖了 unsigned long long 的核心需求一行定义免去后顾之忧。top 初始化为 -1 是顺序栈的常见写法好处是 top 直接就是栈顶元素的下标判断空栈只需要top -1判断满栈只需要top MAX_STACK_SIZE - 1比“top 初始化为 0、栈顶在 top-1”那套更直观。2.3 栈的三个基本操作压栈、弹栈、判空少一个都转不起来顺序栈的操作函数写起来很短但每个都有值得说明的参数边界。先看压栈和弹栈int push(SeqStack *s, int value) { if (s-top MAX_STACK_SIZE - 1) { printf(栈满无法压入 %d\n, value); return 0; } s-data[(s-top)] value; return 1; } int pop(SeqStack *s, int *value) { if (s-top -1) { printf(栈空无法弹出\n); return 0; } *value s-data[(s-top)--]; return 1; } int isEmpty(SeqStack *s) { return s-top -1; }三个函数我都加了返回值而不是 void这是实用代码和书本代码的差别。书本喜欢写void push(...)然后默认不会满现实中栈满和栈空都是要处理的异常路径。push 里s-data[(s-top)]是先移动 top 再写入pop 里*value s-data[(s-top)--]是先取值再下移 top两个方向相反写混了就会出现“压栈覆盖旧值”或者“弹栈拿到脏数据”的诡异现象。之后所有用栈的地方参数都传指针而不是传值因为栈结构体里的 data 数组比较大传值复制浪费栈空间而且修改不会反映到外面传指针配合返回值既能改栈内容又能报错这才是能进工程代码的写法。3. 用 C 语言写出可复现的数制转换实例完整代码与逐段拆解3.1 主转换函数除 base 取余余数压栈结束弹栈核心逻辑其实就是“循环除 循环弹”但实现里有几个参数和边界需要说清楚。先看主体代码void convertBase(int number, int base) { SeqStack stack; stack.top -1; if (base 2 || base 36) { printf(不支持的进制 %dbase 范围应为 2~36\n, base); return; } if (number 0) { printf(0\n); return; } int absNum number; if (number 0) { absNum -number; // 符号放到最后处理余数计算用绝对值 printf(-); } while (absNum 0) { int remainder absNum % base; if (!push(stack, remainder)) { return; // 栈满说明 MAX_STACK_SIZE 不够直接退出 } absNum / base; } while (!isEmpty(stack)) { int digit; pop(stack, digit); if (digit 10) { printf(%d, digit); } else { printf(%c, A (digit - 10)); // 10-A, 11-B, ..., 35-Z } } printf(\n); }这里的循环流程就是标准数制转换的 C 语言表达while (absNum 0)控制短除法一直做到商为 0余数absNum % base压栈absNum / base更新商。循环结束后栈里自底向上存着余数序列弹栈时自顶向下输出正好得到正确结果。参数base限定在 2 到 36 之间因为 36 进制正好用完 0-9 加 A-Z 这套字母表超过 36 要么没有足够字符表示余数要么会造成字符集混乱。3.2 数字到字符的映射小于 10 直接输出大于等于 10 用 ASCII 偏移上面的代码里有一段关键映射A (digit - 10)。这是教科书里 ASCII 运算的标准写法digit 以整数形式从栈里弹出来但如果 digit 是 10直接printf(%d, digit)会输出两个字符“10”而十六进制里 10 应该显示成单字符 A。ASCII 码表里 A 到 Z 是连续的字符A加上偏移量(digit - 10)就能把整数余数精确映射到对应字母。比如 digit 10 时A 0输出 Adigit 15 时A 5输出 F。有同学会问为什么不用一个char digits[] 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ这样的映射表直接查下标两种写法都行但我个人倾向 ASCII 偏移原因有二一是少写一个全局字符数组函数局部性更好二是当 base 扩展到更大范围时ASCII 偏移仍然满足“前 10 个数字 连续字母”这个条件不需要改表。不过要注意如果 base 超过 36这套映射就不够用了到那时才需要引入映射表或者自定义符号集。3.3 一次性跑通的完整测试代码编译、运行、检查输出把上面的函数串到一个完整程序里需要包含头文件、栈定义、栈操作和 main 函数。这里贴一个可以直接编译的最小骨架#include stdio.h #define MAX_STACK_SIZE 64 typedef struct { int data[MAX_STACK_SIZE]; int top; } SeqStack; int push(SeqStack *s, int value) { if (s-top MAX_STACK_SIZE - 1) { printf(栈满无法压入 %d\n, value); return 0; } s-data[(s-top)] value; return 1; } int pop(SeqStack *s, int *value) { if (s-top -1) { printf(栈空无法弹出\n); return 0; } *value s-data[(s-top)--]; return 1; } int isEmpty(SeqStack *s) { return s-top -1; } void convertBase(int number, int base) { // 省略见上个代码块 } int main() { convertBase(13, 2); // 期望输出 1101 convertBase(255, 16); // 期望输出 FF convertBase(8, 8); // 期望输出 10 convertBase(0, 2); // 期望输出 0 convertBase(-13, 2); // 期望输出 -1101 return 0; }编译命令是常规的 GCC 三件套gcc -Wall -o convert convert.c-Wall打开警告代码里有没有“定义了未使用”或者“隐式声明”一眼就能看到然后./convert直接跑。上面五组测试覆盖了五种典型情况普通正数、大数转十六进制、转回自身进制、零值、负数。每次改动代码后跑一遍这五组基本能确定核心逻辑没被改坏。我通常在写完这个版本后还会把MAX_STACK_SIZE临时改成 5 跑一次观察栈满时程序是否优雅退出——这比事后 debug 省心得多。4. 数制转换实例里的高频翻车点5 个真实踩坑记录与排查思路4.1 栈没用到头余数顺序反了直接输出出来是逆序现象是转换 13 为二进制输出结果是 1011 而不是 1101。很多第一次写的人把短除法的余数直接printf出来得到的前几个余数其实是结果的低位打印顺序天然相反。原因就是没有借助栈的 LIFO 特性把余数逆序。解决方法是把余数压栈全部压完后再统一弹栈输出你也可以用一个数组存余数然后从后往前遍历但那就是用数组模拟了栈的行为代码反而更长。排查时看输出是不是“从低位到高位”如果是直接改成栈输出即可。4.2 除零与非法 base 参数导致程序直接崩溃现象是convertBase(10, 0)或convertBase(10, 1)时程序在% base处抛出浮点异常。原因是base为 0 时取模是除零错误base为 1 时短除法永远除不尽商永远是原数死循环直到栈满。解决方法是进入逻辑前先校验base范围不满足2 base 36就直接 return 并给出提示。这一步看起来多余但当你的函数被其他模块调用时调用方可能传进来一个未初始化变量没有校验就等于把崩溃风险埋在了最底层。4.3 负数转换结果出现一长串 1符号位处理方式不对现象是convertBase(-13, 2)输出1111111111110011这种类似补码的串或者输出一长串 1 再加一个 -13。原因是代码里直接对负数做number % baseC 语言里负数取模的结果是负数或 0除法的商也会向 0 取整整个循环行为变得难以预测。解决方法是开头把负号单独处理先取绝对值参与短除法输出负号再做进制转换。实际业务里如果真需要输出补码形式的二进制位串那应该用无符号类型并且固定按位宽输出走另一套逻辑不能和普通数制转换混在一起。4.4 栈容量不够导致转换中断溢出后结果缺失后半段现象是把一个很大的整数转成二进制时输出到一半程序提示“栈满”。原因是MAX_STACK_SIZE设得太小比如设成 16而 int 转二进制需要 32 位。解决方法是把栈容量定义成 64覆盖常见类型同时 push 函数保留返回值一旦满栈就回滚或报错避免继续压栈覆盖数组外内存。这个坑最容易出现在从教材代码复制改写的过程中教材里栈容量往往只是“手写一个能演示的 10”作业题数据量小看不出问题换成长整数立刻翻车。4.5 弹栈后的脏数据交换压栈与弹栈写法造成不可复现的乱码现象是连续调用convertBase多次第一次结果正确第二次结果尾部出现随机数字。原因是弹栈用s-data[(s-top)--]与压栈用s-data[(s-top)]写反导致读写位置错位旧数据未被覆盖。解决方法是把压栈和弹栈作为唯一入口其他代码不要直接访问stack.data和stack.top每次转换前显式stack.top -1杜绝上次残留。这类问题用调试器断点很难一眼看出来因为数据残留是随机的个人经验是写一个printStack辅助函数空栈时打印top-1方便确认每次调用前栈状态是不是真的干净。5. 从实例到通用版本把数制转换扩展成你想用的工具函数5.1 支持任意进制的通用接口设计与安全边界上面 convertBase 把结果直接打印到控制台作为教学演示没问题但如果你想把它用在项目里返回值比打印更实用。常见做法是把结果写进一个字符缓冲区int toBaseString(int number, int base, char *out, int outSize) { if (base 2 || base 36 || out NULL || outSize 2) { return -1; // 非法参数 } // 内部复用顺序栈逻辑把 printf 替换成 snprintf 写入 out }参数设计里最关键的是outSize调用方必须传入缓冲区长度函数内部用snprintf或手动下标控制写入位置任何情况下都不越过outSize - 1。返回值约定成“成功返回写入字符数失败返回 -1”这样调用方可以用if (toBaseString(...) 0)统一处理错误路径。这个设计把一次性打印函数变成了可复用组件配合 char 数组存储结果后续不管是要写到日志、拼进 JSON 还是直接通过网络发出去都只用处理字符串。编码时建议在里面顺便把负号处理、零值处理、栈满回滚都放在同一层避免调用方再重复处理这些边界。5.2 大整数场景改用 unsigned long long 和高精度数组int 只有 32 位最大值约 21 亿转成二进制最多 32 个字符。但实际业务里你可能要转换时间戳、文件大小、ID 这类大整数。常见做法是两步走第一步把入参类型改成unsigned long long栈元素类型同步改成unsigned long long这样最大支持到 1844 亿亿转成二进制位数也不超过 64栈容量 64 刚好够。第二步如果还要支持更大数字那就需要用数组模拟高精度除法把大数按十进制位拆进 int 数组从高位到低位逐位做“除以 base 并进位”这个过程本身就是短除法的逐位推广。注意unsigned long long没有负数概念调用前先把符号单独拆出来否则负数转无符号会直接变成超大正数结果完全不可读。5.3 验证你的转换函数手工用例、边界用例与对拍测试代码写完了如何确认它真的对我常用的验证方法是三个层次叠加。第一层是手工用例上面 main 里那五组就已经覆盖了基本逻辑。第二层是边界用例专门测 0、1、负数、INT_MAX、INT_MIN、base 为 2、16、36、以及非法 base。第三层是对拍测试写一个不依赖栈的“朴素实现”比如用数组存余数再逆序输出跑同一组随机数比较两边的字符串是否一致。对拍测试在数据结构和算法学习里是查错利器数制转换这种逻辑简单的场景对拍一轮能找出 90% 以上的低级错误。另外我还习惯在代码里临时加一组printf(压栈: %d\n, remainder)的调试输出观察余数序列是否符合“先低位后高位”确认无误后删除——这种自己验证过的代码比抄来的代码可靠得多。说句体己话数制转换这个实例看起来小但它是“栈特性与算法天然匹配”的经典样本。我早年给一个嵌入式设备写配置解析器当时懒得用栈硬用数组加一个翻转循环结果莫名多了一堆麻烦后来老老实实推倒重来用栈实现代码量反而少了三分之一。这算是我在数制转换这件事上最深刻的教训。希望帮到你如果你也有过“明明该用栈却硬用数组”的经历建议把这篇里的通用接口抄回去试试跑通之后你对栈的理解会上一个台阶。本文还有配套的精品资源点击获取
返回列表