数据结构:二叉树的线索化

引言

本文将用c++的代码演示二叉树的线索化全过程。我们在创建一棵非常普通的二叉树之后,会发现一个很大的缺点,就是我们的结点是有左右孩子的,但是对于叶子结点来说,它的左孩子或者右孩子是空的,没有存任何的数据,但是我们也知道,指针的大小是4字节,也就是说如果这棵树很大,那么就会存在很多很多的空指针占用了内存空间,很浪费。

所以我们必须要利用着一些指针,让这一些指针有作用。而线索二叉树的本质就是如果左孩子是空,那么就让左指针指向本节点的前驱节点,同理,右节点指向本节点的后继节点。

简单计算

假设我们有n个结点,我们可以计算一下有多少个空指针。如果一个指针有孩子,那么反过来可以说这个结点有父节点,所以有多少个结点有父节点呢?

很明显,除了根节点以外所有节点都有父节点,所以数量是n-1,那么总指针2n,所以空指针有2n-(n-1)

结构

既然有的结点是指向了前驱,有的结点是指向了孩子,那我们怎么知道这个结点的指针到底是指向的什么呢?所以我们需要一个标志来记录一下,也就是flag

cpp 复制代码
struct BTNode{
    char data;
    BTNode* left;
    BTNode* right;
    int lflag, rflag; // flag = 0指针域指向孩子,flag = 1指针域指向线索节点
};

然后我们既然要连接前驱结点和后继结点,那么我们很显然是需要双指针进行操作的,一个指向这个结点的前面,一个指向当前的指针。

然后还有一个很重要的点,就是遍历的顺序。我们二叉树遍历顺序是十分重要的,一个好的遍历顺序可以大大的减少我们头发掉落的可能性。

我们这里选择是中序遍历,至于为什么,我们会放在代码的最后讲解

代码

OK,既然思路有了,那么我们就开始工作!!!

首先初始化一个结点

cpp 复制代码
BTNode* InitBTree(char value){
    BTNode* s = new BTNode();
    s->data = value;
    s->lflag = s->rflag = 0;
    s->left = s->right = nullptr;
    return s;
}

然后保证我们之后可以找到我们想要的数据

注意一下:这里的查找都是要保证指针是指向的孩子,而不是前驱结点或者后继结点。

我们查找数据的方式是左边查找完,查找右边的,当然,我们把这个函数的出口,也就是处理这个结点的过程,放在了最开始

cpp 复制代码
BTNode* Find(BTNode* root, char fx){ // 查找叶子节点fx
    if(root->data == fx){  // 每一次处理结点都是在开始,这个也就是递归的出口
        return root;
    }
    BTNode* ans = nullptr;
    if(root->left != nullptr && root->lflag == 0){ // 先左边找
        ans = Find(root->left, fx);
        if(ans != nullptr){
            return ans;
        }
    }
    if(root->right != nullptr && root->rflag == 0){ // 再右边找
        ans = Find(root->right, fx);
        if(ans != nullptr){
            return ans;
        }
    }
    return nullptr;
}

然后就是构建这一棵树的过程。不过为了知道我们到底是插在了左孩子还是右孩子,我们选择在传入参数的时候,传入一个flag,来记录操作者想要插入的位置

cpp 复制代码
BTNode* Insert(BTNode* root, char x, char fx, int flag){ // 我们这里的插入都是在叶子结点进行的插入
    BTNode* f = Find(root, fx); 
    BTNode* s = new BTNode();
    s->data = x;
    s->lflag = s->rflag = 0;
    s->left = s->right = nullptr;
    if(flag == 0){
        f->left = s;
    } else {
        f->right = s;
    }
    return root;
}

然后就是中序遍历的整个基本框架,先遍历左边,然后处理结点,再遍历右边

cpp 复制代码
void InOrderBTree(BTNode* root){
    if(root == nullptr){
        return;
    }
    InOrderBTree(root->left);

    Visit(root);

    InOrderBTree(root->right);
    
}

处理这个结点的操作如下:我们不仅仅要连接两个结点,还要改变指针的意义。

