资讯动态

树的三种存储表示方法详解:双亲、孩子、孩子兄弟表示法

发布时间:2026/9/19 17:15:14 来源:尧图企业网站定制
先说个现象。我见过不少同学学树的时候能把定义背得滚瓜烂熟——节点、根、叶子、子树、深度、层次说起来头头是道真让写代码就卡住了。原因倒也不复杂树在逻辑上非常直观可一旦要落到内存里立刻就会碰到一个核心矛盾——树的分支是不固定的一个节点可能有两个孩子也可能有二十个孩子你怎么知道该给它分配多少空间这篇就围绕“树的三种表示方法”展开把双亲表示法、孩子表示法、孩子兄弟表示法逐一讲透每一种是怎样设计的、代码怎么实现、适合放在什么场景全部给出完整可编译的C语言实现。适合刚学数据结构的学生也适合工作几年后想系统地补基础、或者要处理文件目录遍历、设备树解析、表达式树这类真实需求的开发同学参考。我第一次把这三种表示方法真正想明白是在做一个小型文件系统遍历工具的时候。一开始对着“树”这种结构发愁后来把三种存储方式各写了一遍才发现它们本质上是同一件事的三种取舍你要快速找到父节点还是快速找到孩子节点还是要一种能统一适配各种树形算法的通用表示。没有哪种是绝对好的只有合不合适。这篇我把源码、测试过程、踩坑记录全部放出来供你对照着动手跑一遍。1. 树到底该怎么存三种表示方法的设计思路1.1 先从一棵最简单的树说起在讨论“怎么存”之前得先把“存什么”这件事说清楚。一棵树由若干节点构成节点之间有明确的父子关系每个节点最多只能有一个父节点但可以有任意多个子节点。叶子节点是没有子节点的节点根节点是没有父节点的节点同一个父节点下的几个节点互为兄弟。举个例子下面这棵树非常典型A / \ B C / / \ D E FA是根B和C是A的孩子D是B的孩子E和F是C的孩子。B和C互为兄弟E和F也互为兄弟。逻辑结构很简单但存储的问题立刻出现了。如果每个节点都固定留两个指针那这棵树跟二叉树没区别可现实里一个节点可能有三个、五个甚至二十个孩子。“到底预留多少空间才够”就成了一道没有标准答案的题。预留少了节点多了放不下预留多了又浪费内存。这种“分支数不确定”带来的存储尴尬就是我们需要多种表示方法的根本原因。1.2 三种方法的共同出发点既然树的形状不固定那核心思路就只剩下一条把不固定的关系用一种固定的、计算机好处理的方式存下来。围绕这个目标常见的解法和设计思路主要有三种也是数据结构教材里最常见的三种第一种是双亲表示法核心思路是“找爸爸”。因为树里每个节点只有一个父亲这是一个非常稳定的信息所以只需要给每个节点额外记录一个“父亲是谁”的下标就可以了。第二种是孩子表示法核心思路是“记儿子”。既然孩子数量不确定那就用动态的链表把每个节点的所有孩子串起来谁有几个孩子就存几个不浪费空间。第三种是孩子兄弟表示法核心思路是“把树变成二叉树”。每个节点只记录两个关键信息第一个孩子是谁、下一个兄弟是谁这样不管原来有多少个分支最终都能统一成一种二叉结构。这三种方法前两种保留了“树的原貌”第三种干脆把多叉树降维成了二叉树。理解这一层你后面学B树、字典树、表达式树、哈夫曼树、设备树解析之类的具体应用时就会轻松很多——因为万变不离其宗它们都要回答同一个问题这棵树在内存里怎么放。2. 双亲表示法找爸爸最快的存储方案2.1 核心思想与结构设计双亲表示法的设计思路非常朴素每个节点只有唯一的父节点那我用一个数组把所有节点按顺序存下来每个节点里不只有自己的数据还记录一个“父节点在数组中的下标”。根节点没有父节点所以它的父下标用-1表示。这种存储方式完全不需要指针也不需要链表只需要一组连续的内存空间。它牺牲了“直接找孩子”的能力换来了“直接找爸爸”的高效率。只要给我一个节点的位置我马上就能通过它记录的父节点下标找到它的父亲时间复杂度是O(1)这是三种方法里找父节点最快的。结构体定义可以这样设计#define MAX_TREE_SIZE 100 // 树节点数据 父节点下标 typedef struct { char data; // 节点数据这里用 char 做演示 int parent; // 父节点在数组中的下标根节点为 -1 } PTNode; // 整棵树本质上就是一个节点数组 当前的节点个数 typedef struct { PTNode nodes[MAX_TREE_SIZE]; int n; // 当前节点数 } PTree;数据域你完全可以根据业务需求改成int、结构体、字符串等其他类型。这里用char是为了让代码运行结果看起来更直观。2.2 完整代码实现光说理论没意思我直接放一段可以编译运行的完整代码。这段代码实现的功能是初始化一棵树、向树里添加节点、指定父节点下标来建立父子关系、打印每个节点的父子关系、查找某个节点的所有孩子。#include stdio.h #define MAX_TREE_SIZE 100 #define INVALID -1 typedef struct { char data; int parent; } PTNode; typedef struct { PTNode nodes[MAX_TREE_SIZE]; int n; } PTree; // 初始化空树 void InitTree(PTree* T) { T-n 0; } // 向树中添加一个节点返回新节点在数组中的下标 int AddNode(PTree* T, char data, int parentIndex) { if (T-n MAX_TREE_SIZE) { printf(树已满无法添加节点 %c\n, data); return INVALID; } int index T-n; T-nodes[index].data data; T-nodes[index].parent parentIndex; T-n; return index; } // 打印整棵树的父子关系 void PrintTree(PTree* T) { printf(下标\t数据\t父节点下标\n); for (int i 0; i T-n; i) { printf(%d\t%c\t%d\n, i, T-nodes[i].data, T-nodes[i].parent); } } // 查找某个节点的所有孩子打印出来 void PrintChildren(PTree* T, int index) { if (index 0 || index T-n) { printf(节点下标非法\n); return; } printf(节点 %c 的孩子, T-nodes[index].data); int hasChild 0; for (int i 0; i T-n; i) { if (T-nodes[i].parent index) { printf(%c , T-nodes[i].data); hasChild 1; } } if (!hasChild) { printf(无); } printf(\n); } int main() { PTree T; InitTree(T); int root AddNode(T, A, INVALID); int b AddNode(T, B, root); int c AddNode(T, C, root); int d AddNode(T, D, b); int e AddNode(T, E, c); int f AddNode(T, F, c); PrintTree(T); PrintChildren(T, root); PrintChildren(T, c); return 0; }运行结果很直观数组下标0到5依次对应A、B、C、D、E、F父节点下标则清楚标记了每个节点的来源。A的父下标是-1B和C的父下标都是0D的父下标是1E和F的父下标都是2。2.3 这样存有什么好处又有什么坑双亲表示法最大的优势是找父亲极快且结构极其简单不需要动态内存管理不会出现内存泄漏、悬空指针这类问题。在一些频繁向上追溯的场景中这种表示法非常合适。比如并查集本质上就是一个简化版的双亲表示法——每个集合的代表元素当作树根其他元素记录“上级是谁”。但它也有明显的短板找孩子必须遍历整个数组时间复杂度是O(n)。如果你频繁需要“一个节点下面有哪些子节点”这种结构就不太舒服。另外它按物理顺序把节点塞进数组这棵树本身在逻辑上是乱的你无法从数组顺序直接还原出树的层次结构。也就是说双亲表示法很适合“维护隶属关系”但不太适合“按层次遍历”。实操中有一个容易踩的坑父节点下标必须小心处理千万别用0当“无父节点”的哨兵值因为数组下标0是合法位置。根节点的parent一定要设成-1否则后面遍历逻辑里可能会出现一个莫名其妙的“根节点的父亲是下标0的节点”排查起来很费劲。3. 孩子表示法把“孩子”串起来3.1 核心思想与结构设计既然双亲表示法的痛点是找孩子太慢那就自然有人想到反过来干脆把每个节点的“孩子列表”直接存下来。这就是孩子表示法的思路。实现方式通常是把数组和链表结合整棵树仍然有一个节点数组但每个数组元素不只是存数据了它还带一个指针指向该节点的第一个孩子每个孩子节点又通过自己的另一个指针指向下一个兄弟。这样绕着某个节点的孩子链表走一圈就能拿到它的所有孩子。这种方式把“动态的孩子数量”交给了链表处理内存使用非常灵活。它不需要提前预设每个节点有几个孩子也不会浪费大量空闲指针空间。结构体如下#define MAX_TREE_SIZE 100 // 孩子链表节点存孩子下标 指向下一个孩子 typedef struct CTNode { int child; // 孩子在数组中的下标 struct CTNode* next; // 指向下一个孩子节点 } CTNode; // 树节点存数据 指向孩子链表的头指针 typedef struct { char data; CTNode* firstChild; // 第一个孩子 } CTBox; // 整棵树 typedef struct { CTBox nodes[MAX_TREE_SIZE]; int n; } CTree;需要提一句很多教材还会在这个结构上再加一个parent域构成“带双亲的孩子链表”。那样既保留找孩子快的能力又弥补找父亲慢的短板代价就是每个节点多存一个int属于典型的时间换空间的取舍。这篇文章先按不带parent的经典版讲哪天你真需要双向找加一个字段就行逻辑完全一样。3.2 完整代码实现下面这段代码构建一棵和孩子表示法匹配的树并支持打印某个节点的孩子、按先根顺序遍历整棵树。注意释放内存的细节我在代码里做了相应处理。#include stdio.h #include stdlib.h #define MAX_TREE_SIZE 100 typedef struct CTNode { int child; struct CTNode* next; } CTNode; typedef struct { char data; CTNode* firstChild; } CTBox; typedef struct { CTBox nodes[MAX_TREE_SIZE]; int n; } CTree; // 初始化 void InitTree(CTree* T) { T-n 0; for (int i 0; i MAX_TREE_SIZE; i) { T-nodes[i].firstChild NULL; } } // 添加节点 int AddNode(CTree* T, char data) { if (T-n MAX_TREE_SIZE) { printf(树已满\n); return -1; } int index T-n; T-nodes[index].data data; T-nodes[index].firstChild NULL; T-n; return index; } // 把 childIndex 挂到 parentIndex 的孩子链表中 void AddChild(CTree* T, int parentIndex, int childIndex) { if (parentIndex 0 || parentIndex T-n || childIndex 0 || childIndex T-n) { printf(下标非法\n); return; } CTNode* node (CTNode*)malloc(sizeof(CTNode)); node-child childIndex; node-next NULL; if (T-nodes[parentIndex].firstChild NULL) { T-nodes[parentIndex].firstChild node; } else { // 尾插保持孩子顺序 CTNode* p T-nodes[parentIndex].firstChild; while (p-next ! NULL) { p p-next; } p-next node; } } // 打印某个节点的孩子 void PrintChildren(CTree* T, int index) { if (index 0 || index T-n) { printf(下标非法\n); return; } printf(节点 %c 的孩子, T-nodes[index].data); CTNode* p T-nodes[index].firstChild; if (p NULL) { printf(无\n); return; } while (p ! NULL) { printf(%c , T-nodes[p-child].data); p p-next; } printf(\n); } // 先根遍历整棵树输出根节点再依次遍历每棵子树 void PreOrder(CTree* T, int rootIndex) { if (rootIndex 0 || rootIndex T-n) { return; } printf(%c , T-nodes[rootIndex].data); CTNode* p T-nodes[rootIndex].firstChild; while (p ! NULL) { PreOrder(T, p-child); p p-next; } } // 释放整棵树的所有孩子链表内存 void FreeTree(CTree* T) { for (int i 0; i T-n; i) { CTNode* p T-nodes[i].firstChild; while (p ! NULL) { CTNode* tmp p; p p-next; free(tmp); } T-nodes[i].firstChild NULL; } } int main() { CTree T; InitTree(T); int root AddNode(T, A); int b AddNode(T, B); int c AddNode(T, C); int d AddNode(T, D); int e AddNode(T, E); int f AddNode(T, F); AddChild(T, root, b); AddChild(T, root, c); AddChild(T, b, d); AddChild(T, c, e); AddChild(T, c, f); PrintChildren(T, root); PrintChildren(T, c); printf(先根遍历); PreOrder(T, root); printf(\n); FreeTree(T); return 0; }运行这段代码你会看到A的孩子是B和CC的孩子是E和F先根遍历输出的顺序是A B D C E F和树本身的层次结构完全对应。3.3 优缺点与典型场景孩子表示法最大的优点是找一个节点下的所有子树特别快直接扫链表就行。因为每个节点的孩子链表独立存储树的结构也一目了然。这种特性让它非常适合做“自上而下”的递归遍历操作比如文件目录树的展开、组织架构的向下查询、菜单树渲染等。它的缺点也很明显找父节点麻烦。每个节点没有记录父亲想找父亲就只能把所有节点扫一遍时间复杂度高。另一个问题是引入了动态内存分配代码写起来比纯数组复杂一个不留神就会造成内存泄漏。实际操作中我建议你在确保逻辑正确后一定要把这棵树的释放函数写好。尤其是用孩子链表时很多人只释放了节点数组忘了释放链表上的每个孩子节点块跑一次测试看不出来长时间运行内存就会越涨越高。上面代码里的FreeTree就是专门做这件事的。4. 孩子兄弟表示法把树变成一棵二叉树4.1 核心思想与结构设计前两种方法都是在“树”的框架内想办法孩子兄弟表示法换了一个更取巧的角度把多叉树强行转成二叉树。它的做法是每个节点只保留两个指针firstChild指向自己的第一个孩子nextSibling指向自己的下一个兄弟。这样一来一棵五叉树、十叉树都会被组织成一种标准的二叉链表结构。比如前面那棵树的节点B它的第一个孩子是D下一个兄弟是C于是B的左指针指向D右指针指向C。这种转换带来的好处非常明显所有二叉树的成熟算法——先序、中序、后序遍历线索化按层遍历甚至AVL树、红黑树里的一些调整思路——都可以直接拿过来用。对于复杂树形结构的算法研究和工程实现来说这是一种非常优雅的降维手段。结构体定义如下typedef struct CSNode { char data; struct CSNode* firstChild; // 第一个孩子 struct CSNode* nextSibling; // 下一个兄弟 } CSNode;这里我用的是动态指针而非数组下标。因为它本身就是一个二叉链表用指针来表达两个方向的关系最自然。如果后面需要加数据域、加父指针思路也完全一样。4.2 完整代码实现下面这段代码构建同样的A、B、C、D、E、F树再把每个节点的孩子和兄弟关系建立起来最后用类似二叉树先序遍历的方式输出全部节点。为什么说是“类似”因为这里的结构虽然在物理上是二叉链表逻辑含义仍然是多叉树遍历时你要按“先根、再所有孩子”的顺序来写。#include stdio.h #include stdlib.h typedef struct CSNode { char data; struct CSNode* firstChild; struct CSNode* nextSibling; } CSNode; // 创建单个节点 CSNode* CreateNode(char data) { CSNode* node (CSNode*)malloc(sizeof(CSNode)); node-data data; node-firstChild NULL; node-nextSibling NULL; return node; } // 在父节点下追加一个孩子节点 void AddChild(CSNode* parent, CSNode* child) { if (parent-firstChild NULL) { parent-firstChild child; } else { CSNode* p parent-firstChild; while (p-nextSibling ! NULL) { p p-nextSibling; } p-nextSibling child; } } // 先根遍历输出当前节点然后遍历所有孩子 void PreOrder(CSNode* root) { if (root NULL) { return; } printf(%c , root-data); CSNode* p root-firstChild; while (p ! NULL) { PreOrder(p); p p-nextSibling; } } // 后根遍历先遍历所有孩子最后输出当前节点 void PostOrder(CSNode* root) { if (root NULL) { return; } CSNode* p root-firstChild; while (p ! NULL) { PostOrder(p); p p-nextSibling; } printf(%c , root-data); } // 释放整棵树 void FreeTree(CSNode* root) { if (root NULL) { return; } FreeTree(root-firstChild); FreeTree(root-nextSibling); free(root); } int main() { CSNode* A CreateNode(A); CSNode* B CreateNode(B); CSNode* C CreateNode(C); CSNode* D CreateNode(D); CSNode* E CreateNode(E); CSNode* F CreateNode(F); AddChild(A, B); AddChild(A, C); AddChild(B, D); AddChild(C, E); AddChild(C, F); printf(先根遍历); PreOrder(A); printf(\n); printf(后根遍历); PostOrder(A); printf(\n); FreeTree(A); return 0; }这段代码的核心点在于AddChild函数。它先判断父节点有没有孩子如果没有直接把新孩子挂在firstChild上如果有就沿着孩子的nextSibling链走到末尾再挂上去。细心的读者会发现这其实就是“兄弟链表的尾插法”它保证了同层节点的顺序和插入顺序一致。4.3 三种表示法在项目里怎么选每次我在工程里需要选择树形存储方案时都会把三种方法的关键特性拉出来对比一遍。这里直接整理成一张表方便你查阅表示方法找父节点找孩子节点空间特点适合场景代码复杂度双亲表示法O(1)O(n)遍历数组紧凑固定数组并查集、隶属关系查询、设备树静态配置低孩子表示法需要遍历O(k)扫链表每个节点带一个链表动态分配文件目录、菜单渲染、组织架构中等孩子兄弟表示法需另加parent域O(k)扫兄弟链二叉链表每个节点两个指针表达式树、通用树形算法、森林转换中等偏上如果你面对的问题主要是“这个节点属于谁、它的上级链是什么”用双亲表示法最省事如果是“给我遍历出这棵树的所有子节点”孩子表示法更顺手如果你不只处理一棵树还要处理森林、或者要做大量树的算法变形那孩子兄弟表示法几乎是不二之选。很多系统里的目录树、UI组件树、语法分析中的表达式树底层结构都跟孩子兄弟表示法思路一致。5. 实操过程用三种方法实现同一棵树5.1 测试树的搭建与运行写数据结构代码光看理论容易眼高手低。我建议你拿到代码后第一步先别急着看输出而是自己动手构建一棵树然后分别用三种表示法实现一遍。我这次用的测试树就是前面反复出现的那一棵A为根B、C为A的孩子D为B的孩子E、F为C的孩子。三种代码我都编译运行过环境是Linux下的gcc直接用gcc编译即可比如gcc parent_method.c -o parent_method ./parent_method三个程序的核心输出如下双亲表示法打印出一张节点表清楚展示每个节点的父节点下标比如D的父下标是1。孩子表示法打印出A的孩子为B、CC的孩子为E、F同时先根遍历输出A B D C E F。孩子兄弟表示法先根遍历输出A B D C E F后根遍历输出D B E F C A。前后根遍历的结果很有意思。先根遍历结果就是树的自然自上而下顺序后根遍历则是“先子树后根”。如果你把后根遍历结果和双亲表示法中的父子关系对照会更容易理解树结构的递归性质。5.2 三种实现结果的横向对比同样一棵树三种代码的写法完全不同但表达的逻辑关系完全一致。这就是数据结构课程里经常强调的“逻辑结构”与“存储结构”分离树的逻辑形态是A下面有B和C这是确定的至于用数组存父亲下标、用链表存孩子、还是用二叉链表存孩子和兄弟都是存储层面的选择。我实践中发现一个特别值得体会的点孩子兄弟表示法的先根遍历代码最短只有几行递归却能处理任意复杂的树形结构。这正是“降维”带来的收益——把不规律的多叉树变成规律的二叉树之后递归逻辑变得非常统一。很多高性能的树形处理库底层处理逻辑都遵循这个设计哲学。反过来双亲表示法的打印逻辑也很简洁但是它没法做这种递归遍历——你想从根节点出发走到所有叶子就必须在数组里反复扫效率层面明显吃亏。这说明了一个很朴素的规律数据结构的选择本质上就是在选“哪些操作要快、哪些操作可以慢”。5.3 内存细节与稳定性三种方法里双亲表示法完全不用malloc所以不存在内存泄漏问题孩子表示法和孩子兄弟表示法都用到了动态内存释放顺序就得特别注意。我在最初写孩子兄弟表示法的FreeTree时犯过一个特别典型的错误先free了根节点再去递归释放它的孩子和兄弟。结果程序跑起来就崩因为free之后再去访问root-firstChild已经属于悬空指针操作。正确的顺序是先递归释放左子树和右子树最后释放根节点。这跟二叉树后序遍历的顺序是吻合的。孩子表示法的FreeTree也容易踩坑不能只遍历节点数组把firstChild对应的链表释放完就急着结束因为有的链表节点可能被子节点复用或引用混乱。更安全的做法是在释放一个节点时先从头到尾释放它的孩子链表再把该节点对应数组位置的firstChild置空避免二次释放。另外一个细节孩子表示法用尾插法维持孩子顺序时每次都要从头遍历链表找尾节点时间复杂度是O(k)。如果插入操作特别频繁可以额外维护一个tail指针或者用头插法牺牲顺序来换效率。面试里聊到这种问题能主动说出这个权衡通常会让面试官印象更深。6. 常见问题与排查技巧实录6.1 我踩过的几个坑第一个坑是根节点父下标设置。用双亲表示法的时候有人习惯把根节点的parent设成0或-1觉得无所谓。实际上如果设成0就相当于把根节点自己当成了自己的父亲遍历或查找祖先节点时会陷入死循环。强烈建议统一用-1作为“无父节点”的哨兵值因为数组下标0是合法数据位置不能污染。第二个坑是孩子表示法的内存释放。我最早写的时候只释放了每个节点的firstChild链表没把所有孩子节点都释放干净。后来用valgrind查内存泄漏一排红字才意识到孩子链表里每个CTNode都是独立malloc出来的必须逐个free。这一点在做长生命周期服务时特别重要比如一个常驻的进程反复构建和销毁树结构内存泄漏会越积越多最后只能重启服务来缓解。第三个坑是孩子兄弟表示法的遍历顺序。很多人一看到二叉链表就按照二叉树的中序遍历去写结果输出完全不对。原因在于孩子兄弟表示法虽然物理形态是二叉树但语义仍然是“第一个孩子”和“下一个兄弟”不是“左子树”和“右子树”。遍历时一定要清楚firstChild方向走的是深度方向nextSibling方向走的是广度方向。写错了整棵树的顺序就全乱了。6.2 常见问题速查表问题现象可能原因解决思路双亲表示法打印时出现“根节点的父亲是0号节点”根节点parent设成了0而不是-1统一使用-1作为空父节点标记孩子表示法遍历时少了某些节点插入孩子时用头插法或尾插法逻辑混乱明确头插和尾插的适用场景保持插入方式一致程序运行一段时间后内存持续上涨孩子链表或兄弟链表的节点没有释放遍历整棵树逐个free动态分配的节点孩子兄弟表示法输出顺序和预期不一致错误使用二叉树遍历思路先根时先访问当前节点再沿firstChild进入深度方向最后沿nextSibling进入广度方向节点数量大时双亲表示法找孩子极慢该场景频繁访问子节点双亲表示法不占优更换为孩子表示法或孩子兄弟表示法还有一点补充关于“如何把一种表示法转换成另一种”。这在实际工程里挺常见的比如你拿到一个设备树的静态配置内部是类似键值对的层级结构你要在内存里构建成方便遍历的树。我的习惯是先建一个孩子表示法的中间结构把孩子关系明确存储下来然后再转换成其他存储方式。因为孩子表示法最贴近人对树形结构的直觉转换逻辑最能保证不出错。6.3 几个能让你少走弯路的小技巧确定使用哪种表示法之前先把你最频繁的操作列出来。如果你有80%的操作都是“从叶子节点往上找祖先”那双亲表示法就是最优解反之如果你主要做“从根出发递归扫描整棵树”孩子表示法或孩子兄弟表示法更合适。不要一上来就套用某个模板存储结构的选择应该跟着操作走。如果需要在一个项目里同时用到多种表示法可以在结构体里预留一个“类型标记”字段比如用枚举标识当前树采用哪种存储策略这样后续扩展或转换时会轻松很多。真实项目里一棵树在运行过程中也可能发生结构变化从静态配置变成动态增删这时表示法本身可能也需要随之调整。写测试用例时我习惯用很小的树先验证逻辑比如只有两三个节点的树甚至是一个只有根节点的树。边界条件往往最容易暴露问题根节点的父亲是不是-1叶子节点的孩子链表是不是空空树遍历时会不会崩溃把这些边界情况过一遍代码的稳定性能提升一大截。6.4 三种表示法之外的延伸思考聊到这里你可能会发现树形结构的存储本身就是一个“空间换时间、时间换空间”的经典权衡问题。双亲表示法用固定数组换来了极简的内存管理孩子表示法用链表换来了灵活的孩子扩展孩子兄弟表示法用统一的二叉结构换来了算法的通用性。这些思想不只适用于“树”本身。回过头看B树为什么每一层存多个关键字、为什么节点分裂合并字典树为什么用字符作为路径而不是在节点里存字符串设备树解析、语法分析里的表达式树又是怎么在内存中构建和遍历的它们处理的具体问题不同但底层都绕不开“如何存储节点、如何表达节点之间的关系”这一核心命题。把三种基础表示方法吃透再看这些高阶结构你会觉得它们不过是在这个思路上加了各自的业务规则而已。我个人在实际项目里最常用的还是孩子兄弟表示法因为它的通用性最强能兼容我后续对树做的各种算法扩展。但如果是快速写个原型或者处理简单的父子关系查询我直接用双亲表示法代码量最少、也最容易调试。最后分享一个小技巧在调试树形代码时养成先画图再写码的习惯把树画出来哪一步遍历输出不对对照图一眼就能看出来比自己盯着代码干想高效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价