线索二叉树的实现(c语言)

一、前言:什么是二叉树的线索化?(为什么要有二叉树的线索化?)

  • 通过前面内容的学习,我们知道了二叉树 的存储结构其实是通过二叉链表的方式实现的。但二叉链表由于每个结点均有左右孩子域,这使得叶子结点以及部分非叶子结点出现了孩子指针域为空的情况。

如图例:

  • 可以发现的是,随着二叉树结点个数 的增长,二叉树中空指针域的个数越来越多,这会造成不小的存储空间浪费。
  • 假设,现在需要你求解一个节点的前驱节点/后续节点,你要如何进行呢?
  • 通过遍历方法我们确实可以知道二叉树中某个结点的前驱和后继分别是谁,但是这是建立在二叉树遍历的基础上的。一旦二叉树遍历结束后,如果想要知道二叉树中某个结点的前驱或后继时,得从头结点开始遍历二叉链表,这将会造成不小的时间开销。
  • 如图中的二叉链表如果我想要找到 的前驱是谁,得遍历一遍二叉树才能搞定
  • 既然有那么多空指针域,为什么不能把这些空指针域利用起来,让每个空指针域用来记录当前结点的前驱 呢?这样一来,是不是就解决了空指针域造成的存储空间浪费以及查找前驱后继时时造成的时间浪费呢?
    因此,二叉线索树诞生了

二、二叉线索树的概念

2.1:名词解释

  • 让二叉树中的空指针域记录二叉树某种遍历次序下的前驱和后继结点的地址,这样的话,再想找二叉树中某个结点的前驱后继时间性能将大为提升。
  • 这种将二叉链表中指向前驱后继的指针称为线索 ,加上线索的二叉链表称之为线索链表 ,线索链表所对应的二叉树称之为线索二叉树(Threaded Binary Tree)

三、二叉线索树的实现

3.1:二叉线索树的基本数据结构

c 复制代码
typedef struct ThreadedTreeNode { // 线索化二叉树节点
    eleType val;
    struct ThreadedTreeNode* left;  // 左指针
    struct ThreadedTreeNode* right; // 右指针
    int leftTag;  // 左标志位:0 表示指向左子树,1 表示指向前驱
    int rightTag; // 右标志位:0 表示指向右子树,1 表示指向后继
} ThreadedTreeNode;
//leftTag和rightTag默认为0
  • eleType val:存储节点的值,根据宏定义,这里是 char 类型。 - struct ThreadedTreeNode* left:指向左子节点的指针。
  • struct ThreadedTreeNode* right:指向右子节点的指针。
  • int leftTag:标志位,0 表示 left 指针指向左子树,1 表示 left 指针指向前驱节点。
  • int rightTag:标志位,0 表示 right 指针指向右子树,1 表示 right 指针指向后继节点。

3.2:初始化节点

c 复制代码
// 初始化节点
ThreadedTreeNode* Init_Node(eleType val) 
{
    ThreadedTreeNode* node = (ThreadedTreeNode*)malloc(sizeof(ThreadedTreeNode));
    node->val = val;
    node->left = NULL;
    node->right = NULL;
    node->leftTag = 0;
    node->rightTag = 0;
    return node;
}
  • ThreadedTreeNode* Init_Node(eleType val):一个函数,用于初始化一个 ThreadedTreeNode 节点。
    • ThreadedTreeNode* node = (ThreadedTreeNode*)malloc(sizeof(ThreadedTreeNode));:为新节点分配内存空间。
    • node->val = val;:将传入的值存储到节点的 val 成员中。
    • node->left = NULL;node->right = NULL;:将左右子节点指针初始化为 NULL
    • node->leftTag = 0;node->rightTag = 0;:将左右标志位初始化为 0,表示它们初始时都指向左右子树。
    • return node;:返回初始化好的节点指针。

3.3:创建一个普通二叉树

