线索二叉树的详细实现(C++)

线索二叉树概述

二叉树虽然是非线性结构,但二叉树的遍历却为二叉树的结点集导出了一个线性序列。如果我们希望很快找到某一结点的前驱或后继,但不希望每次都要对二叉树遍历一遍,这就需要把每个结点的前驱和后继信息记录下来。

为了做到这一点,可在原来的二叉链表中增加一个前驱指针域(pred)和一个后继指针域(succ),分别指向该结点在某种次序下的前驱结点和后继结点,但是这种方法将会导致很多结点中有没有利用的空指针,先看下面的例子。

比如:现在有一个结点数为 n 的二叉树,采用二叉链表形式存储。对于每个结点均有指向左右孩子的指针域,可以得出:结点数为 n 的二叉树共有 n - 1 条有效分支路径。而且二叉树中存在 2n - (n - 1) = n + 1 个空指针域。

线索二叉树的详细实现-图1

此二叉树有 6 个结点,存在 7 个空指针域。该二叉树的中序遍历为:CBEDFA,从遍历的结果输出中,可以轻松直到 B 结点的前驱结点为 C,后继结点为 E。但是这种关系是在建立好二叉树的基础上进行遍历得到的,是否可以在建立二叉树的时候,就记录下前驱后继的关系呢,如果可以,在后续寻找任意结点去寻找前驱结点和后继结点时将大大提升效率。

线索化

上面说过为每个结点增加一个前驱指针域和一个后继指针域,其实大可不必。完全可以将利用这些空的指针域,将其指向前驱或后继,定义规则如下:

如果结点左子树为空,则该结点的左孩子指向其前驱结点。

如果结点右子树为空,则该结点的右孩子指向其后继结点。

这种指向前驱和后继的指针称为线索,将一刻二叉树以某种次序遍历,并添加线索的过程称为线索化。

线索化即解决了空间的浪费和前驱后继的记录问题。

那么问题来了,我们如何区分一个结点的 lchild 指针是指向左孩子还是指向前驱结点呢?

为了解决这个问题,区别线索和子女指针,在每个结点中设置两个标志 ltag 和 rtag。以中序线索二叉树为例,如果 ltag == 0,标明 leftChild 域中存放的是指向左子女结点的指针,否则 leftChild 域中是指向该结点中序下的前驱的线索;如果 rtag == 0,标明 rightChild 域中存放的是指向右子女结点的指针,否则 rightChild 域中是指向该结点中序下的后继的线索。由于标记位只需占用一个二进位,每个结点所需存储空间节省得多。

那么将得出如下结论:

寻找当前结点在中序下的后继

当 rightchild == NULL,如果 rtag == 1,表示无后继

当 rightchild != NULL,如果 rtag == 0,后继为其右子树的中序遍历的第一个结点;如果 rtag==1 后继为右子女结点。

寻找当前结点在中序下的前驱

当 leftchild == NULL,如果 ltag == 1,表示无前驱

当 leftchild != NULL,如果 ltag == 0,前驱为其左子树中序遍历的最后一个结点;如果 ltag==1 前驱为左子女结点。

添加了 ltag 和 rtag 标志后的结点结构如下图所示:
线索二叉树的详细实现-图2
之前的二叉树线索化后将如下图:
线索二叉树的详细实现-图3

线索二叉树的实现

线索二叉树的节点定义

//线索二叉树结点类型
template <typename T>
struct ThreadNode
{
    T data;                                                                                       //结点数据
    ThreadNode<T> *leftChild, *rightChild;                                                        //左孩子和右孩子
    int ltag, rtag;                                                                               //左右子树标志位
    ThreadNode(const T item) : data(item), leftChild(NULL), rightChild(NULL), ltag(0), rtag(0) {} //结点类的构造函数
};

中序线索二叉树的创建

对于一个已存在的二叉树按中序遍历进行线索化的算法中用到了一个指针 pre,它在遍历过程中总是指向遍历指针 p 的中序下的前驱结点,即在中序遍历过程中刚刚访问过的结点。在做中序遍历时,只要一遇到空指针域,立即填入前驱或后继线索。

