☰
C语言链式栈实现括号匹配:原理详解与完整代码
2026/10/7 16:48:45 网站建设 项目流程

写这篇东西的起因很简单:前阵子帮学生看代码,一个挺聪明的小伙子卡在了一大坨if嵌套里,程序死活编译不过。他一行行盯着看,眼睛都快贴屏幕上了,还是找不到哪个括号配错了对。我当时就跟他说,这种问题别靠肉眼硬刚,把字符挨个压进一个栈里,遇到左括号就入栈,遇到右括号就弹出来比对,跑到最后栈是空的,说明括号都匹配上了——两分钟就能定位问题。他半信半疑地把这个逻辑写出来,果然一下子就找到了那个多余的右括号。也就是从那次之后,我越来越觉得,用“括号匹配”当引子去讲链式栈的用法,简直是C语言教学里最聪明的设计之一:问题本身人人都能看懂,背后牵出的数据结构却一点都不浅。

这篇文章我会从为什么选链式栈而不是顺序栈讲起,把栈的初始化、入栈、出栈、遍历显示这些基本操作一个个掰开揉碎,再给出一套完整的括号匹配算法实现,最后聊聊这段代码放到真实场景里能做点什么。无论是刚学完指针和结构体、想找个像样的综合练习的初学者,还是想复习数据结构基础、顺手补一下C语言内存管理的老手,这篇应该都能对得上你的胃口。

1. 为什么括号匹配这道题,天生就该用链式栈来做

先别急着看代码,把逻辑顺一遍比写代码更重要。括号匹配的规则其实很简单:每一个右括号都必须和自己左边“最近的那个尚未配对的左括号”一一对应。这个“最近”两个字,就是栈之所以能派上用场的根本原因。

1.1 “后进先出”和括号匹配是同一个模型

栈的特性四个字就讲完了:后进先出。往栈里放东西,永远只能放在最顶上,取东西也只能从最顶上拿。你用浏览器的时候,点“后退”回到的是上一次访问的页面,而不是最开始那个页面,这就是栈在起作用。

