【数据结构与算法】第22篇:线索二叉树(Threaded Binary Tree)

一、什么是线索二叉树

1.1 空指针的利用

对于 n 个节点的二叉树,有 2n 个指针域,其中 n-1 个指向实际节点,其余 n+1 个是空指针

线索二叉树(Threaded Binary Tree):

  • 如果左孩子为空,则指向前驱节点

  • 如果右孩子为空,则指向后继节点

  • 需要增加标志位区分是指向孩子还是线索

1.2 节点结构

c

复制代码
typedef struct ThreadNode {
    int data;
    struct ThreadNode *left;
    struct ThreadNode *right;
    int ltag;  // 0: 指向左孩子, 1: 指向前驱线索
    int rtag;  // 0: 指向右孩子, 1: 指向后继线索
} ThreadNode, *ThreadTree;

画个图理解:

text

复制代码
普通节点:          线索化后:
  [data]            [data]
  /    \            /    \
left  right      left   right
  ↓     ↓          ↓      ↓
孩子  孩子       前驱/   后继/
                 孩子    孩子

二、中序线索化

2.1 算法思路

中序线索化就是在中序遍历的过程中,把空指针指向其前驱或后继。

关键 :用一个指针 pre 记录当前节点的前驱节点。

text

复制代码
中序遍历顺序:左子树 → 根 → 右子树

线索化规则:
- 如果当前节点的左孩子为空,left 指向前驱(pre),ltag = 1
- 如果 pre 的右孩子为空,pre->right 指向当前节点,rtag = 1
- 更新 pre = 当前节点

2.2 手动推导

以这棵树为例:

text

复制代码
        1
       / \
      2   3
     / \   \
    4   5   6

中序遍历结果:4 2 5 1 3 6

线索化过程

当前节点 pre 操作
4 NULL 4左空→左指NULL;pre为空,不处理右
2 4 2左不空;4右空→4右指向2
5 2 5左空→左指2;2右不空
1 5 1左不空;5右空→5右指向1
3 1 3左不空;1右不空
6 3 6左空→左指3;3右空→3右指向6

2.3 代码实现

c

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

typedef struct ThreadNode {
    int data;
    struct ThreadNode *left;
    struct ThreadNode *right;
    int ltag;  // 0: left指向左孩子, 1: left指向前驱
    int rtag;  // 0: right指向右孩子, 1: right指向后继
} ThreadNode, *ThreadTree;

ThreadNode* createNode(int value) {
    ThreadNode *node = (ThreadNode*)malloc(sizeof(ThreadNode));
    node->data = value;
    node->left = NULL;
    node->right = NULL;
    node->ltag = 0;
    node->rtag = 0;
    return node;
}

// 构建测试树
ThreadTree buildTree() {
    ThreadNode *root = createNode(1);
    root->left = createNode(2);
    root->right = createNode(3);
    root->left->left = createNode(4);
    root->left->right = createNode(5);
    root->right->right = createNode(6);
    return root;
}

ThreadNode *pre = NULL;  // 全局变量,记录前驱节点

// 中序线索化(递归)
void inThread(ThreadTree root) {
    if (root == NULL) return;
    
    // 线索化左子树
    inThread(root->left);
    
    // 处理当前节点
    if (root->left == NULL) {
        root->left = pre;
        root->ltag = 1;
    }
    if (pre != NULL && pre->right == NULL) {
        pre->right = root;
        pre->rtag = 1;
    }
    pre = root;
    
    // 线索化右子树
    inThread(root->right);
}

// 中序线索化入口
void createInThread(ThreadTree root) {
    pre = NULL;
    if (root != NULL) {
        inThread(root);
        // 处理最后一个节点的右指针
        if (pre != NULL && pre->right == NULL) {
            pre->rtag = 1;
        }
    }
}

三、中序线索二叉树的遍历

3.1 找后继节点

在中序线索树中,一个节点的后继有两种情况:

  • 如果 rtag == 1,后继就是 right(线索)

  • 如果 rtag == 0,后继是右子树中最左边的节点