//使用前序遍历创建二叉树(未线索化)
void CreateTree(ThreadNode<T> *&subTree)
{
    T item;
    if (cin >> item)
    {
        if (item != RefValue)
        {
            subTree = new ThreadNode<T>(item); //构造结点
            if (subTree == NULL)
            {
                cout << "空间分配错误!" << endl;
                exit(1);
            }
            CreateTree(subTree->leftChild);  //递归创建左子树
            CreateTree(subTree->rightChild); //递归创建右子树
        }
        else
        {
            subTree = NULL;
        }
    }
}

//中序遍历对二叉树进行线索化
void createInThread(ThreadNode<T> *current, ThreadNode<T> *&pre)
{
    if (current == NULL)
    {
        return;
    }
    createInThread(current->leftChild, pre); //递归左子树的线索化
    if (current->leftChild == NULL)          //建立当前结点的前驱结点
    {
        current->leftChild = pre;
        current->ltag = 1;
    }
    if (pre != NULL && pre->rightChild == NULL) //建立当前结点的后继结点
    {
        pre->rightChild = current;
        pre->rtag = 1;
    }
    pre = current;                            //用前驱记住当前的结点
    createInThread(current->rightChild, pre); //递归右子树的线索化
}

//中序遍历对创建好的普通二叉树进行中序线索化
void CreateInThread()
{
    ThreadNode<T> *pre = NULL; //第一个结点的左子树置为NULL
    if (root != NULL)
    {
        createInThread(root, pre);
        //处理中序遍历的最后一个结点,最后一个结点的右子树置为空
        pre->rightChild = NULL;
        pre->rtag = 1;
    }
}

中序线索化二叉树的成员函数

//寻找中序下第一个结点
ThreadNode<T> *First(ThreadNode<T> *current) //返回以*current为根的中序线索二叉树中序遍历的第一个结点
{
    ThreadNode<T> *p = current;
    while (p->ltag == 0)
    {
        p = p->leftChild; //循环找到最左下角结点
    }
    return p;
}

//寻找中序下的后继结点
ThreadNode<T> *Next(ThreadNode<T> *current)
{
    ThreadNode<T> *p = current->rightChild;
    if (current->rtag == 0)
    {
        return First(p);
    }
    else
    {
        return p;
    }
}

//寻找中序下最后一个结点
ThreadNode<T> *Last(ThreadNode<T> *current) //返回以*current为根的中序线索二叉树中序遍历的最后一个结点
{
    ThreadNode<T> *p = current;
    while (p->rtag == 0)
    {
        p = p->rightChild;
    }
    return p;
}

//寻找结点在中序下的前驱结点
ThreadNode<T> *Prior(ThreadNode<T> *current)
{
    ThreadNode<T> *p = current->leftChild;
    if (current->ltag == 0)
    {
        return Last(p);
    }
    else
    {
        return p;
    }
}

中序线索化二叉树的中序遍历的算法

先利用上面的 First() 成员函数找到二叉树在中序序列下的第一个结占,把它作为当前结点,然后利用求后继结点的运算 Next() 成员函数按中序次序逐个访问,直到二叉树的最后一个结点。

//中序线索化二叉树的中序遍历的算法
void InOrder(ThreadNode<T> *p)
{
    for (p = First(root); p != NULL; p = Next(p))
    {
        cout << p->data << " ";
    }
    cout << endl;
}

中序线索化二叉树的前序遍历算法

前序序列中的第一个结点即二叉树的根,因此从根结点开始前序遍历二叉树。

若当前结点有左子女,则前序下的后继结点即为左子女结点,否则,若当前结点有右子女,则前序后继即为右子女结点。

对于叶结点,则沿着中序后继线索走到一个有右子女结点的结点,这个右子女结点就是当前结点的前序后继结点。

//中序线索化二叉树的前序遍历的算法
void PreOrder(ThreadNode<T> *p)
{
    while (p != NULL)
    {
        cout << p->data << " "; //先访问根节点
        if (p->ltag == 0)
        {
            p = p->leftChild; //有左子树,即为后继
        }
        else if (p->rtag == 0) //否则,有右子树,即为后继
        {
            p = p->rightChild;
        }
        else //无左右子树
        {
            while (p != NULL && p->rtag == 1) //检测后继线索
            {
                p = p->rightChild; //直到找到有右子树的结点
            }
            if (p != NULL)
            {
                p = p->rightChild; //该结点的右子树为后继
            }
        }
    }
    cout << endl;
}

中序线索化二叉树的后序遍历算法

