(b)树的表示

官官方信息技术老师·19 页·深入(追求细节与边界)·0 次浏览·38 天前
数据结构内存布局链式与数组

树的表示

看完你能说出五种存树方法的优劣并徒手画出每种内存布局

按 空格/→ 演示下一步

问一句
1 / 19 页

全部页面点击任意一页,跳回舞台从这页播放

数据结构内存布局链式与数组

树的表示

看完你能说出五种存树方法的优劣并徒手画出每种内存布局

1第 1 页 · 树的表示

从画在纸上的树到内存

上一页我们把树画成了带箭头的图:圆圈是结点,线是父子关系。但内存里没有纸和笔,只有一格一格连续的单元。问题来了:怎么把这张图塞进这些格子里?

纸上是图形
位置、箭头都是画出来的,随你摆放,没有固定坐标
内存是数组
只有编号 0,1,2… 的连续单元,图的结构信息必须丢掉或者编码
约定映射规则
核心:定一套规则,说清「哪个格子里放谁的子结点」
семей家谱抄进表格对应 →树的存储

家谱的辈分关系,对应表格里用行号约定的父子指向

2第 2 页 · 从画在纸上的树到内存

父亲表示法:核心思想

上一页我们把树搬进了内存,但每个结点该记些什么?想想公司的人事系统:员工表里有一栏「直属上司」,填的是上司的工号。树里每个结点只要记下父亲的位置,整棵树就稳了。

父亲下标
每个结点额外存一个数:父结点在数组中的位置
根结点特殊
根没有父亲,用 -1 或自身标记表示「到顶了」
只用一个数组
所有结点顺序存放,父子关系全靠下标维系
员工表的直属上司栏对应 →父亲表示法

上司工号对应父亲下标,查表即知谁是顶头上司

3第 3 页 · 父亲表示法:核心思想

父亲表示法的结构图解

上排是原树,下排是内存中的数组:每格存 data 和 parent(父亲的下标),根记 -1。

图解渲染中…
a0:根结点:没有父亲,parent 存 -1 作为标记a3:D 的父亲是 B(下标 1),所以 parent 列填 1
4第 4 页 · 父亲表示法的结构图解

父亲表示法上怎么找父亲、找孩子

在父亲表示法里,找父亲和找孩子是两件成本完全不同的事。

1
看结构
每个节点只存一个 parent 下标,指向父亲位置
2
找父亲
直接读 parent 字段,一步到位,O(1)
3
找孩子
没有孩子指针,只能从头扫全表比对 parent
4
复杂度
n 个节点扫一遍,找孩子 O(n),差异由此而来
5第 5 页 · 父亲表示法上怎么找父亲、找孩子

父亲表示法的结构定义

c

用 C 结构体数组落地父亲表示法:定义、初始化、找父结点一次看全。

代码高亮加载中…

核心只有一行字段 parent:存父亲下标。找父亲就是读数组元素的一个字段,O(1);-1 兼作根的标记和越界哨兵。

6第 6 页 · 父亲表示法的结构定义

孩子表示法:核心思想

父亲表示法倒着放:每人都记「我爹是谁」。但找孩子时只能全表扫一遍,很慢。换个方向想——让每个结点自己记住孩子,找孩子不就快了吗?这就是孩子表示法的出发点。

结点数组
所有结点顺序存放,数据域与下标一一对应
孩子链表
每个结点挂一条单链表,存所有孩子的下标
方向反转
父亲法查孩子要遍历全表,孩子法查孩子顺着链走
部门的下属名单对应 →孩子链表

部门负责人手里的下属名单,对应结点挂的孩子链表

7第 7 页 · 孩子表示法:核心思想

孩子链表的结构图解

左边是存所有结点的顺序表,右边每个结点拖出一条单链表,专门串它的孩子。

图解渲染中…
a1:0号是根R,它的孩子是1号A、2号Bn1:链表里存的是孩子的下标,不是孩子内容null3:C是叶子,没有孩子,链表为空
8第 8 页 · 孩子链表的结构图解

父亲 vs 孩子:各快在哪一步

两种表示法都能存树,但一个查父快、一个查子快——混淆点在于:到底快在哪一步、代价是什么。

父亲表示法
  • 结构:每个节点存 parent 指针
  • 找父:沿 parent 一步 O(1)
  • 找子:全表扫一遍 O(n)
孩子表示法
  • 结构:每个节点挂孩子链表
  • 找父:没有 parent,需 O(n) 扫
  • 找子:顺着链表 O(度数)
高频找父亲(如并查集溯源)选左;高频枚举孩子(如遍历、调度)选右。
9第 9 页 · 父亲 vs 孩子:各快在哪一步

父亲+孩子表示法

刚才对比过:找父亲快靠 parent 列,找孩子快靠孩子链表,各赢一半。那能不能把两张表拼在一起?可以——给每个结点同时挂上父亲下标和第一条孩子边。

