数据结构(15.3)线索二叉树

本文介绍了线索二叉树的概念,它利用空指针存储结点的前驱和后继,解决二叉树遍历中查找前驱和后继的问题。文章详细讲解了中序线索二叉树的存储结构、初始化、创建、中序线索化过程以及遍历和查找操作,还提供了C++代码实现。
摘要由CSDN通过智能技术生成

前言

树形结构是一种非线性结构,我们对二叉树进行遍历,是将二叉树的结点按照一定的规则排列成一个线性序列,这实际上是对一个非线性化结构进行了线性化操作。前面提到过,线性结构除了第一个结点和最后一个结点以外,每一个结点都有且只有一个前驱和后继。观察我们的二叉树可以发现,我们没有是办法直接在树中找到每个结点的前驱和后继的,这个信息只能在二叉树的遍历中得到。

img_1
与此同时,二叉树中存在很多空的链域,造成了空间上的浪费。(有n个结点,就会有n+1个空链域)

为了解决这些问题,有人想到可以使用空的链域来存放结点的前驱和后继,于是线索二叉树就出现了。

img_2

现在,二叉树的子结点指向有两种可能性:既可能指向其左子树或者右子树,也可能指向其前驱或者后继。当结点指向的是前驱或者后继时,称结点为线索。拥有线索的二叉树就是线索二叉树。把空指针转化为线索的过程,就叫做线索化。

注意:将二叉树按照不同的规则来排列可以得到不同的序列,结点的前驱和后继也会不同,这样线索的指向也不同了。也就是说,对同一个二叉树,按照不同的次序进行线索化,可以分别得到先序线索二叉树、中序线索二叉树和后序线索二叉树。本文讲的是中序线索的二叉树。

线索二叉树的存储结构

我们知道,二叉树的结点有三个部分,分别是数据域、左孩子和右孩子。当使用空指针来存放线索时,我们不知道左右孩子指针指向的是子树还是线索。为了解决这个问题,线索化二叉树需要增加两个标志域,通过标志来协助判断。

//标记
typedef enum {
   
  	//子树
    LINK,
   //线索
    THREAD
}TagType;

//线索二叉树的结点
typedef struct BinTreeNode{
   
    //数据域
    ElemType data;
    //左孩子/线索
    struct BinTreeNode *leftChild;
    //右孩子/线索
    struct BinTreeNode *rightChild;
    //左标记
    TagType leftTag;
    //右标记
    TagType rightTag;
}BinTreeNode;

线索二叉树的初始化与创建

线索化二叉树就是对二叉树里的空指针进行修改,使其指向结点前驱或者后继,而前驱和后继显然无法在创建时就获得。

这意味着要得到线索二叉树,首先要存在一个二叉树。因此在初始化和创建上,线索二叉树与二叉树是相同的。

创建是指通过输入一串字符串来生成二叉树,要有特殊的字符来表示结点不存在。因此初始化时要设置标记的值。

//初始化
void InitBinTree(BinTree *bt, ElemType ref){
   
    //初始化树根为空
    bt->root = NULL;
    //设置结束标记
    bt->refvalue = ref;
}

创建:

为了方便,先写一个生成结点的方法:

//生成一个结点
BinTreeNode *GetNewNode(ElemType x){
   
    BinTreeNode *s = (BinTreeNode *)malloc(sizeof(BinTreeNode));
    assert(s != NULL);
    
    s->data = x;
    s->leftChild = s->rightChild = NULL;
    s->leftTag = s->rightTag = LINK;
    
    return s;
}

然后实现创建方法:


//创建
void CreateBinTree(BinTree *bt, char *str){
   
    CreateBinTree(bt, bt->root, str);
}
void CreateBinTree(BinTree *bt, BinTreeNode *&t, char *&str){
   
    if (*str == bt->refvalue) {
   
        t = NULL;
    }else{
   
        t = GetNewNode(*str);
        CreateBinTree(bt, t->leftChild, ++str);
        CreateBinTree(bt, t->rightChild, ++str);
    }
}

注:这次的代码是用c++来写的,有些地方使用了引用,和直接用指针区别不大:

//创建
void CreateBinTree(BinTree *bt, char *str){
   
    CreateBinTree1(bt, &(bt->root), str);
}
void CreateBinTree1(BinTree *bt, BinTreeNode **t,char *&str){
   
    if (*str == bt->refvalue) {
   
        t = NULL;
    }else{
   
        *t = GetNewNode(*str);
        CreateBinTree1(bt, &(*t)->leftChild, ++str);
        CreateBinTree1(bt, &(*t)->rightChild, ++str);
    }
}

注注:字符串如果不使用引用,而是用char **str的话,我使用的编译器会出现一些问题,因此这里字符串还是使用的引用。

二叉树的中序线索化

来比较一下中序遍历方法和线索化方法:

中序遍历:

//中序遍历-递归
void InOrder(BinTreeNode *t){
   
    if (t != NULL) {
   
        //左子树递归遍历
        InOrder(t->leftChild);
      
        //输出结点信息
        printf("%4c",t->data);
      
        //右子树递归遍历
        InOrder(t->rightChild);
    }
}

线索化:

void CreateInThread(BinTreeNode *&t, BinTreeNode *&pre){
   
    if (t != NULL) {
   
        //左子树递归线索化
        CreateInThread(t->leftChild, pre);
        
        //将空指针变为线索
        if (t ->leftChild == NULL) {
   
            //左子树为空->左子树指向前驱结点
            t->leftTag = THREAD;
            t->leftChild = pre;
        }
        if (pre != NULL && pre->rightChild == NULL) {
   
            //前驱结点的右子树为空->前驱结点的右子树指向该结点(后继)
            pre->rightTag = THREAD;
            pre->rightChild = t;
        }
        //更新前驱结点
        pre = t;
        
        //右子树递归线索化
        CreateInThread(t->rightChild, pre);
    }
}

可以发现,线索化方法和中序遍历方法在结构上是一致的,只不过是中间的操作有所区别。实际上,因为结点的前驱或后继信息只能在遍历中得到,中序线索化的实质就是在中序遍历的过程中,对空指针进行修改。

当我们找到一个子结点为空的结点时,假如是左结点,那么就要修改它的标记值,并且让它指向本结点的前驱。这说明了我们需要一个额外的变量来保存前驱结点。

假如是右结点,我们此时是不知道本结点的后继结点的。

但是注意:对本结点的前驱结点来说,本结点就是其后继结点。也就是说我们这时候能得到前驱结点的后继信息,因此可以判断前驱结点的右结点是否为空,假如为空,则修改它的标记值,然后让它指向本结点。

这时候本结点访问完毕了,需要更新一下前驱结点的指向(本结点成为了下一个结点的前驱结点)。这同样也说明,对结点的右结点进行修改,是要放到下一个结点中处理的。

img_3
img_4

有人可能注意到,本结点的右结点放到下一个结点来操作,那么到最后一个结点的右结点是不会被操作到的

img_5

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值