首先从根结点出发,寻找在后序序列中的第一个结点。寻找的方法是从根出发,沿着左子女链一直找下去,找到左子女不再是左子女指针的结点,再找到该结点的右子女,在以此结点为根的子树上再重复上述过程,直到叶结点为止。

接着,从此结点开始后序遍历中序线索二又树。在遍历过程中,每次都先找到当前结点的父结点,如果当前结点是父结点的右子女,或者虽然当前结点是父结点的左子女,但这个父结点没有右子女,则后序下的后继即为该父结点;否则,在当前结点的右子树(如果存在)上重复执行上面的操作。这种后序遍历过程必须搜寻父结点,并确定当前结点与其父结点的关系,即是左子女还是右子女。

//中序线索二叉树的后序遍历算法
void PostOrder(ThreadNode<T> *p)
{
    ThreadNode<T> *t = p;
    while (t->ltag == 0 || t->rtag == 0) //寻找后续第一个结点
    {
        if (t->ltag == 0)
        {
            t = t->leftChild;
        }
        else if (t->rtag == 0)
        {
            t = t->rightChild;
        }
    }
    cout << t->data << " ";         //访问第一个结点
    while ((p = Parent(t)) != NULL) //每次都先找到当前结点的父结点
    {
        //若当前结点是父节点的右子树或者当前结点是左子树,但是这个父节点没有右子树,则后续下的后继为改父节点
        if (p->rightChild == t || p->rtag == 1)
        {
            t = p;
        }
        //否则,在当前结点的右子树(如果存在)上重复执行上面的操作
        else
        {
            t = p->rightChild;
            while (t->ltag == 0 || t->rtag == 0)
            {
                if (t->ltag == 0)
                {
                    t = t->leftChild;
                }
                else if (t->rtag == 0)
                {
                    t = t->rightChild;
                }
            }
        }
        cout << t->data << " ";
    }
}

中序线索二叉树中求父节点

中序线索化二叉树后序遍历的算法中用到了求父节点的算法,程序中包括两条查找父结点的路径。第一种选择是从当前结点走到树上层的一个中序前驱(不一定是直接前驱),然后向右下找父结点。第二种选择是从当前结点走到树上层的一个中序后继(不一定是直接后继),然后向左下找父结点。以下通过一个具体的例子来说明为什么不可以只采用一种方法。

线索二叉树的详细实现-图4

例如上图寻找结点 * 的父结点的两条路径。

一条路径是从结点 * 沿左子女链走到 b,然后顺中序前驱线索走到 + ,而 + 就是 * 的父结点。

另一条路径是从结点 * 沿右子女链走到 d,然后顺中序后继线索走到 -,再向左子女方向走到结点 +,找到结点 * 的父结点。

对于此例,无论第一条路径还是第二条路径都能找到父结点。但情况不总是这样。例如:在找结点 + 的父结点,从 + 沿左子女链将走到结点 a,而 a 无中序前驱线索,因此这条路径失败,但通过另一条路径找到了结点 + 的父结点 -。说明了只采用一种方法是不行的。

程序实现是先试探第一条路径,如果走到中序序列的第一个结点而告失败,则改换后一条路径寻找父结点。只有找根结点的父结点,这两种方法才都会失败。因为从根结点沿左子女链一定走到中序序列的第一个结点,沿右子女链一定走到中序序列的最后一个结点。然而,根结点根本就无父结点,所以这种特例在开始就排除了。

//在中序线索化二叉树中求父节点
ThreadNode<T> *Parent(ThreadNode<T> *t)
{
    ThreadNode<T> *p;
    if (t == root) //根节点无父节点
    {
        return NULL;
    }
    for (p = t; p->ltag == 0; p = p->leftChild)
        ; //求*t为根的中序下的第一个结点p
    //情况1
    if (p->leftChild != NULL) //当p左子树指向不为空
    {
        //令p为p的左子树指向的结点,判断此结点是否并且此节点的左右子树结点的指向都不为t,再将p为p的右孩子结点
        for (p = p->leftChild; p != NULL && p->leftChild != t && p->rightChild != t; p = p->rightChild)
            ;
    }
    //情况2
    //如果上面的循环完了,由于是p==NULL结束的循环,没有找到与t相等的结点,就是一直找到了中序线索化的第一个结点了,这时候这种就要用到情况2的方法
    if (p == NULL || p->leftChild == NULL)
    {
        //找到*t为根的中序下的最后一个结点
        for (p = t; p->rtag == 0; p = p->rightChild)
            ;
        //让后让他指向最后一个结点指向的结点,从这个结点开始,以此判断它的左孩子孩子和右孩子是否和t相等
        for (p = p->rightChild; p != NULL && p->leftChild != t && p->rightChild != t; p = p->leftChild)
            ;
    }
    return p;
}