不过要注意的是,前驱结点的第一个是nullptr,所以如果对于一个空指针进行操作,就会报错,所以我们在处理前驱结点的时候需要时刻小心

cpp 复制代码
void Visit(BTNode* p){
    // p和pre是一对前驱和后继的关系
    if(p->left == nullptr){ // 给p添加前驱线索
        p->lflag = 1;
        p->left = pre;
    }
    if(pre != nullptr && pre->right == nullptr){ // 给pre添加后继线索(要保证pre不是空的,因为一开始是根节点,肯定是空的)
        pre->rflag = 1;
        pre->right = p;
    }
    pre = p;
}

然后我们有了整个线索二叉树之后,我们来处理一下找前驱和找后继的操作:

找前驱:

如果一个结点的左孩子是空,那么说明这是第一个结点,因为除了第一个结点,其他结点的左指针要么是左孩子,要么是前驱节点。

如果不为空,并且还正好标记是1,直接拿数据就可以了

但是如果是左孩子,那么我们就需要利用中序遍历的特点了。中序遍历的特点是左中右,如果右结束了,那么这一棵左子树就结束了,这颗左子树结束就到了中间结点,也就是我们的目标节点。 就是如果一个结点有左子树,那么这颗左子树的最右边那一个结点就是它的前驱结点。

cpp 复制代码
    // 找前驱
    BTNode* p = Find(root, x);
    if(p->left == nullptr){
        std::cout << "no exist" << std::endl;
    } else {
        if(p->lflag == 1){
            std::cout << p->left->data << std::endl;
        } else {
            // p->left指向的是左孩子,不是前驱,并且p->left不是空,此时前驱一定是在p的左子树中,而且一定是左子树中最靠右的结点
            BTNode* q = p->left;
            // q 只要是右孩子存在,就一直往右走,直到右孩子不存在
            while(q->right != nullptr && q->rflag == 0){
                q = q->right;
            }
            std::cout << q->data << std::endl; 
        }
    }

同理,对于找后继结点,我们找右子树的最左边这个结点。不过要注意的是,后继节点有可能是空结点,所以我们还是要多多小心

cpp 复制代码
    // 找后继
    if(p->right == nullptr){
        std::cout << "no exist" << std::endl;
    } else {
        if(p->rflag == 1 && p->right != nullptr){
            std::cout << p->right->data << std::endl;
        } else {
            // 此时后继一定在右子树中,且一定是第一个遍历的结点,所以一定是右子树中的最左边的结点
            BTNode* q = p->right;
            // q 只要是左孩子存在,就一直往左走,直到左孩子不存在
            while(q->left != nullptr && q->lflag == 0){
                q = q->left;
            }
            std::cout << q->data << std::endl; 
        }
    }

总结

大家其实在整个代码的过程中,也慢慢发现了中序遍历的优势,因为中序遍历把父节点插在了中间,导致我们找前驱节点和找后继节点更加容易。所以这也就是我们为什么选择中序遍历的原因

本篇文章到这里就结束了,希望可以帮助到大家理解线索二叉树!!!!

相关推荐
2401_872418781 小时前
算法入门:并查集(Disjoint Set / Union-Find):连通性问题的利器
算法
luj_17682 小时前
R语言生态优势与学习曲线分析
c语言·开发语言·网络·经验分享·算法
计算机安禾2 小时前
【算法分析与设计】第36篇:计算几何基础:凸包问题的分治与扫描线解法
大数据·人工智能·算法·机器学习·剪枝
货拉拉技术2 小时前
飞速发展的计算机视觉
人工智能·算法
如竟没有火炬2 小时前
寻找峰值——二分
java·开发语言·数据结构·python·算法·散列表
noipp2 小时前
推荐题目:洛谷 P1115 最大子段和
算法
Lumbrologist2 小时前
【C++】零基础入门 · 第 17 节:多线程编程基础
java·c++·算法
轻闲一号机3 小时前
【语音】笔记
前端·笔记·算法
aWty_3 小时前
实分析入门(12)--可测函数
学习·数学·算法·实变函数