括号匹配正好也是一模一样的逻辑。遇到(、[、{就压进栈里,等遇到)、]、}时,我们需要找的并不是整个表达式里“第一次出现的那个左括号”,而是“最近一个还没被配对的左括号”——也就是栈顶那个。如果栈顶弹出来的左括号和当前的右括号是同一类型,说明这一对是合法的;如果不是,说明括号类型交叉嵌套了,比如[(这种配对了但类型不对,或者(]直接就是错的。

这个“最近匹配”的直觉和栈的“后进先出”完全是一回事。你用数组、链表、或者甚至手写一个计数器去模拟,最后都会发现还是得退化成“栈”这个模型。所以括号匹配教科书般适合用来讲栈:规则清晰,边界情况多,而且出错的时候你能亲眼看到数据结构到底是怎么运转的。

1.2 链式栈和顺序栈的选型博弈

那具体实现栈的时候,有两个路子:用数组做顺序栈,或者用链表做链式栈。初学者可能觉得既然数组更好写,那直接上顺序栈不就行了?这里面的门道值得掰扯一下。

顺序栈的思路是开一个固定大小的数组,再用一个top变量记录栈顶位置。入栈就像把数组下标往后挪一格再存值,出栈就是top减一。它的优点是访问速度快、不需要频繁申请内存,缺点是容量在编译期就定死了。假设你预估失误,括号嵌套个几百层,数组开小了会溢出;开大了呢,又浪费。

链式栈的思路则完全反过来:每个元素是一个结构体节点,节点里存数据,还有一个指针指向下一个节点。入栈就创建一个新节点接到栈顶,出栈就把栈顶节点摘下来释放掉。它没有容量上限,内存用多少算多少,缺点是每一次入栈都要malloc,性能上会有些损耗,而且用完之后必须记得free,不然就内存泄漏了。

那括号匹配这道题,到底选哪个?说实话,两种都能做。但我个人更倾向于在教学阶段用链式栈,理由有三条:

  1. 括号嵌套深度在理论上不受限。虽然实际代码不会嵌套一万层,但链式栈在逻辑上更接近“无限容量”的抽象,你不用心里一直惦记着那个数组边界。
  2. 链式栈涉及结构体、指针、动态内存分配、链表操作,这一套组合下来,本身就是综合练习的好素材。代码量比顺序栈大一些,但练到的东西也多。
  3. 面试和笔试里,手写链式栈的频率比顺序栈高得多。LeetCode上很多栈相关的题目,底层实现思路都是链式的。

当然,如果你只是想在竞赛里快速交卷,顺序栈的代码量确实更少。但咱们这篇的目的是把东西讲透,所以走链式栈的路子。

2. 链式栈的结构体定义与初始化——先把地基夯实

动手写代码之前,先把数据模型想清楚。链式栈本质上是一个“只在头部增删”的链表,所以结构体定义和链表的节点定义非常像。

2.1 节点结构体的设计思路

链式栈的每个节点需要两块信息:一个是数据域,存的是栈里的实际元素;一个是指针域,指向下一个节点。括号匹配场景里,我们压入栈的是一个字符,所以数据域用char类型就够了。

typedef struct StackNode { char data; // 数据域:存储括号字符 struct StackNode *next; // 指针域:指向栈的下一个节点 } StackNode;

有几点值得说。next指针之所以写成struct StackNode *next而不是StackNode *next,是因为在类型别名typedef还没执行完之前,StackNode这个名字还不存在,编译器不认识它。你说这是玄学也好、细节也罢,总之面试的时候冷不丁被问到一次,答不上来还是很尴尬的。

另外,你可能会想用char *直接存字符串,然后压入一个字符串,但那是另一个话题。括号匹配一次只处理一个字符,用char简单干净,调试的时候一眼就能看出栈顶是什么。

2.2 栈顶指针与链表的头节点等价

栈需要有一个“栈顶指针”来指向最顶上的节点。在这个实现里,栈顶指针其实就是链表的头指针。链表的头指针指向第一个节点,栈顶指针也指向第一个节点——两者是一样的东西。

初始化要做的事,就是把栈置空。很多人第一次写这里容易犯迷糊:到底是把指针置NULL就够了,还是得malloc一个头节点?

StackNode *initStack() { return NULL; // 空栈就是栈顶指针为NULL }

没错,就这样,一行代码。链式栈的空栈状态就是栈顶指针为空。不需要弄什么“哨兵头节点”,那在部分链表场景里确实有用,但在链式栈里纯属画蛇添足。你要是额外malloc了一个头节点,后面每次判断栈是否为空都得把头节点的情况单独拎出来考虑,代码复杂度会直线上升。

这里也顺手提一句为什么top指针要用指针的指针。现实中初始化函数通常写成这样:

void initStack(StackNode **top) { *top = NULL; }

如果只传StackNode *top,那么在函数内部修改top的值,本质上只修改了形参的拷贝,外面的指针变量不会跟着变。想要在函数内部改动外部的指针,就必须多包一层指针。很多初学者在这个地方翻过车,症状就是栈永远“初始化失败”,怎么压都压不进去。

如果不想用指针的指针,也可以让初始化函数返回StackNode *,就像前面那段代码,调用时直接StackNode *top = initStack();。两条路都对,教学时我更偏好后者,写起来顺手,也不容易因为忘了传地址而踩坑。

3. 入栈和出栈的完整实现——链式栈的核心操作拆解

搭好了结构体,下面就到了整篇文章的重头戏:入栈(push)和出栈(pop)。这两个操作是栈的灵魂,括号匹配的全部逻辑都建立在它们之上。

3.1 入栈操作:新节点永远插在最前面

入栈的本质就三步:创建新节点、把数据放进去、把新节点连到栈顶。因为栈只允许在顶部操作,所以新节点始终是链表的新头节点。

StackNode *push(StackNode *top, char value) { // 1. 为新节点分配内存 StackNode *newNode = (StackNode *)malloc(sizeof(StackNode)); if (newNode == NULL) { printf("内存分配失败!\n"); return top; // 内存都没了,栈保持不变 } // 2. 写入数据 newNode->data = value; // 3. 让新节点指向原来的栈顶,然后新节点成为栈顶 newNode->next = top; top = newNode; return top; }

这个操作的时间复杂度是O(1),因为无论栈里已经塞了多少元素,都只需要改两个指针,不需要遍历。这也是栈比很多其他数据结构高效的原因之一:它锁死了操作位置,所以代价恒定。

这里有三个细节必须提。

第一,malloc之后要检查返回值是否为NULL。很多教学代码懒得写这一步,但你实际跑程序的时候,尤其在内存紧缺的环境下,malloc确实可能失败。一旦返回NULL还继续往下赋值,就是解引用空指针,程序直接崩溃。C语言没有异常机制,所有错误都得自己兜着,所以这个检查不能省。

第二,先给newNode->next赋值,再把top更新为newNode,这个顺序不能乱。如果你先执行了top = newNode,那么原来的栈顶节点指针就找不到了,链表从中间断了,后面的节点全部丢失。真出这种bug的时候特别坑:程序能跑,但栈里的元素莫名其妙就丢了,排查起来会绕很大一个弯。

第三,返回值不能丢。我写的是返回新的栈顶指针。如果你在调用时写了push(top, '(');,忘记接收返回值,那top还是原来的旧值,链表照样断掉。所以要么像上面这样用返回值接住,要么就用指针的指针void push(StackNode **top, char value),在函数内部直接修改*top。两条路选一条,千万别混。

3.2 出栈操作:栈顶弹出,指针后移,最后释放内存

出栈同样三步:取栈顶元素的值、把栈顶指针往后挪、释放被摘下的节点。

int pop(StackNode **top, char *result) { if (*top == NULL) { return 0; // 栈为空,出栈失败 } StackNode *temp = *top; // 临时保存栈顶节点 *result = temp->data; // 把栈顶数据取出 *top = temp->next; // 栈顶指针下移 free(temp); // 释放原栈顶节点 return 1; // 出栈成功 }

我这次特意用了指针的指针写法,因为出栈操作不仅要取出数据,还要修改栈顶指针本身。如果你用单指针StackNode *top,在函数内部给top赋新值也影响不到外部变量,出栈出来的结果没法同步。

还有,free这一步千万不要省。链式栈自己管内存,申了多少就要还多少。你在一个循环里疯狂push一万个节点,从不free,程序表面安然无恙,但内存占用一路飙升,最后系统把进程杀掉你都不知道为什么。

另外注意一个细节:我让出栈函数返回int而不是char。原因在于,空栈的时候根本没有元素可取,这时候需要一个“出栈失败”的信号。如果你让函数返回char,那失败时怎么办?返回'\0'?万一栈里存的恰好就是'\0'呢?没法区分。用int做返回值区分成功和失败,再用指针参数把数据带出来,是C语言里非常经典的做法。

如果你实在不习惯指针的指针,也可以换一种写法,用单指针加返回值:

StackNode *pop(StackNode *top, char *result) { if (top == NULL) { return NULL; } StackNode *temp = top; *result = temp->data; top = top->next; free(temp); return top; }

逻辑完全一样,只是改成了返回值更新法。两种风格我都见过,ACM选手偏爱后者,工程码农习惯前者,看你自己的手感。

3.3 链栈的遍历显示:验证栈内状态最直观的方法

调试的时候,光靠眼睛跟踪指针变量很折磨人,最直接的办法就是写一个遍历函数,把栈里的元素从顶到底全部打印出来。因为栈只能从栈顶访问,所以遍历顺序天然就是“后进先出”。

void displayStack(StackNode *top) { if (top == NULL) { printf("栈为空\n"); return; } printf("栈顶 -> "); StackNode *current = top; while (current != NULL) { printf("%c ", current->data); current = current->next; } printf("<- 栈底\n"); }

这里用一个current指针从栈顶一路往后走。千万不要直接动传入的top,否则你的栈顶指针都不知道飞去哪了。每次入栈出栈之后,调用一次displayStack,就能非常直观地看到“栈里现在还剩什么”。我在给学生演示括号匹配算法的时候,每处理一个字符就打印一次栈的状态,那种层层压入又层层弹出的感觉,看代码是一回事,看打印输出又是另一回事,后者理解的效率要高得多。

3.4 摧毁整个栈:避免内存泄漏的收尾操作

前面说了,链式栈的内存全指望自己回收。栈用完了,得写一个“清空”函数把链表上的每一个节点都释放掉。这个函数的思想是:不断弹出栈顶,直到栈空。

void clearStack(StackNode **top) { while (*top != NULL) { StackNode *temp = *top; *top = (*top)->next; free(temp); } printf("栈已清空\n"); }

有人可能觉得,程序一结束操作系统会自动回收内存,写不写这个函数无所谓。这话在只跑一次的判断题程序里勉强说得通,但一旦你的栈是在一个大型程序的某个模块里用到的,模块结束而进程还在,那内存就泄漏了。生产环境下的内存泄漏就是这么一点一点攒出来的。养成良好的收尾习惯,从写练习代码的时候就开始,成本最低。

4. 括号匹配算法:从概念到可运行的完整代码

前面所有铺垫,都是为了这一刻:把栈的操作组装成一个能真正检测括号匹配的程序。

4.1 算法的判断流程与边界条件

括号匹配算法的完整流程大概是这样走:

  1. 初始化一个空栈。
  2. 从字符串的第一个字符扫到最后一个字符。
  3. 如果是左括号((、[、{),入栈。
  4. 如果是右括号()、]、}),先看栈是否为空。如果为空,说明这个右括号前面根本没有可配对的左括号,直接判定不匹配。
  5. 如果栈不为空,弹出栈顶左括号,判断它和当前右括号是否是同一类型。
  6. 整个字符串扫描结束后,如果栈里还有剩余左括号,说明有些括号从未被关闭,还是不匹配;栈为空,才说明所有括号都正确配对了。

边界情况要特别注意:空字符串应该算匹配成功,因为不存在任何未配对的括号;只有右括号没有左括号的情况要在第4步拦下来;只有左括号没有右括号的情况要靠最后栈清空与否来判断。

4.2 完整代码实现与逐步解析

下面这段代码是一个完整的可运行程序,你直接复制进编译器就能跑:

#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct StackNode { char data; struct StackNode *next; } StackNode; // 初始化空栈 StackNode *initStack(void) { return NULL; } // 入栈 StackNode *push(StackNode *top, char value) { StackNode *newNode = (StackNode *)malloc(sizeof(StackNode)); if (newNode == NULL) { printf("内存分配失败!\n"); return top; } newNode->data = value; newNode->next = top; return newNode; } // 出栈 StackNode *pop(StackNode *top, char *result) { if (top == NULL) { return NULL; } StackNode *temp = top; *result = temp->data; top = top->next; free(temp); return top; } // 判断左右括号是否匹配 int isMatch(char left, char right) { if (left == '(' && right == ')') return 1; if (left == '[' && right == ']') return 1; if (left == '{' && right == '}') return 1; return 0; } // 括号匹配核心算法 int checkBrackets(const char *str) { StackNode *top = initStack(); int length = (int)strlen(str); for (int i = 0; i < length; i++) { char ch = str[i]; if (ch == '(' || ch == '[' || ch == '{') { top = push(top, ch); } else if (ch == ')' || ch == ']' || ch == '}') { // 遇到右括号,但栈是空的,说明无左括号可配 if (top == NULL) { printf("第 %d 个字符 '%c' 匹配失败:多余的右括号\n", i + 1, ch); clearStack(&top); return 0; } char topChar; top = pop(top, &topChar); if (!isMatch(topChar, ch)) { printf("第 %d 个字符 '%c' 匹配失败:与栈顶 '%c' 类型不匹配\n", i + 1, ch, topChar); clearStack(&top); return 0; } } // 其他字符,比如字母数字、空格,直接忽略,不参与匹配 } if (top != NULL) { printf("匹配失败:存在未配对的左括号\n"); displayStack(top); clearStack(&top); return 0; } printf("括号全部匹配成功!\n"); clearStack(&top); return 1; } void displayStack(StackNode *top) { if (top == NULL) { printf("栈为空\n"); return; } printf("栈顶 -> "); StackNode *current = top; while (current != NULL) { printf("%c ", current->data); current = current->next; } printf("<- 栈底\n"); } void clearStack(StackNode **top) { while (*top != NULL) { StackNode *temp = *top; *top = (*top)->next; free(temp); } } int main(void) { const char *test1 = "((a+b)*[c-d])"; const char *test2 = "([)]"; const char *test3 = "(a+b)*(c-d"; const char *test4 = "(a+b))"; const char *test5 = ""; printf("测试1:%s\n", test1); checkBrackets(test1); printf("\n测试2:%s\n", test2); checkBrackets(test2); printf("\n测试3:%s\n", test3); checkBrackets(test3); printf("\n测试4:%s\n", test4); checkBrackets(test4); printf("\n测试5:空字符串\n"); checkBrackets(test5); return 0; }

特别提醒一下:我在clearStack用的是StackNode **top,而上面代码里调用时传&top是匹配的;但push返回新栈顶,pop接收旧栈顶并返回新栈顶,两种风格混在一起用,初学者可能看着看着就迷糊了。这确实是一个教学上的取舍:我想让你见识两种常见写法,但代价就是代码风格不够统一。你实际写工程代码时,建议统一成一种风格:要么全部用返回值传新栈顶,要么全部用二级指针,混着用容易出隐蔽bug。

整个程序的运行结果应该是:

  • ((a+b)*[c-d]):匹配成功。
  • ([)]:第3个字符)和栈顶[类型不匹配,失败。
  • (a+b)*(c-d:最后栈里剩了(,失败。
  • (a+b)):第6个字符)是多余的,失败。
  • "":匹配成功。

这五个测试用例分别对应了五种不同的情况:正常嵌套、交叉不匹配、缺右括号、缺左括号、空输入。我自己调试的时候,也是拿这几组数据反复跑,直到全部通过才敢说程序逻辑没问题。

4.3 为什么有效括号的判断其实是在做“消除”而不是“计数”

这里再把思维拔高一点。很多人第一反应是用三个计数器分别记录三种括号的数量,每当看到左括号就加一,看到右括号就减一,最后看三个计数器是不是都归零。这个思路对()这种单一类型的括号管用,但对([)]这种交叉嵌套就彻底失效了。因为(跟]虽然数量上配平了,但它们在结构上压根不是一对。

栈的思路本质上是一个“即时消除”的过程:每看到一个右括号,就把它和最近的左括号配对,配对成功就把这对消除掉,栈里只保留“还没配对的左括号”。这个“消除”操作完成之后,剩下的问题只剩“栈是否为空”。它比计数更贴近括号语法的真实语义——括号不仅要数量对,位置和顺序也必须对。

这也是为什么像IDE、编译器的语法检查器,全都用栈来校验括号配对的根本原因。它不是“一种办法”而已,它是这个问题在计算模型上的最优解之一。

5. 链式栈的常见坑点与调试经验——这些教训是用一次次的段错误换来的

代码写出来了,能跑通不算完。要真正理解链式栈,必须把那些最容易出事的坑提前摸一遍。下面几个问题,几乎每个学到这里的人都会踩上至少一个。

5.1 空栈出栈:最经典的段错误来源

前面写的pop里先判断了top == NULL,但很多人一开始是不写的。直接上来就StackNode *temp = top; *result = temp->data;。如果此时栈是空的,top是NULL,对NULL取->data,在你面前的就是一个字面意义的“段错误”,程序当场崩溃。

这种错误最让人头疼的地方在于,它不一定是稳定复现的。有时候字符串里最后一个字符恰好是右括号,栈刚好被弹空了,下一个字符又来一个右括号,崩溃就发生了。你调试半天,可能试了好几个用例都没问题,偏偏某一条输入就会炸。排查的思路是:每次出栈前先问“栈是否为空”,这永远不该被省略。

括号匹配算法里,右括号来临时栈为空这个分支尤其重要,必须在入逻辑之前就单独处理。很多 LeetCode 上的题解为什么看起来很短?不是省略了这个判断,而是判断逻辑被写进了循环条件里,比如先判断top == NULL就直接返回。别学着学着把这一步给省了。

5.2 忘记接住返回值或忘记free

我之前带的学生里,十个人至少有四个人犯过这个错:`

push(top, '('); // 没有接收返回值

top的值压根没变,入栈白入了。链表类的操作,基本都要么传二级指针,要么接住返回值,二者必有其一。千万别觉得push内部改了top,外部的top就能跟着变——C语言没有引用传参,只传值。

另一类错误是free放错了位置。入栈函数里malloc的新节点,生命周期要延续到出栈时才能free。有些初学者在入栈函数结尾就把节点释放了,结果发现压进去的字符全是乱码。那是因为节点内存虽然已经还给系统了,但指针还指着那块已经失效的区域,访问它属于未定义行为,什么奇怪的结果都可能冒出来。这类问题用调试器查很熬人,最好从开头就养成“谁分配谁释放、什么时候分配什么时候释放”的习惯。

5.3 用char *直接扫描时,别忘掉字符串的长度和边界

checkBrackets函数用了strlen(str)获取长度,再通过下标访问每一个字符。这里有个隐藏前提:传入的字符串必须是以'\0'结尾的合法C字符串。如果你手动构造的字符数组忘了末尾的'\0',strlen会一路读到野内存去,返回一个巨大的数字,循环条件瞬间失控,程序直接乱成一锅粥。

如果不想依赖strlen,也可以改成指针扫描,直接while (*str != '\0')。两种写法本质一样,但我更推荐下标法,调试时能看到i的值,出错的时候好定位到底是第几个字符出的问题。

5.4 嵌套深度的真实测试与栈溢出风险

链式栈理论上没有容量上限,但别高兴太早。每入栈一个节点,就调用一次malloc。如果你拿一个嵌套了上百万层括号的字符串去测,每层都要malloc,内存消耗巨大不说,递归深入太多还可能把栈空间耗尽。这属于极端场景,真实的代码不会嵌套那么深,但那种“我们不怕大数据”的幻觉不能有。

实测下来,嵌套几千层括号在普通PC上没有任何压力,代码跑得非常顺滑。这个量级已经远超绝大多数真实代码的嵌套深度了。真要说链式栈比顺序栈有优势的场景,恰恰就是这种“深度不可预知”但实际又不会太夸张的情况——数组开大了浪费,开小了又不够,链表这边完全不用纠结。

5.5 用调试输出验证每一步状态

实战经验告诉我,写完链式栈之后先别急着跑括号匹配,而是先单独测试push、pop、displayStack三个基本操作。比如:

StackNode *top = initStack(); top = push(top, 'a'); top = push(top, 'b'); top = push(top, 'c'); displayStack(top); // 预期输出:栈顶 -> c b a <- 栈底 char ch; top = pop(top, &ch); printf("弹出:%c\n", ch); // 预期输出:弹出:c displayStack(top); // 预期输出:栈顶 -> b a <- 栈底

先确保每个零件没有问题,再组装成完整系统。这比直接写一个500行的大程序然后从头调错要清爽一百倍。C语言不像那些带垃圾回收的高级语言,每一步内存操作都得自己心里有数。你能看到打印里的c b a,就说明指针关系是对的,后面出了任何问题都不用再怀疑链表的连接了。

6. 括号匹配链式栈之外:这个模型还能走向哪里

写完了括号匹配,千万别以为链式栈的学习就到此为止了。数据结构这种东西,最大的价值不是背下来某种操作步骤,而是明白“什么样的问题底子是什么模型”。括号匹配只是栈模型最经典但也最浅的一个应用。

6.1 表达式求值:从语法到运算的直接延伸

你自己写一个计算器,输入一串中缀表达式1 + 2 * 3,计算机怎么知道要先算乘再算加?普通的从左往右扫描根本做不到。经典的做法是把中缀表达式转成后缀表达式,再用栈来求值。遇到数字入栈,遇到运算符就弹出两个操作数做运算,再把结果压回去。整个过程你会惊讶地发现,跟括号匹配里“遇到右括号弹出左括号”的模式几乎一模一样。可以说,如果你今天把链式栈的push和pop练熟了,明天学表达式求值就是水到渠成的事。

6.2 函数调用的底层到底是怎么运作的

每一门高级语言都有函数调用栈。你调用函数A,A里边再调用函数B,B执行完返回,A接着往下跑。这个“嵌套调用再逐层返回”的先后顺序,和栈的后进先出完全吻合。每调用一个函数,系统就往调用栈里压入一个栈帧,存着局部变量、返回地址这些信息;函数一返回,这个栈帧就被弹出。这也是为什么递归不能无限深入——栈的容量是有限制的,递归太深就会栈溢出。理解了链式栈,你再看递归调用栈这几个字,脑子里就不再是一片模糊了。

6.3 浏览器历史记录与撤销操作的同一个模型

你按一下浏览器“后退”按钮,回到的是上一个页面,不是最初打开的页面,这就是一个标准栈行为。编辑器里的Ctrl+Z也是,每撤销一步就弹出最近的那个操作。这些日常场景都是栈模型,只是它们跑在你看不见的地方。学数据结构最有意思的时刻,就是你突然意识到,原来身边这么多看起来八竿子打不着的东西,底层都在干同一件事。

6.4 再往后走:从中缀转后缀到树与图

栈学会了,往后还有队列、二叉树、图。中缀转后缀练的是“运算符优先级”,二叉树遍历里有一种非递归写法就用栈模拟递归过程,图的深度优先搜索同样可以基于栈。你会发现栈这个“简单的容器”,简直是无处不在的骨架。C语言本身没有内建栈容器,但只要你会写链表,你就拥有了一切的起点。

把链式栈的代码吃透,之后每学一个新结构,回头看看这份练习代码,都会多一分亲切感。数据结构的学习曲线前期确实陡峭,但你亲手写过的每一行malloc和free,都会在后面的路上不断给你回报。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询