【PHP数据结构】完全二叉树、线索二叉树及树的顺序存储结构
在上篇文章中,我们学习了二叉树的基本链式结构以及建树和遍历相关的操作。今天我们学习的则是一些二叉树相关的概念以及二叉树的一种变形形式。
完全二叉树
什么叫完全二叉树呢?在说到完全二叉树之前,我们先说另外一个名词:“满二叉树”。像我们之前文章中演示过的那个二叉树,就是一颗“满二叉树”。在这颗树中,所有的结点都有两个孩子结点,没有哪个结点是只有一个孩子结点的,并且所有最底层的叶子结点都在同一层,这种树就称为“满二叉树”,也称为“完美二叉树”。

是不是非常漂亮的一颗树?没错,这种二叉树非常地完美,它没有多余的结点,也没有缺少的结点,非常的漂亮。但是,在现实中,完美的东西是很稀少的,人生总会有一点缺憾嘛。我们尽量不要让自己有太多的缺憾,但也总不能过上没有一丝缺憾的人生。所以,我们允许叶结点出现在最下层和次下层,而且最下层的叶结点集中在树的左部,也就是叶结点只能有左子树,那么,这样的一颗略带缺憾的树就叫做“完全二叉树”。不要担心它不完美,因为这样略带缺憾的人生才是完整的嘛,所以“完全二叉树”是一种理想的树结构。

从定义中,我们可以看出,一颗“满二叉树”,必定是一颗“完全二叉树”,而一颗叶子结点都在一层的并且所有结点都有左右孩子结点的“完全二叉树”也就是一颗”满二叉树“。
为什么要讲”满二叉树“和”完全二叉树“呢?当然是为了我们接下来的内容做铺垫。因为”满二叉树“是最符合二叉树性质的一颗树。还记得树系列的第一篇文章中介绍过的二叉树的那五个性质吗?当时我们就是以那颗”满二叉树“为例进行讲解的。而其中的 性质5 ,就是我们学习使用顺序结构存储二叉树的基础。
二叉树的顺序存储
通过”满二叉树“的概念,以及二叉树的 性质5 我们就可以实现使用一个数组来存储顺序结构的实现。
$treeList = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'];
相信大家不陌生吧,在上篇文章中,我们就是通过这个数组来建立链树的,而这个数组其实就是一个线性存储的二叉树。我们通过对比二叉树的 性质5 来看一下。
A 结点的下标是 1 ,它是我们的树根。它的子结点是 B 和 C ,对应的下标分别是 2 和 3 ,也就是 1 * 2 和 1 * 2 + 1 。
同理,我们再选取一个结点 F 。它的下标是 6 ,所以它的左孩子结点的下标是 6 * 2 = 12 ,对应的是 L ;它的右孩子结点是 6 * 2 + 1 = 13 ,对应的是 M 。
反过来看,一个结点的父结点就是 i / 2 。我们看下 K 结点的下标是 11 ,它的父结点就是 11 / 2 ,舍去小数点是下标 5 的位置,也就是结点 E ;结点 J 的下标是 10 ,它的父结点是 10 / 2 ,也是下标为 5 的 E 结点。
这下想以大家就明白了用数组是如何表示一颗二叉树结构了吧。而且数组这种结构更加的一维,更能体现出对于树的操作就是二维化一维的一种表示,也就是非线性转线性,这样才能让我们方便地操作这些数据。
针对顺序存储结构,也就是数组元素的遍历,也是可以使用先序、中序、后序以及层序的形式。不过这些遍历方法都需要根据二叉树的 性质5 来进行遍历。但更重要的是,只要给我一个下标,我们通过二叉树的性质,就可能很容易地知道它的下级结点和上级结点的位置,能够快速地获得这些结点的信息。这一大特点是链式结构的二叉树所没有的。
如果我们要存储的不是一颗”满二叉树“呢?甚至它都不是一颗完全二叉树的情况下,只需要将对应的结点设置为空值就行了。比如:
$treeList = ['', 'A', 'B', 'C', 'D', 'E', 'I', '', '', '', '', 'H', '', 'J', '', ''];
这颗树的结构所对应的二叉树图形就是这样的:

然后在建链树的方法中,我们只需要再增加一个判断就可以了。我们就可以通过这样一个顺序存储的二叉树快速地生成一颗链式存储的二叉树,方便我们之后的操作。
// 建立二叉树
function CreateBiTree($arr, $i)
{
if (!isset($arr[$i]) || !$arr[$i]) { // 这里增加了个判断,如果数组元素为空
return null;
}
$t = new TBTNode();
$t->data = $arr[$i];
$t->lChild = CreateBiTree($arr, $i * 2);
$t->rChild = CreateBiTree($arr, $i * 2 + 1);
return $t;
}
线索二叉树
一环套一环,接下来我们再来讲讲”线索二叉树“。这又是个什么东西呢?
从上面的学习中,我们知道了”满二叉树“和”完全二叉树“。但是这种结构都是非常理想的树结构,不过真实的情况可能大部分都是”理想很丰满,现实很骨感“。很多树并不能形成那样的完全二叉树的形式,更别提”满二叉树“了。而树的遍历又经常会使用栈或者队列来实现,这两种遍历方式基本都是线性的,也就是最好情况下也是 O(n) 的时间复杂度。那么,有没有什么更快一点的方式来提高遍历的效率呢?
我们这样来尝试一下:
如果树的叶子结点的左孩子结点为空,就让它指向前驱(上级)结点
如果树的叶子结点的右孩子结点为空,就让它指向后继结点
这样有什么好处呢?我们可以避免掉大范围的递归操作,从而加快树的遍历速度。在整个算法中,它并没有什么优势,因为我们需要将一颗树进行线索化,也就是去改变它的叶子结点的左右孩子的指向,这也是一次遍历。但是,如果你的操作是经常需要遍历,而且是来回的多次遍历,那么它的整体性能是要强于普通二叉树的遍历的。因为在一次线索化之后,它的遍历就是在快速的查找叶子结点的基础上进行普通的线性遍历操作,而不是递归操作。
对于线索二叉树来说,我们需要改变树的结点存储数据结构。
// 线索二叉树结点
class TBTNode
{
public $data;
public $lTag = 0;
public $rTag = 0;
public $lChild;
public $rChild;
}
我们增加了两个标志位,当 $lTag 或 $rTag 为 1 时,$lChild 或 $rChild 分别指向前驱或后继结点。这样在最后的遍历时,我们就可以快速地通过这个 tag 标志位分辨出结点的指向状态。
然后我们先简单地建立一颗树。使用上一节中的那个示例。
// 建立二叉树
function CreateBiTree($arr, $i)
{
if (!isset($arr[$i]) || !$arr[$i]) { // 这里增加了个判断,如果数组元素为空
return null;
}
$t = new TBTNode();
$t->data = $arr[$i];
$t->lChild = CreateBiTree($arr, $i * 2);
$t->rChild = CreateBiTree($arr, $i * 2 + 1);
return $t;
}
$treeList = ['', 'A', 'B', 'C', 'D', 'E', 'I', '', '', '', '', 'H', '', 'J', '', ''];
$tree = CreateBiTree($treeList, 1);
接下来就是最重要的线索化过程,我们可以建立前序、中序、后序的线索二叉树。对应的,在最后的线索二叉树遍历时获得的结果也将是这三种遍历方式所对应的结果。在这里,我们学习最普遍的也是最经典的”中序线索二叉树“。所以,我们以中序遍历的形式将这颗树线索化。
// 线索化
function InThread(?TBTNode $p, ?TBTNode &$pre)
{
if ($p) {
// 递归,左子树线索化
InThread($p->lChild, $pre);
if (!$p->lChild) {
// 建立当前结点的前驱线索
$p->lChild = $pre;
$p->lTag = 1;
}
if ($pre && !$pre->rChild) {
// 建立当前结点的后继线索
$pre->rChild = $p;
$pre->rTag = 1;
}
$pre = $p; // $pre 指向当前的 $p ,作为 $p 将要指向的下一个结点的前驱结点指示指针
$p = $p->rChild; // $p 指向一个新结点,此时 $pre 和 $p 分别指向的结点形成了一个前驱后继对,为下一次线索化做准备
// 递归,右子树线索化
InThread($p, $pre);
}
}
// 创建线索二叉树
function createInThread(TBTNode $root)
{
$pre = null; // 前驱结点指针
if($root){
InThread($root, $pre);
$pre->rChild = null; // 非空二叉树,线索化
$pre->rTag = 1; // 后处理中序最后一个结点
}
}
createInThread($tree);
var_dump($tree);
// object(TBTNode)#1 (5) {
// ["data"]=>
// string(1) "A"
// ["lTag"]=>
// int(0)
// ["rTag"]=>
// int(0)
// ["lChild"]=>
// object(TBTNode)#2 (5) {
// ["data"]=>
// string(1) "B"
// ["lTag"]=>
// int(0)
// ["rTag"]=>
// int(0)
// ["lChild"]=>
// object(TBTNode)#3 (5) {
// ["data"]=>
// string(1) "D"
// ["lTag"]=>
// int(1)
// ["rTag"]=>
// int(1)
// ["lChild"]=>
// NULL
// ["rChild"]=>
// *RECURSION*
// }
// ……
关于算法的具体步骤在注释中已经写得很详细了。一句话总结就是在中序遍历的过程中,根据结点的信息来确定它的左右孩子的形式,如果有左右孩子就继续,如果没有任一一个孩子的话,就将左右结点指向前驱或者后继。建立之后的线索二叉树就如图所示:

最后就是遍历了。我们需要的是能够快速地获得最左叶子结点的信息,然后就是下一跳的信息,这时,线索的威力就发挥出来了。
// 以 $p 为根的中序线索二叉树中,中序序列下的第一个结点,也就是最左边那个结点
function First(?TBTNode $p){
while($p->lTag == 0){
$p = $p->lChild; // 最左下结点(不一定是叶子结点)
}
return $p;
}
// 在中序二叉树中,结点 $p 在中序下的后继结点
function NextNode(?TBTNode $p){
if($p->rTag == 0){
return First($p->rChild);
}else{
return $p->rChild; // 如果 rTag == 1 ,直接返回后继线索
}
}
// 在中序线索二叉树上进行中序遍历
function Inorder(TBTNode $root){
// 第一个结点 结点不为空 下一个结点
for($p = First($root);$p;$p=NextNode($p)){
echo $p->data, ',';
}
}
Inorder($tree); // D,B,E,H,A,I,J,C,
当遇到 $lTag 不为 0 的结点时,这个结点就是最左的那个结点了,如果这个不为空的话,【输出】它。接着我们获得下一跳的结点,也就是判断这个结点的右孩子 $rTag 标志,如果是为 0 的,也就是它还有右孩子,【输出】后向下查找,直到找到一个 $rTag 也为 1 的结点,直接返回这个结点的后继,也就是中序遍历的中间那个结点,【输出】它。
最后输出的顺序是不是和我们中序遍历的结果一样呢?注意看代码,在遍历中序线索二叉树的时候,我们没有用一个递归吧,全部是使用的 while() 和 for() 就完成了对这个线索二叉树的遍历。
总结
坚持到现在不容易,不能再小看数据结构了吧?现在还只是树,我们的图都还没开始呢!当然,也不要害怕,一步一步的学,慢慢掌握,不要幻想一口气吃成个胖子。写完这篇文章我也不可能马上就手写出一个中序的线索二叉树来的。大家还是以理解原理为主,如果说真能手写的话,那也是为了面试而去背的或者是为了考研而准备的。这样的小同学在面试中我反而要更多问一些其它的问题,毕竟临时抱佛脚的准备远不如深入理解带来的感悟更能打动人!
测试代码:
参考资料:
《数据结构》第二版,严蔚敏
《数据结构》第二版,陈越
《数据结构高分笔记》2020版,天勤考研
关注公众号:【硬核项目经理】获取最新文章
添加微信/QQ好友:【xiaoyuezigonggong/149844827】免费得PHP、项目管理学习资料
知乎、公众号、抖音、头条搜索【硬核项目经理】
B站ID:482780532
【PHP数据结构】完全二叉树、线索二叉树及树的顺序存储结构的更多相关文章
- EOJ 3348 树的顺序存储结构
前面介绍了树的链式存储结构,那么如何用顺序存储来存储一棵树呢?在顺序存储时,我们除了存储每个结点值外,还要存储树中结点与结点之间的逻辑关系(即双亲与孩子结点之间的关系).下面介绍树的双亲存储法. 编号 ...
- 数据结构之线索二叉树——C语言实现
线索二叉树操作 (1) 线索二叉树的表示:将每个节点中为空的做指针与右指针分别用于指针节点的前驱和后续,即可得到线索二叉树. (2) 分类:先序线索二叉树,中序线索二叉树,后续线索二叉树 (3) 增 ...
- javascript实现数据结构:线索二叉树
遍历二叉树是按一定的规则将树中的结点排列成一个线性序列,即是对非线性结构的线性化操作.如何找到遍历过程中动态得到的每个结点的直接前驱和直接后继(第一个和最后一个除外)?如何保存这些信息? 设一棵二叉树 ...
- 数据结构4:顺序表(线性表的顺序存储结构)及C语言实现
逻辑结构上呈线性分布的数据元素在实际的物理存储结构中也同样相互之间紧挨着,这种存储结构称为线性表的顺序存储结构. 也就是说,逻辑上具有线性关系的数据按照前后的次序全部存储在一整块连续的内存空间中,之间 ...
- ※数据结构※→☆非线性结构(tree)☆============二叉树 顺序存储结构(tree binary sequence)(十九)
二叉树 在计算机科学中,二叉树是每个结点最多有两个子树的有序树.通常子树的根被称作“左子树”(left subtree)和“右子树”(right subtree).二叉树常被用作二叉查找树和二叉堆或是 ...
- 【Java】 大话数据结构(9) 树(二叉树、线索二叉树)
本文根据<大话数据结构>一书,对Java版的二叉树.线索二叉树进行了一定程度的实现. 另: 二叉排序树(二叉搜索树) 平衡二叉树(AVL树) 二叉树的性质 性质1:二叉树第i层上的结点数目 ...
- 数据结构《9》----Threaded Binary Tree 线索二叉树
对于任意一棵节点数为 n 的二叉树,NULL 指针的数目为 n+1 , 线索树就是利用这些 "浪费" 了的指针的数据结构. Definition: "A binary ...
- ****** 二 ******、软设笔记【数据结构】-KMP算法、树、二叉树
五.KMP算法: *KMP算法是一种改进的字符串匹配算法. *KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的.具体实现就是实现一个next()函 ...
- SDUT 3340 数据结构实验之二叉树一:树的同构
数据结构实验之二叉树一:树的同构 Time Limit: 1000MS Memory Limit: 65536KB Submit Statistic Problem Description 给定两棵树 ...
随机推荐
- 【笔记】求数据前n个主成分以及对高维数据映射为低维数据
求数据前n个主成分并进行高维数据映射为低维数据的操作 求数据前n个主成分 先前的将多个样本映射到一个轴上以求使其降维的操作,其中的样本点本身是二维的样本点,将其映射到新的轴上以后,还不是一维的数据,对 ...
- SQL 练习11
查询至少有一门课与学号为" 01 "的同学所学相同的同学的信息 SELECT * from Student WHERE SId in (SELECT SId from sc WHE ...
- 【现学现卖】th:href标签动态路径设置,thymeleaf获取session中的属性值
update:2020-02-28:按道理来说这个功能在前后端分离的时候应该不怎么用的上,基本到现在我还是没遇到过有这样的需求,不过也是一种方法就是.th:href="@{/{role}/l ...
- 「移动端」touch事件,touchEvent对象
随着智能手机普及,有越来越多的手机网页和网页版游戏,手机触摸.移动.旋转等等,多种操作.一般电脑的人机交互靠的是鼠标,而手机用的就是触摸.区别有: PC 端一个电脑只能有一个鼠标,而移动端有多点触摸. ...
- noip 模拟 6
果然考试一多就改不过来了 考试经过 上来看题,T1似乎是一个计数题,但看见1e9的数据范围就觉得不可做,拿了20部分分匆忙跑路 T2是个图论题,不过一看统计种类就发现是自己不会的东西,瞄准30分冲了一 ...
- 题解—P3000 [USACO10DEC]Cow Calisthenics G
做这题之前最好学会 "树形 \(dp\) 求树的直径"这一前缀知识(虽然我会但是我还是没想出来) 几乎想到要求直径这道题也没什么问题了(这不是废话吗,为什么题面里给了"直 ...
- rsync基本使用
概念 rsync是linux系统下的数据镜像备份工具.使用快速增量备份工具Remote Sync可以远程同步,支持本地复制,或者与其他SSH.rsync主机同步. 目前,已支持跨平台,可以在Windo ...
- docker部署elasticsearch-+-Kibana(6-8)-+-SpringBoot-2-1-6
elasticsearch快速开始 docker run -d --name elasticsearch --net somenetwork -p 9200:9200 -p 9300:9300 -e ...
- UWP App Data存储和获取
这篇博客介绍如何在UWP开发时,如何存储App Data和获取. App Data是指用户的一些设定,偏好等.例如,App的主题,是否接收推送,离线接收消息等.需要区分下App Data和User D ...
- WPF QQ群发助手
一.界面如下