c 复制代码
// 创建普通二叉树(同前面代码中一样,略去重复部分)
ThreadedTreeNode* CreateTree(eleType a[], int size, int index, eleType nullNode) 
{
    if (index >= size || a[index] == nullNode) 
    {
        return NULL;
    }
    ThreadedTreeNode* root = Init_Node(a[index]);   //初始化节点
    root->left = CreateTree(a, size, 2 * index + 1, nullNode);  //递归创建二叉树
    root->right = CreateTree(a, size, 2 * index + 2, nullNode);
    return root;
}
  • ThreadedTreeNode* CreateTree(eleType a[], int size, int index, eleType nullNode):一个递归函数,用于根据数组元素创建普通二叉树。
    • if (index >= size || a[index] == nullNode):如果当前索引超出数组范围或元素等于 nullNode,返回 NULL,表示该位置无节点。
    • ThreadedTreeNode* root = Init_Node(a[index]);:创建当前位置的节点。
    • root->left = CreateTree(a, size, 2 * index + 1, nullNode);:递归创建左子树。
    • root->right = CreateTree(a, size, 2 * index + 2, nullNode);:递归创建右子树。
    • return root;:返回创建好的根节点。

3.4:通过中序遍历,线索化二叉树。

c 复制代码
// 中序遍历线索化
ThreadedTreeNode* prev = NULL; // 全局变量记录前驱节点
void InThreading(ThreadedTreeNode* node) 
{
    if (node == NULL) 
    {
        return;
    }

    // 线索化左子树
    InThreading(node->left);

    // 处理当前节点
    if (node->left == NULL) // 如果左子树为空,建立前驱线索
    { 
        node->left = prev;
        node->leftTag = 1;
    }
    if (prev != NULL && prev->right == NULL)  // 如果前驱的右子树为空,建立后继线索
    {
        prev->right = node;
        prev->rightTag = 1;
    }
    prev = node; // 更新前驱为当前节点

    // 线索化右子树
    InThreading(node->right);
}
  • ThreadedTreeNode* prev = NULL;:全局变量,用于记录中序遍历的前驱节点。
  • void InThreading(ThreadedTreeNode* node):对二叉树进行中序线索化的函数。
    • if (node == NULL) { return; }:如果当前节点为空,返回。
    • InThreading(node->left);:先对左子树进行线索化。
    • if (node->left == NULL) {...}:如果当前节点的左子节点为空,将 left 指针指向前驱节点(prev),并将 leftTag 置为 1
    • if (prev!= NULL && prev->right == NULL) {...}:如果前驱节点的右子节点为空,将前驱节点的 right 指针指向当前节点,并将 rightTag 置为 1
    • prev = node;:更新前驱节点为当前节点。
    • InThreading(node->right);:最后对右子树进行线索化。

      通过中序遍历线索化的图示

3.5:遍历输出(线索化二叉树)

c 复制代码
void InOrder_Threaded(ThreadedTreeNode* root) 
{
    // 从根节点开始遍历
    ThreadedTreeNode* node = root;
    while (node!= NULL) 
    {
        // 找到最左边的节点,即中序遍历的起始节点
        while (node->leftTag == 0) 
        {
            node = node->left;
        }
        // 输出当前节点的值
        printf("%c ", node->val);
        // 若当前节点的右标志位为 1,表示存在后继线索
        while (node->rightTag == 1) 
        {
            // 按照后继线索移动到下一个节点
            node = node->right;
            // 输出该后继节点的值
            printf("%c ", node->val);
        }
        // 进入右子树
        node = node->right;
    }
}