加一列 parent
结点表保留 parent 下标,找父亲回到父亲表示法的速度
挂孩子链表
每个结点带 firstChild 指针,串起孩子链表,找孩子不再扫全表
空间代价
每结点多一个指针域,n 个结点多出 n 个指针的空间
手机通讯录对应 →父亲+孩子表示法

每人存直属上级的电话,又存自己的下属分组

10第 10 页 · 父亲+孩子表示法

父亲+孩子表示法的实现

python

带孩子链表和 parent 域的结构定义,以及找父亲、找孩子两个操作的真实实现。

代码高亮加载中…

parent 域存下标实现 O(1) 找父亲;children 列表沿链表找孩子。add 时同时维护两处,双向信息各付一次代价。

11第 11 页 · 父亲+孩子表示法的实现

长子+兄弟:核心思想

前面父亲表示法好找爹却难找孩子,孩子表示法正好反过来。能不能只用一种固定结构,两个方向都够用?只需给每个结点记两件事:第一个孩子是谁,下一个兄弟是谁。

只记两个指针
每结点存 firstChild 和 nextSibling 两个指针
长子继承
firstChild 指向结点的第一个孩子
兄弟手拉手
nextSibling 把同一层的兄弟串成一条链
树变二叉
任意树都能唯一转成这种结构,形式与二叉链表一致
长子继承制对应 →长子+兄弟表示

家长只认长子,长子的弟弟们手拉手排队

12第 12 页 · 长子+兄弟:核心思想

长子+兄弟的结构图解

每个结点只有两个指针:fc 指向第一个孩子,nb 指向下一个兄弟。顺着箭头就能还原整棵树。

图解渲染中…
a1:根结点A:fc指向长子B,它没有右兄弟a2:B的fc指长子C,nb指兄弟Ea3:C的nb串起兄弟链C→Da5:E是B的下一个兄弟,由B的nb指到
13第 13 页 · 长子+兄弟的结构图解

一般树如何变换成二叉树

任意一般树只需三步,就能规整成左孩子右兄弟的二叉树。

1
加线
各兄弟之间连一条线,把同层的兄弟横向串起来
2
抹线
每个结点只保留与长子的连线,抹掉到其他孩子的线
3
旋转
以根为轴顺时针转45°,横线变右子树、竖线变左子树
14第 14 页 · 一般树如何变换成二叉树

长子+兄弟的结点定义

c

用 C 语言定义长子-兄弟结点,并在转换后的二叉树上先序遍历。

代码高亮加载中…

结构体只有两个指针:左指长子、右指兄弟;遍历函数递归地走这两条链,转换后的二叉树完全复用二叉树遍历逻辑。

15第 15 页 · 长子+兄弟的结点定义

五种表示法总对比

五种表示法学完容易混:都是存树,凭什么分五种?比一比找父、找子、空间三笔账就清楚了。

父亲表示法
  • 找父 O(1),顺着 parent 一跳即达
  • 找孩子要全表扫描,O(n)
  • 只存一个下标,空间最省
长子+兄弟表示法
  • 找父难:无父指针,需从根遍历
  • 找孩子 O(d):沿长子横走兄弟链
  • 每结点两指针,空间换取结构规整
高频找父选父亲表示法;要频繁遍历孩子、或需转成二叉树处理时,选长子+兄弟。
16第 16 页 · 五种表示法总对比

自测:给一棵树选表示法

点击作答

某个应用需要频繁地找指定结点的所有孩子,哪种表示法访问路径最直接、最高效?

17第 17 页 · 自测:给一棵树选表示法

要点收束

  • ✓没有万能表示法,选型取决于操作频率
  • ✓找父亲用父亲法,遍历孩子用孩子法
  • ✓长子+兄弟用二叉结构统一一般树
  • ✓空间换时间:多存指针换来双向可达
  • ✓先把操作需求列清,再定结构
延伸主题:从森林到二叉树的转换树与二叉树的互相还原树的遍历在物理结构上的实现
18第 18 页 · 要点收束

课后思考

三个问题没有唯一答案:先自己想十分钟,再看参考答案,

1父亲表示法找一个结点的所有孩子最坏要 O(n),能否既保留父亲域又把找孩子降到 O(度)?代价是什么?

参考答案思路:父亲域 + 孩子链表并存。每个结点多一条链表指针,空间从 O(n) 增到约 O(n+m)。思考方向:多花的空间换来了哪一步的加速?

2同一棵一般树转成“长子+兄弟”二叉树时结果唯一吗?如果规定兄弟从右到左接入,会发生什么?

参考答案唯一性来自遍历约定:长子固定、兄弟顺序固定,结果才唯一。换接入顺序会得到不同形状的二叉树,但还原后仍是原树——可动手画一棵三叉树验证。

3文件系统的目录树层级极深、每层结点数不定,你会用哪种表示法?哪些真实约束会影响选择?

参考答案线索:找父亲(cd ..)要父亲域,列目录要孩子链,可用父亲+孩子或长子+兄弟。再想磁盘按块分配、指针存储成本等真实约束如何改写结论。

19第 19 页 · 课后思考