数据结构+算法(第13篇):精通二叉树的“独门忍术”——线索二叉树(上)

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

引言

二叉树的叶子节点的孩子都是空节点(Null),如果展开显示,如下图:

数据结构+算法(第13篇):精通二叉树的“独门忍术”——线索二叉树(上)_第1张图片

图 1 原始二叉树

二叉树的遍历方法,有“前序遍历”“中序遍历”和“后序遍历”三种。

“前序遍历”的规则:

  1. 先访问当前节点,再访问其左子树,最后访问右子树;
  2. 访问子树时,按照规则1递归执行。

“中序遍历”的规则:

  1. 先访问左子树,再访问当前节点,最后访问右子树;
  2. 访问子树时,按照规则1递归执行。

“后序遍历”的规则:

  1. 先访问左子树、再访问右子树,最后访问当前节点;
  2. 访问子树时,按照规则1递归执行。

如果要写出非递归的遍历算法,无论用哪种遍历方法,根据《再不会“降维打击”你就Out了!》《神力加身!动态编程》三篇文章中讲到的知识和技巧,都要借助堆栈来记忆“历史路径”以用于回溯。此方法是经典做法,但同时也有两个显著弊端:

  1. 堆栈需要额外的存储;
  2. 额外需要的存储带来的空间复杂度也不是O(1)型的——是与节点总数动态相关。

那么是否存在能找到一种技巧来解决上述的弊端呢?

今天就来介绍一种“奇技淫巧”——线索二叉树——来搞定这个问题:)

什么是线索二叉树?

严格意义上的线索二叉树定义如下:

一个二叉树通过如下的方法“穿起来”:所有原本为空的右(孩子)指针改为指向该节点在中序序列中的后继,所有原本为空的左(孩子)指针改为指向该节点的中序序列的前驱。

本文为了追求更直观、更快速的算法效果,对上述传统线索二叉树做了如下改良:

  1. 不同的遍历方法,对应的线索二叉树不同;
  2. 尽可能只利用后继节点。

这里先以“中序遍历”对应的线索二叉树为例。

“中序遍历”的线索二叉树说白了,就是两条规则:

  1. 将当前节点左子树中的最右叶子节点的右孩子指针指向当前节点本身;
  2. 对每个节点,递归执行规则1。

规则1看起来比较绕,用一张图来表示:

数据结构+算法(第13篇):精通二叉树的“独门忍术”——线索二叉树(上)_第2张图片

图2 “中序遍历”的线索二叉树

图2就是图1对应的“中序遍历”的线索二叉树。

线索二叉树的意义

传统的非递归型遍历算法,最挑战的地方在于要记忆“回溯点”。

以“中序遍历”为例,它要先访问当前节点的左子树之后,再访问当前节点——这意味着,访问完左子树前,先要记住当前节点位置;否则,访问完左子树之后,就找不到返回位置了。经典做法是通过堆栈来记忆。如果不想引入额外存储,那么怎么“记住”呢?

对比图2和图1,可以看出:“中序遍历”的线索二叉树其实就是复用了指向“空节点”的指针!——它将当前节点左子树的最右叶子节点的右孩子指针(原来指向“空节点”),指向了当前节点!

至于“前序遍历”的线索二叉树,就是将当前节点左子树的最右叶子节点的右孩子指针(原来指向“空节点”),指向当前节点的直接右孩子:

数据结构+算法(第13篇):精通二叉树的“独门忍术”——线索二叉树(上)_第3张图片

图3 “前序遍历”的线索二叉树

至于“后序遍历”的线索二叉树,就复杂了:

  1. 仅仅利用叶子节点的指针,解决不了所有后继节点的寻址;
  2. 非叶子节点的孩子指针无法直接复用,若用于指示后继节点,会丢失本来的孩子节点的链接。

数据结构+算法(第13篇):精通二叉树的“独门忍术”——线索二叉树(上)_第4张图片

图4 “后序遍历”的线索二叉树构造问题

解决上述困难,有两种途径:

  1. 利用其它遍历方法的线索二叉树来做“后序遍历”;
  2. 对原始二叉树做结构改造,以符合前驱或者后继寻址的需要。

如何将二叉树转换成线索二叉树?

为了节省篇幅,本文仅介绍“中序遍历”的线索二叉树的转换以及遍历算法。

构造线索二叉树的目的,说到底还是为了遍历。那么这就引出一个现实问题:

到底是构造完线索二叉树之后,再启动遍历呢?还是边构造边遍历呢?

从上面的线索二叉树的定义就可以看出,为了复用“空节点”指针,需要访问叶子节点,这个已经是遍历的一部分了,所以为了“不走重复路”,最经济的方法是边构造边遍历。

递归型“中序遍历”的线索二叉树的转换以及遍历算法:

数据结构+算法(第13篇):精通二叉树的“独门忍术”——线索二叉树(上)_第5张图片

数据结构+算法(第13篇):精通二叉树的“独门忍术”——线索二叉树(上)_第6张图片

数据结构+算法(第13篇):精通二叉树的“独门忍术”——线索二叉树(上)_第7张图片

数据结构+算法(第13篇):精通二叉树的“独门忍术”——线索二叉树(上)_第8张图片

非递归型“中序遍历”的线索二叉树的转换以及遍历算法:

数据结构+算法(第13篇):精通二叉树的“独门忍术”——线索二叉树(上)_第9张图片

数据结构+算法(第13篇):精通二叉树的“独门忍术”——线索二叉树(上)_第10张图片

后记:

接下来的文章将分别介绍“前序遍历”的线索二叉树的各种转换以及遍历算法、“后序遍历”的线索二叉树的各种转换以及遍历算法。

你可能感兴趣的:(数据结构与算法,数据结构,算法)