详细代码实现

#include <iostream>
using namespace std;

//线索二叉树结点类型
template <typename T>
struct ThreadNode
{
    T data;                                                                                       //结点数据
    ThreadNode<T> *leftChild, *rightChild;                                                        //左孩子和右孩子
    int ltag, rtag;                                                                               //左右子树标志位
    ThreadNode(const T item) : data(item), leftChild(NULL), rightChild(NULL), ltag(0), rtag(0) {} //结点类的构造函数
};

//线索二叉树类
template <typename T>
class ThreadTree
{

public:
    //构造函数(普通)
    ThreadTree() : root(NULL) {}

    //指定结束标志RefValue的构造函数
    ThreadTree(T value) : root(NULL), RefValue(value) {}

    //使用前序遍历创建二叉树(未线索化)
    void CreateTree() { CreateTree(root); }

    //中序遍历对创建好的普通二叉树进行中序线索化
    void CreateInThread()
    {
        ThreadNode<T> *pre = NULL; //第一个结点的左子树置为NULL
        if (root != NULL)
        {
            createInThread(root, pre);
            //处理中序遍历的最后一个结点,最后一个结点的右子树置为空
            pre->rightChild = NULL;
            pre->rtag = 1;
        }
    }
    //中序线索化二叉树的中序遍历的算法
    void InOrder() { InOrder(root); }
    //中序线索化二叉树的前序遍历的算法
    void PreOrder() { PreOrder(root); }
    //中序线索二叉树的后序遍历算法
    void PostOrder() { PostOrder(root); }

private:
    //使用前序遍历创建二叉树(未线索化)
    void CreateTree(ThreadNode<T> *&subTree)
    {
        T item;
        if (cin >> item)
        {
            if (item != RefValue)
            {
                subTree = new ThreadNode<T>(item); //构造结点
                if (subTree == NULL)
                {
                    cout << "空间分配错误!" << endl;
                    exit(1);
                }
                CreateTree(subTree->leftChild);  //递归创建左子树
                CreateTree(subTree->rightChild); //递归创建右子树
            }
            else
            {
                subTree = NULL;
            }
        }
    }
    //中序遍历对二叉树进行线索化
    void createInThread(ThreadNode<T> *current, ThreadNode<T> *&pre)
    {
        if (current == NULL)
        {
            return;
        }
        createInThread(current->leftChild, pre); //递归左子树的线索化
        if (current->leftChild == NULL)          //建立当前结点的前驱结点
        {
            current->leftChild = pre;
            current->ltag = 1;
        }
        if (pre != NULL && pre->rightChild == NULL) //建立当前结点的后继结点
        {
            pre->rightChild = current;
            pre->rtag = 1;
        }
        pre = current;                            //用前驱记住当前的结点
        createInThread(current->rightChild, pre); //递归右子树的线索化
    }

    //寻找中序下第一个结点
    ThreadNode<T> *First(ThreadNode<T> *current) //返回以*current为根的中序线索二叉树中序遍历的第一个结点
    {
        ThreadNode<T> *p = current;
        while (p->ltag == 0)
        {
            p = p->leftChild; //循环找到最左下角结点
        }
        return p;
    }

    //寻找中序下的后继结点
    ThreadNode<T> *Next(ThreadNode<T> *current)
    {
        ThreadNode<T> *p = current->rightChild;
        if (current->rtag == 0)
        {
            return First(p);
        }
        else
        {
            return p;
        }
    }