如图所示

  • void InOrder_Threaded(ThreadedTreeNode* root):这是一个函数,用于对线索化二叉树进行中序遍历。
    • ThreadedTreeNode* node = root;:首先,创建一个指针 node,并将其初始化为根节点 root,从根节点开始遍历。
    • while (node!= NULL):只要 node 不为空,就会继续遍历操作。
    • while (node->leftTag == 0) { node = node->left; }:此内层循环用于找到中序遍历的起始节点,即最左边的节点。只要当前节点的 leftTag 为 0,意味着该节点有左子树,就将 node 指针向左移动。当 leftTag 不为 0 时,说明已经到达最左边的节点。
    • printf("%c ", node->val);:输出当前节点的值。
    • while (node->rightTag == 1) {...}:若当前节点的 rightTag 为 1,说明当前节点有后继节点(通过线索化建立的后继线索)。在这个内层循环中,会不断沿着后继线索移动,并输出节点的值。
    • node = node->right;:当当前节点的 rightTag 不为 1 时,意味着需要进入右子树,将 node 指针移到右子树继续遍历。

  • 这个函数利用了线索化二叉树的 leftTagrightTag 标志位,进行中序遍历。在正常的二叉树中序遍历中,我们通常需要使用栈或递归。
  • 而在线索化二叉树中,通过 leftTag 找到最左边的节点,通过 rightTag 找到后继节点,避免了栈或递归的使用,提高了遍历的效率。
  • 需要注意的是,该函数其中 leftTag 为 0 表示指向左子树,为 1 表示指向前驱节点,rightTag 为 0 表示指向右子树,为 1 表示指向后继节点。这样,该函数可以沿着这些线索依次访问节点,完成中序遍历并输出节点的值。

3.5.1步骤解析:

以下是一个更详细的例子,帮助你理解 InOrder_Threaded 函数的工作原理。假设我们有以下线索化二叉树:

       A
      / \
     B   C
    / \   \
   D   E   F

假设我们已经对这个二叉树进行了中序线索化
(中序遍历结果为:D B E A C F)

在这个线索化二叉树中,假设 nil 表示 NULL,并且线索化后的结构为:

  • DleftTag = 1left 指针指向前驱(这里假设为 nil,因为 D 是第一个节点)。
  • DrightTag = 1right 指针指向后继 B
  • BleftTag = 0left 指针指向左子树 D
  • BrightTag = 1right 指针指向后继 E
  • EleftTag = 1left 指针指向前驱 B
  • ErightTag = 1right 指针指向后继 A
  • AleftTag = 0left 指针指向左子树 B
  • ArightTag = 0right 指针指向右子树 C
  • CleftTag = 0left 指针指向左子树(这里假设为 nil)。
  • CrightTag = 1right 指针指向后继 F
  • FleftTag = 1left 指针指向前驱 C
  • FrightTag = 1right 指针指向后继(这里假设为 nil,因为 F 是最后一个节点)。

以下是使用 InOrder_Threaded 函数对上述线索化二叉树进行中序遍历的步骤:

c 复制代码
// 中序遍历线索化二叉树
void InOrder_Threaded(ThreadedTreeNode* root) 
{
    ThreadedTreeNode* node = root;
    while (node!= NULL) 
    {
        // 找到最左边的节点
        while (node->leftTag == 0) 
        {
            node = node->left;
        }
        // 输出当前节点的值
        printf("%c ", node->val);
        // 按照后继线索遍历
        while (node->rightTag == 1) 
        {
            node = node->right;
            printf("%c ", node->val);
        }
        // 进入右子树
        node = node->right;
    }
}

解释上述代码:

  1. 中序遍历线索化二叉树
    • 调用 InOrder_Threaded 函数,从根节点 A 开始遍历。
    • 首先,node 指针从 A 开始,通过 while (node->leftTag == 0) { node = node->left; } 找到最左边的节点 D
    • 输出 D 的值,因为 DrightTag 为 1,会继续沿着后继线索输出 B 的值。
    • 由于 BrightTag 为 1,继续输出 E 的值。
    • 因为 ErightTag 为 1,输出 A 的值。
    • 此时 ArightTag 为 0,进入 A 的右子树,继续上述过程,找到 C,输出 C 的值。
    • 因为 CrightTag 为 1,输出 F 的值。

