文章目录
线索二叉树
一、基本概念
我们都知道在二叉树地遍历中我们只能找到节点的左、右孩子信息,而不能直接得一个节点地前驱和后继的信息。而如果要得到这些信息可采用以下两种方法:
- 第一种方法是将二叉树遍历一遍,在遍历过程中便可得到节点的前驱和后继,但这种动态访问浪费时间;
- 第二种方法是充分利用二叉链表中的空链域,将遍历过程中节点的前驱、后继信息保存下来。
如果一棵二叉树,所有原本为空的右孩子改为指向该节点的遍历结果的后继,所有原本为空的左孩子改为指向该节点的遍历结果的前驱,那么修改后的二叉树被称为线索二叉树 (Threaded binary tree, TBT)。指向前驱、后继的指针被称为线索,对二叉树以某种遍历顺序进行扫描并为每个节点添加线索的过程称为二叉树的线索化 ,进行线索化的目的是为了加快查找二叉树中某节点的前驱和后继的速度。
对二叉树按照不同的遍历次序进行线索化,可以得到不同的线索二叉树,包括先序线索二叉树、中序线索二叉树和后序线索二叉树。这里重点介绍中序遍历的线索化。
简单来说就是:二叉树的线索化实现了空指针的 “废物利用”,将左右空指针分别指向了对应遍历结果的前驱和后继。是不是超级简单
一旦这个树被线索化之后,我们再去遍历这棵树就能像链表一样从头到尾依次访问,畅通无阻,使得二叉树的遍历表现得更加高效,更加灵活,同时也实现了二叉树遍历的非递归形式。
二、实现思路
那么我们该怎么去实现这样一个线索化呢?仔细观察上面的图你会发现,我们新添加了的线索——也就是图中的虚线,可以很直接地看出哪个是原来的边,哪个是线索。那这个在代码中怎么体现呢?这里我们就可以一个节点的结构体中加上两个参数:ltag
和 rtag
。
它们的作用如下:
ltag = 0 | left 指针指向节点的左孩子 |
---|---|
ltag = 1 | left 指针指向节点的遍历前驱 |
rtag = 0 | right 指针指向节点的右孩子 |
rtag = 1 | right 指针指向节点的遍历后继 |
这样一来就能区分了,只要这个 ltag
和 rtag
为 1 了,就说明对应的指针是一个线索。
三、线索化实现
1. 节点结构体定义
// ThreadBinaryTree.h
typedef char TBTDataType;
typedef struct ThreadBinaryTreeNode {
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
TBTDataType data;
int ltag;
int rtag;
} TBTNode;
2. 线索二叉树的初始化
// ThreadBinaryTree.c
TBTNode* GetNewNode(TBTDataType data){
TBTNode* newNode = (TBTNode*)malloc(sizeof(TBTNode)); // 申请新的空间
newNode->data = data; // 初始化数据
newNode->ltag = newNode->rtag = 0; // 记得初始化为0
newNode->left = newNode->right = NULL;
return newNode;
}
// ThreadBinaryTreeTest.c
void test1(){
TBTNode* A = GetNewNode('A');
TBTNode* B = GetNewNode('B');
TBTNode* C = GetNewNode('C');
TBTNode* D = GetNewNode('D');
TBTNode* E = GetNewNode('E');
A->left = B;
A->right = C;
B->left = D;
B->right = E;
C->left = F;
}
3. 线索二叉树的销毁
注意:我们销毁一个线索二叉树和销毁一个普通的二叉树是一样的,我们不会管线索的那条 “虚线”,因此我们只沿着实际的边进行销毁,所以在递归销毁节点之前先要判断该指针是否为实际指向左右孩子的指针,即判断
ltag
或rtag
是否为 0。
// ThreadBinaryTree.c
void TreeDestroy(TBTNode** proot) {
if (*proot == NULL) {
return;
}
// 只沿着实际的边进行销毁
if((*proot)->ltag == 0) TreeDestroy(&((*proot)->left)); // 注意取地址之后才是二级指针
if((*proot)->rtag == 0) TreeDestroy(&((*proot)->right));
free(*proot);
*proot = NULL;
}
4. 中序遍历
和上面的销毁逻辑一样,我们正常中序遍历的时候也是只沿着原本的边进行遍历,所以在递归调用前先判断
ltag
和rtag
的值。
// ThreadBinaryTree.c
void InOrder(TBTNode* root) {
if (root == NULL) {
// printf("NULL ");
return;
}
// 按照“边”进行遍历
if(root->ltag == 0)
InOrder(root->left);
printf("%c ", root->data);
if(root->rtag == 0)
InOrder(root->right);
}
5. *线索化
为了理解线索化的思路,首先来思考一个问题:我们中序遍历结果的每一个节点,都是在哪一行打印(访问)的?这太简单了,仅用了 0 秒便说出了答案,当然是这一行:
printf("%c ", root->data);
好,那么既然在这一行我们可以依次的访问到每一个节点,那么我们进行线索化的时候就可以在这里进行操作。我们需要的是将节点的空指针指向前驱和后继,而你又可以访问到每一个节点,我们就可以在这里记录一下你当前访问到的节点 PreNode
,那么我在访问下一个节点的时候我就可以跟这个 PreNode
进行一个线索化的操作——让该节点的左指针指向 PreNode
(前驱)。同时我又可以让 PreNode
的右指针指向我当前的节点,也就是 PreNode
的后继。这样也就完成了线索化。
(当然上面说的建立线索化的前提还得判断它们的左右指针是否为空)
下面我们来具体看一下中序遍历的线索化是如何在代码中实现的:
// ThreadBinaryTree.h
extern TBTNode* PreNode; // PreNode设计成全局变量,注意这里加extern,且不要初始化!文章末尾再说为什么
// ThreadBinaryTree.c
TBTNode* PreNode = NULL; // 在此文件内的开始初始化PreNode
// 基于中序遍历的代码结构进行改造
void BuildInOrderThread(TBTNode* root) {
if (root == NULL) {
// printf("NULL ");
return;
}
if(root->ltag == 0)
BuildInOrderThread(root->left);
/************在此区间内可以对当前访问到的节点然后进行线索化操作*************/
if(root->left == NULL){
root->left = PreNode; // 如果当前节点左指针为空,那就指向前驱
root->ltag = 1; // 改为 1 说明左指针为一条线索
}
if(PreNode && PreNode->right == NULL){ // 前一个节点有可能是空,所以要加一个判断条件
PreNode->right = root; // 如果前一个节点的右指针为空,就指向后继
PreNode->rtag = 1; // 说明右指针为一条线索
}
PreNode = root; // 更新前一个节点为当前节点,然后当前节点继续遍历着走
/************在此区间内可以对当前访问到的节点然后进行线索化操作*************/
if(root->rtag == 0) BuildInOrderThread(root->right);
}
这就完了吗?当然没有,现在还存在两个问题:
- 当我们访问到中序遍历的最后一个节点的时候,我们没法将这个最后一个节点的右指针变成一个线索,也就是无法将最后一个节点的
rtag
赋值为 1。- 我们想要对这个已经线索化好之后的二叉树进行遍历,那么我们必须要获取到中序遍历的第一个节点才行,但是上面的函数没有给到我们中序遍历的第一个节点的信息。
解决方案:
对于问题一:这个时候我们就需要为这个函数再另外设计出这样一个功能去弥补这样一个缺陷,正所谓没有什么是一层 ”封装“ 解决不了的,如果有,那就两层。于是我们就可以把上面的函数作为一个子函数封装到一个正儿八经的 BuildInOrderThread
当中。
// ThreadBinaryTree.c
void __BuildInOrderThread(TBTNode* root) { // 函数名前面添加两条下划线
// 内容和上面的一样(递归调用的函数名要改改)
}
void BuildInOrderThread(TBTNode* root) {
__BuildInOrderThread(root);
PreNode->right = NULL; // 弥补缺陷————让最后的节点的右指针变为线索
PreNode->rtag = 1;
return;
}
对于问题二:我们定义一个新的全局变量 InOrderRoot
表示中序遍历的第一个节点,这样一来,我们就可以从这个节点开始来挨个遍历整个二叉树了。
// ThreadBinaryTree.h
extern TBTNode* InOrderRoot; // 同样地,注意这里加extern,且不要初始化。
// ThreadBinaryTree.c
TBTNode* InOrderRoot = NULL; // 在此文件内的开始初始化
void __BuildInOrderThread(TBTNode* root) {
// 同上
/************在此区间内可以对当前访问到的节点然后进行线索化操作*************/
if(InOrderRoot == NULL)
InOrderRoot = root; // 第一次进到这个区间的时候就是访问到第一个节点的时候,这个时候初始化中序遍历的第一个节点
// 同上
/************在此区间内可以对当前访问到的节点然后进行线索化操作*************/
// 同上
}
解决完这两个问题之后,我们的线索化就算完成了,完整的函数代码如下:
void __BuildInOrderThread(TBTNode* root) {
if (root == NULL) {
// printf("NULL ");
return;
}
if(root->ltag == 0)
__BuildInOrderThread(root->left);
/************在此区间内可以对当前访问到的节点然后进行线索化操作*************/
if(InOrderRoot == NULL)
InOrderRoot = root; // 第一次进到这个区间的时候就是访问到第一个节点的时候,这个时候初始化中序遍历的第一个节点
if(root->left == NULL){
root->left = PreNode; // 如果当前节点左指针为空,那就指向前驱
root->ltag = 1; // 改为 1 说明左指针为一条线索
}
if(PreNode && PreNode->right == NULL){ // 前一个节点有可能是空,所以要加一个判断条件
PreNode->right = root; // 如果前一个节点的右指针为空,就指向后继
PreNode->rtag = 1; // 说明右指针为一条线索
}
PreNode = root; // 更新前一个节点为当前节点,然后当前节点继续遍历着走
/************在此区间内可以对当前访问到的节点然后进行线索化操作*************/
if(root->rtag == 0) __BuildInOrderThread(root->right);
}
void BuildInOrderThread(TBTNode* root) {
__BuildInOrderThread(root);
PreNode->right = NULL; // 弥补缺陷————让最后的节点的右指针变为线索
PreNode->rtag = 1;
return;
}
6. 中序遍历线索化二叉树(实现非递归)
既然我们实现了线索化,那么遍历这个二叉树就会更加快捷高效,可以变得像链表一样,我直接遍历的时候走到下一个节点就是了。所以我们可以编写一个函数来实现 “走到下一个节点” 这样一个操作,我把它命名为 GetNext
。
我们都知道一个节点的右指针如果是一个线索,那么它一定是指向它的后继的,那就好办了,我只需要判断一下 rtag
,如果是 1 那我直接返回当前节点的下一个节点。
if(root->rtag == 1)
return root->right;
那如果右指针不是一个线索呢?那如何找到中序遍历的下一个节点?比如下图中的 A,从结果来看它下一个该遍历的是 F 对吧。怎么来理解这个问题,时刻记住,中序遍历的顺序是:左 根 右。如果一个根有右子树,那么它中序遍历的下一个节点是右子树中最左边的那个节点。仔细想想应该就想通了。
所以放在代码里面我们就好办了,如果这个节点的右指针不是线索,那就先让它往右走一个节点,再让它一直往左走直到不能再走了,那就是这个节点中序遍历的下一个节点。(注意是沿着边走而不是线索)
// 右指针不是线索的情况
root = root->right; // 往右走一步
while(root->ltag == 0 && root->left){ // 沿着边一直走
root = root->left; // 一直往左走
}
return root; // 此时的 root 就是下一个该遍历的节点
补充一个点:这里的 && root->left
实际上是没啥作用的,因为在一个线索二叉树中空指针基本上都被 “废物利用” 了,所有的只要不是中序遍历的开头的节点,那么一个节点的左指针就不可能指向空。
GetNext
函数的完整演示:
// ThreadBinaryTree.c
TBTNode* GetNext(TBTNode* root){
if(root->rtag == 1)
return root->right;
root = root->right;
while(root->ltag == 0 && root->left){
root = root->left;
}
return root;
}
// ThreadBinaryTreeTest.c
BuildInOrderThread(A); // 先线索化
TBTNode* Node = InOrderRoot; // 获取到中序遍历的第一个节点
while(Node){ // 依次打印每个节点
printf("%c ", Node->data);
Node = GetNext(Node); // 更新成为下一个节点
}
7. 最终完整代码
// ThreadBinaryTree.h
#include <stdio.h>
#include <stdlib.h>
typedef char TBTDataType;
typedef struct ThreadBinaryTreeNode {
struct ThreadBinaryTreeNode* left;
struct ThreadBinaryTreeNode* right;
TBTDataType data;
int ltag;
int rtag;
} TBTNode;
extern TBTNode* PreNode;
extern TBTNode* InOrderRoot;
TBTNode* GetNewNode(TBTDataType data);
void InOrder(TBTNode* root);
void __BuildInOrderThread(TBTNode* root);
void BuildInOrderThread(TBTNode* root);
TBTNode* GetNext(TBTNode* root);
void TreeDestroy(TBTNode** proot);
// ThreadBinaryTree.c
#include "ThreadBinaryTree.h"
TBTNode* PreNode = NULL;
TBTNode* InOrderRoot = NULL; // 初始化两个全局变量
TBTNode* GetNewNode(TBTDataType data){
TBTNode* newNode = (TBTNode*)malloc(sizeof(TBTNode)); // 申请新的空间
newNode->data = data; // 初始化数据
newNode->ltag = newNode->rtag = 0; // 记得初始化为0
newNode->left = newNode->right = NULL;
return newNode;
}
void InOrder(TBTNode* root) {
if (root == NULL) {
// printf("NULL ");
return;
}
// 按照“边”进行遍历
if(root->ltag == 0)
InOrder(root->left);
printf("%c ", root->data);
if(root->rtag == 0)
InOrder(root->right);
}
void __BuildInOrderThread(TBTNode* root) {
if (root == NULL) {
// printf("NULL ");
return;
}
if(root->ltag == 0)
__BuildInOrderThread(root->left);
/************在此区间内可以对当前访问到的节点然后进行线索化操作*************/
if(InOrderRoot == NULL)
InOrderRoot = root; // 第一次进到这个区间的时候就是访问到第一个节点的时候,这个时候初始化中序遍历的第一个节点
if(root->left == NULL){
root->left = PreNode; // 如果当前节点左指针为空,那就指向前驱
root->ltag = 1; // 改为 1 说明左指针为一条线索
}
if(PreNode && PreNode->right == NULL){ // 前一个节点有可能是空,所以要加一个判断条件
PreNode->right = root; // 如果前一个节点的右指针为空,就指向后继
PreNode->rtag = 1; // 说明右指针为一条线索
}
PreNode = root; // 更新前一个节点为当前节点,然后当前节点继续遍历着走
/************在此区间内可以对当前访问到的节点然后进行线索化操作*************/
if(root->rtag == 0) __BuildInOrderThread(root->right);
}
void BuildInOrderThread(TBTNode* root) {
__BuildInOrderThread(root);
PreNode->right = NULL; // 弥补缺陷————让最后的节点的右指针变为线索
PreNode->rtag = 1;
return;
}
TBTNode* GetNext(TBTNode* root){
if(root->rtag == 1)
return root->right;
root = root->right;
while(root->ltag == 0 && root->left){
root = root->left;
}
return root;
}
void TreeDestroy(TBTNode** proot) {
if (*proot == NULL) {
return;
}
// 只沿着实际的边进行销毁
if((*proot)->ltag == 0) TreeDestroy(&((*proot)->left)); // 注意取地址之后才是二级指针
if((*proot)->rtag == 0) TreeDestroy(&((*proot)->right));
free(*proot);
*proot = NULL;
}
// ThreadBinaryTreeTest.c
#include "ThreadBinaryTree.h"
void test1(){
TBTNode* A = GetNewNode('A');
TBTNode* B = GetNewNode('B');
TBTNode* C = GetNewNode('C');
TBTNode* D = GetNewNode('D');
TBTNode* E = GetNewNode('E');
TBTNode* F = GetNewNode('F');
A->left = B;
A->right = C;
B->left = D;
B->right = E;
C->left = F;
InOrder(A); // D B E A F C
printf("\n");
BuildInOrderThread(A);
TBTNode* Node = InOrderRoot;
while(Node){
printf("%c ", Node->data);
Node = GetNext(Node);
} // D B E A F C
TreeDestroy(&A);
}
int main() {
test1();
return 0;
}
补充一下之前的疑问,如果在头文件中直接定义了全局变量:
BTNode* PreNode = NULL; BTNode* InOrderRoot = NULL;
这样做会导致每个包含此头文件的源文件( ThreadBinaryTree.c 和 ThreadBinaryTreeTest.c)都获得一份独立的定义。当编译多个源文件后链接时,链接器会发现有多个
PreNode
和InOrderRoot
的定义,从而报 “multiple definition”(重复定义)错误。
线索二叉树 ——【完】