    //寻找中序下最后一个结点
    ThreadNode<T> *Last(ThreadNode<T> *current) //返回以*current为根的中序线索二叉树中序遍历的最后一个结点
    {
        ThreadNode<T> *p = current;
        while (p->rtag == 0)
        {
            p = p->rightChild;
        }
        return p;
    }
    //寻找结点在中序下的前驱结点
    ThreadNode<T> *Prior(ThreadNode<T> *current)
    {
        ThreadNode<T> *p = current->leftChild;
        if (current->ltag == 0)
        {
            return Last(p);
        }
        else
        {
            return p;
        }
    }
    //在中序线索化二叉树中求父节点
    ThreadNode<T> *Parent(ThreadNode<T> *t)
    {
        ThreadNode<T> *p;
        if (t == root) //根节点无父节点
        {
            return NULL;
        }
        for (p = t; p->ltag == 0; p = p->leftChild)
            ; //求*t为根的中序下的第一个结点p
        //情况1
        if (p->leftChild != NULL) //当p左子树指向不为空
        {
            //令p为p的左子树指向的结点,判断此结点是否并且此节点的左右子树结点的指向都不为t,再将p为p的右孩子结点
            for (p = p->leftChild; p != NULL && p->leftChild != t && p->rightChild != t; p = p->rightChild)
                ;
        }
        //情况2
        //如果上面的循环完了,由于是p==NULL结束的循环,没有找到与t相等的结点,就是一直找到了中序线索化的第一个结点了,这时候这种就要用到情况2的方法
        if (p == NULL || p->leftChild == NULL)
        {
            //找到*t为根的中序下的最后一个结点
            for (p = t; p->rtag == 0; p = p->rightChild)
                ;
            //让后让他指向最后一个结点指向的结点,从这个结点开始,以此判断它的左孩子孩子和右孩子是否和t相等
            for (p = p->rightChild; p != NULL && p->leftChild != t && p->rightChild != t; p = p->leftChild)
                ;
        }
        return p;
    }

    //中序线索化二叉树的中序遍历的算法
    void InOrder(ThreadNode<T> *p)
    {
        for (p = First(root); p != NULL; p = Next(p))
        {
            cout << p->data << " ";
        }
        cout << endl;
    }
    //中序线索化二叉树的前序遍历的算法
    void PreOrder(ThreadNode<T> *p)
    {
        while (p != NULL)
        {
            cout << p->data << " "; //先访问根节点
            if (p->ltag == 0)
            {
                p = p->leftChild; //有左子树,即为后继
            }
            else if (p->rtag == 0) //否则,有右子树,即为后继
            {
                p = p->rightChild;
            }
            else //无左右子树
            {
                while (p != NULL && p->rtag == 1) //检测后继线索
                {
                    p = p->rightChild; //直到找到有右子树的结点
                }
                if (p != NULL)
                {
                    p = p->rightChild; //该结点的右子树为后继
                }
            }
        }
        cout << endl;
    }
    //中序线索二叉树的后序遍历算法
    void PostOrder(ThreadNode<T> *p)
    {
        ThreadNode<T> *t = p;
        while (t->ltag == 0 || t->rtag == 0) //寻找后续第一个结点
        {
            if (t->ltag == 0)
            {
                t = t->leftChild;
            }
            else if (t->rtag == 0)
            {
                t = t->rightChild;
            }
        }
        cout << t->data << " ";         //访问第一个结点
        while ((p = Parent(t)) != NULL) //每次都先找到当前结点的父结点
        {
            //若当前结点是父节点的右子树或者当前结点是左子树,但是这个父节点没有右子树,则后续下的后继为改父节点
            if (p->rightChild == t || p->rtag == 1)
            {
                t = p;
            }
            //否则,在当前结点的右子树(如果存在)上重复执行上面的操作
            else
            {
                t = p->rightChild;
                while (t->ltag == 0 || t->rtag == 0)
                {
                    if (t->ltag == 0)
                    {
                        t = t->leftChild;
                    }
                    else if (t->rtag == 0)
                    {
                        t = t->rightChild;
                    }
                }
            }
            cout << t->data << " ";
        }
    }

private:
    //树的根节点
    ThreadNode<T> *root;
    T RefValue;
};

int main(int argc, char *argv[])
{
    /*二叉树如下:
               A                     
             /   \
            B
           / \                  
          C   D
            /   \               
           E     F
            \
             g
    */
    //ABC##DE#G##F### (前序)
    ThreadTree<char> tree('#');
    tree.CreateTree();
    tree.CreateInThread();
    tree.InOrder();   //print: C B E G D F A
    tree.PreOrder();  //print: A B C D E G F
    tree.PostOrder(); //print: C G E F D B A
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code_peak

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值