当运行 InOrder_Threaded(A) 时,会输出:D B E A C F,这是因为中序遍历的顺序是先左子树,然后根节点,最后右子树,而线索化二叉树通过线索方便地找到了前驱和后继,避免了递归或栈的使用。

InOrder_Threaded 函数中:

  • while (node!= NULL):确保遍历不会超出二叉树范围。
  • while (node->leftTag == 0) { node = node->left; }:不断向左移动,找到最左边的节点。
  • printf("%c ", node->val);:输出当前节点的值。
  • while (node->rightTag == 1) { node = node->right; printf("%c ", node->val); }:如果存在后继线索,沿着后继线索输出节点。
  • node = node->right;:如果没有后继线索,进入右子树。

通过这个例子,你可以更清楚地看到 InOrder_Threaded 函数如何利用线索化二叉树的特性,不使用递归或栈,实现中序遍历。

c 复制代码
int main() {
    const char nullNode = '*';
    char a[15] = {
        '-', '+', '/', 'a', 'x', 'e', 'f', '*', '*', 'b', '-', '*', '*', 'c', 'd'
    };
    ThreadedTreeNode* root = CreateTree(a, 15, 0, nullNode);
    InThreading(root);
    printf("中序遍历线索化二叉树:");
    InOrder_Threaded(root);
    printf("\n");
    system("pause");
    return 0;
}
  • int main():程序的入口函数。
    • const char nullNode = '*';:定义一个表示空节点的字符。
    • char a[15] = {...};:存储二叉树节点元素的数组。
    • ThreadedTreeNode* root = CreateTree(a, 15, 0, nullNode);:根据数组元素创建二叉树。
    • InThreading(root);:对创建好的二叉树进行线索化。
    • printf("中序遍历线索化二叉树:");:输出提示信息。
    • InOrder_Threaded(root);:中序遍历线索化二叉树并输出节点值。
    • system("pause");:暂停程序,等待用户按键(Windows 平台特有)。
    • return 0;:程序正常结束,返回 0

这段代码的主要功能是创建一个普通二叉树,对其进行中序线索化,然后使用中序线索化后的结构进行中序遍历。线索化二叉树是一种特殊的二叉树结构,通过 leftTagrightTag 这两个标志位,可以将原本为空的左右子节点指针利用起来,分别指向前驱和后继节点,这样在中序遍历的时候,不需要使用栈或递归就可以实现,提高了遍历的效率。对于存储的数据类型,这里使用 char 作为元素类型,但可以通过修改 eleType 的宏定义来改变存储的数据类型。对于二叉树的创建,使用了一个数组来存储元素,通过 CreateTree 函数递归构建二叉树,使用 nullNode 来表示空节点。线索化过程使用 InThreading 函数,通过 prev 变量记录前驱节点并修改节点的指针和标志位,最后使用 InOrder_Threaded 函数进行中序遍历,利用线索进行高效的遍历操作。

完整参考代码实现:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

#define eleType char

typedef struct ThreadedTreeNode { // 线索化二叉树节点
    eleType val;
    struct ThreadedTreeNode* left;  // 左指针
    struct ThreadedTreeNode* right; // 右指针
    int leftTag;  // 左标志位:0 表示指向左子树,1 表示指向前驱
    int rightTag; // 右标志位:0 表示指向右子树,1 表示指向后继
} ThreadedTreeNode;
//leftTag和rightTag默认为0

// 初始化节点
ThreadedTreeNode* Init_Node(eleType val) 
{
    ThreadedTreeNode* node = (ThreadedTreeNode*)malloc(sizeof(ThreadedTreeNode));
    node->val = val;
    node->left = NULL;
    node->right = NULL;
    node->leftTag = 0;
    node->rightTag = 0;
    return node;
}