c

复制代码
// 找以p为根的子树中最左边的节点
ThreadNode* leftMost(ThreadNode *p) {
    if (p == NULL) return NULL;
    while (p->ltag == 0) {  // 一直往左走
        p = p->left;
    }
    return p;
}

// 找中序后继
ThreadNode* inSuccessor(ThreadNode *p) {
    if (p->rtag == 1) {
        return p->right;  // 线索直接指向后继
    } else {
        return leftMost(p->right);  // 右子树最左边
    }
}

3.2 中序遍历

c

复制代码
// 中序遍历线索二叉树(不需要栈和递归)
void inOrderTraversal(ThreadTree root) {
    if (root == NULL) return;
    
    // 找到最左边的节点(中序第一个)
    ThreadNode *p = leftMost(root);
    
    while (p != NULL) {
        printf("%d ", p->data);
        p = inSuccessor(p);
    }
}

四、完整代码演示

c

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

typedef struct ThreadNode {
    int data;
    struct ThreadNode *left;
    struct ThreadNode *right;
    int ltag;
    int rtag;
} ThreadNode, *ThreadTree;

ThreadNode* createNode(int value) {
    ThreadNode *node = (ThreadNode*)malloc(sizeof(ThreadNode));
    node->data = value;
    node->left = NULL;
    node->right = NULL;
    node->ltag = 0;
    node->rtag = 0;
    return node;
}

ThreadTree buildTree() {
    ThreadNode *root = createNode(1);
    root->left = createNode(2);
    root->right = createNode(3);
    root->left->left = createNode(4);
    root->left->right = createNode(5);
    root->right->right = createNode(6);
    return root;
}

ThreadNode *pre = NULL;

void inThread(ThreadTree root) {
    if (root == NULL) return;
    
    inThread(root->left);
    
    if (root->left == NULL) {
        root->left = pre;
        root->ltag = 1;
    }
    if (pre != NULL && pre->right == NULL) {
        pre->right = root;
        pre->rtag = 1;
    }
    pre = root;
    
    inThread(root->right);
}

void createInThread(ThreadTree root) {
    pre = NULL;
    if (root != NULL) {
        inThread(root);
        if (pre != NULL && pre->right == NULL) {
            pre->rtag = 1;
        }
    }
}

ThreadNode* leftMost(ThreadNode *p) {
    if (p == NULL) return NULL;
    while (p->ltag == 0) {
        p = p->left;
    }
    return p;
}

ThreadNode* inSuccessor(ThreadNode *p) {
    if (p->rtag == 1) {
        return p->right;
    } else {
        return leftMost(p->right);
    }
}

void inOrderTraversal(ThreadTree root) {
    if (root == NULL) return;
    
    ThreadNode *p = leftMost(root);
    while (p != NULL) {
        printf("%d ", p->data);
        p = inSuccessor(p);
    }
}

// 递归中序遍历(用于对比)
void recursiveInorder(ThreadTree root) {
    if (root == NULL) return;
    recursiveInorder(root->left);
    printf("%d ", root->data);
    recursiveInorder(root->right);
}

// 打印线索信息
void printThreadInfo(ThreadTree root) {
    if (root == NULL) return;
    printThreadInfo(root->left);
    printf("节点%d: ltag=%d, rtag=%d", root->data, root->ltag, root->rtag);
    if (root->ltag == 1 && root->left != NULL) {
        printf(", 前驱=%d", root->left->data);
    }
    if (root->rtag == 1 && root->right != NULL) {
        printf(", 后继=%d", root->right->data);
    }
    printf("\n");
    printThreadInfo(root->right);
}

int main() {
    ThreadTree root = buildTree();
    
    printf("原二叉树(递归中序): ");
    recursiveInorder(root);
    printf("\n");
    
    printf("\n--- 开始中序线索化 ---\n");
    createInThread(root);
    
    printf("\n线索信息:\n");
    printThreadInfo(root);
    
    printf("\n线索二叉树中序遍历: ");
    inOrderTraversal(root);
    printf("\n");
    
    return 0;
}