// 创建普通二叉树(同前面代码中一样,略去重复部分)
ThreadedTreeNode* CreateTree(eleType a[], int size, int index, eleType nullNode) 
{
    if (index >= size || a[index] == nullNode) 
    {
        return NULL;
    }
    ThreadedTreeNode* root = Init_Node(a[index]);   //初始化节点
    root->left = CreateTree(a, size, 2 * index + 1, nullNode);  //递归创建二叉树
    root->right = CreateTree(a, size, 2 * index + 2, nullNode);
    return root;
}

// 中序遍历线索化
ThreadedTreeNode* prev = NULL; // 全局变量记录前驱节点
void InThreading(ThreadedTreeNode* node) 
{
    if (node == NULL) 
    {
        return;
    }

    // 线索化左子树
    InThreading(node->left);

    // 处理当前节点
    if (node->left == NULL) // 如果左子树为空,建立前驱线索
    { 
        node->left = prev;
        node->leftTag = 1;
    }
    if (prev != NULL && prev->right == NULL)  // 如果前驱的右子树为空,建立后继线索
    {
        prev->right = node;
        prev->rightTag = 1;
    }
    prev = node; // 更新前驱为当前节点

    // 线索化右子树
    InThreading(node->right);
}

// 中序遍历线索化二叉树
void InOrder_Threaded(ThreadedTreeNode* root) 
{
    ThreadedTreeNode* node = root;
    while (node != NULL) 
    {
        // 找到第一个左子树不为空的节点
        while (node->leftTag == 0) 
        {
            node = node->left;
        }

        // 输出当前节点
        printf("%c ", node->val);

        // 按照后继线索遍历
        while (node->rightTag == 1) 
        {
            node = node->right;
            printf("%c ", node->val);
        }
        // 进入右子树
        node = node->right;
    }
}

int main() {
    //const char nullnode = '*';
    //char a[24] = {
    //    nullnode,'-','+','/','a',	//4
    //    'x','e','f',nullnode,nullnode,	//9
    //    'b','-',nullnode,nullnode,nullnode,	//14
    //    nullnode,nullnode,nullnode,nullnode,nullnode,//19
    //    nullnode,nullnode,'c','d'	//23
    //};
    const char nullNode = '*';
    char a[15] = {
        '-', '+', '/', 'a', 'x', 'e', 'f', '*', '*', 'b', '-', '*', '*', 'c', 'd'
    };

    // 创建普通二叉树
    ThreadedTreeNode* root = CreateTree(a, 15, 0, nullNode);

    // 线索化二叉树
    InThreading(root);

    // 中序遍历线索化二叉树
    printf("中序遍历线索化二叉树:");
    InOrder_Threaded(root);
    printf("\n");

    system("pause");
    return 0;
}
相关推荐
hellBaron13 分钟前
C语言宏和结构体的使用代码
c语言·数据结构·算法
唐棣棣14 分钟前
期末速成C++【知识点汇总完】
开发语言·c++
yannan2019031319 分钟前
【数据结构】(Python)差分数组。差分数组与树状数组结合
开发语言·python·算法
WongKyunban1 小时前
Bash Shell知识合集
开发语言·chrome·bash
匿名码客2 小时前
STM32裸机开发转FreeRTOS教程
c语言·stm32·嵌入式硬件
起个随便的昵称2 小时前
安卓入门一 Java基础
android·java·开发语言
重剑无锋10243 小时前
python实现自动登录12306抢票 -- selenium
开发语言·python·selenium
爱干饭的boy3 小时前
教师管理系统
java·开发语言·c++·windows·python·青少年编程
shinelord明3 小时前
【再谈设计模式】策略模式 ~ 算法与行为的灵活调度员
开发语言·数据结构·算法·设计模式·数据分析·软件工程
AI人H哥会Java4 小时前
【Spring】Spring DI(依赖注入)详解—集合类型的注入——List、Set、Map的配置与注入
java·开发语言·后端·spring·架构