运行结果:

text

复制代码
原二叉树(递归中序): 4 2 5 1 3 6 

--- 开始中序线索化 ---

线索信息:
节点4: ltag=1, rtag=1, 前驱=0, 后继=2
节点2: ltag=0, rtag=0
节点5: ltag=1, rtag=1, 前驱=2, 后继=1
节点1: ltag=0, rtag=0
节点3: ltag=0, rtag=1, 后继=6
节点6: ltag=1, rtag=1, 前驱=3, 后继=0

线索二叉树中序遍历: 4 2 5 1 3 6 

五、前序和后序线索化

5.1 前序线索化

前序线索化与前序遍历类似,区别在于处理当前节点的时机在左右子树之前。

c

复制代码
void preThread(ThreadTree root) {
    if (root == NULL) return;
    
    // 处理当前节点(在递归左右子树之前)
    if (root->left == NULL) {
        root->left = pre;
        root->ltag = 1;
    }
    if (pre != NULL && pre->right == NULL) {
        pre->right = root;
        pre->rtag = 1;
    }
    pre = root;
    
    // 注意:如果左子树是线索,不要递归进去
    if (root->ltag == 0) {
        preThread(root->left);
    }
    if (root->rtag == 0) {
        preThread(root->right);
    }
}

5.2 三种线索化对比

线索化类型 遍历顺序 后继查找 应用场景
中序线索化 左根右 最方便 最常用,支持双向遍历
前序线索化 根左右 较复杂 需要快速前序遍历
后序线索化 左右根 最复杂 较少使用

六、线索二叉树的优缺点

优点 缺点
遍历不需要栈或递归,节省空间 插入删除操作需要维护线索
可以快速找到前驱和后继 增加了标志位的空间开销
中序遍历时间复杂度O(n) 线索化过程本身需要遍历

七、小结

这一篇我们学习了线索二叉树:

要点 说明
核心思想 利用空指针指向前驱/后继,避免空间浪费
节点结构 增加 ltag 和 rtag 区分孩子和线索
中序线索化 在中序遍历过程中设置线索
中序遍历 从最左节点开始,不断找后继
时间复杂度 O(n),不需要递归栈

中序线索化的关键

  • pre 记录前驱节点

  • 当前节点的左空 → 指向 pre

  • pre 的右空 → 指向当前节点

下一篇我们讲树、森林与二叉树的转换。


八、思考题

  1. 为什么中序线索化是最常用的?前序和后序线索化有什么局限性?

  2. 在线索二叉树中,如何找中序前驱节点?

  3. 线索二叉树的插入操作(如在节点p的右子树插入一个新节点)需要注意什么?

  4. 尝试实现前序线索化及其前序遍历。

欢迎在评论区讨论你的答案。

相关推荐
算法鑫探2 小时前
解密2025数字密码:数位统计之谜
c语言·数据结构·算法·新人首发
:mnong2 小时前
Superpowers 项目设计分析
java·c语言·c++·python·c#·php·skills
计算机安禾2 小时前
【数据结构与算法】第21篇:二叉树遍历的经典问题:由遍历序列重构二叉树
c语言·数据结构·学习·算法·重构·visual studio code·visual studio
a里啊里啊2 小时前
测试开发面试题
开发语言·chrome·python·xpath
豆沙糕2 小时前
Python异步编程从入门到实战:结合RAG流式回答全解析
开发语言·python·面试
信奥胡老师2 小时前
P1255 数楼梯
开发语言·数据结构·c++·学习·算法
A.A呐3 小时前
【C++第二十一章】set与map封装
开发语言·c++
扶苏-su3 小时前
Java--获取 Class 类对象
java·开发语言
夜幕下的ACM之路3 小时前
一、基础知识学习(Transformer + 上下文窗口 + Token 计算 + Embedding 向量)
人工智能·学习·transformer·embedding