“废物利用“的艺术:线索二叉树全解析!

🔗 "废物利用"的艺术:线索二叉树全解析!🌲

哈喽,各位代码世界的探险家们!👋

上次我们聊了二叉树遍历的“爱恨情仇”,今天我们来搞点更酷的——给二叉树“穿针引线”,也就是大名鼎鼎的线索二叉树 (Threaded Binary Tree)

你有没有觉得,普通二叉树里那些指向 NULL 的指针好浪费?🤔 对于一个有 n 个节点的二叉树,足足有 n+1NULL 指针!它们就像城市里闲置的土地,啥也没干,太可惜了!

核心思想: 与其让这些指针闲着,不如让它们“再就业”!用它们来指向节点在特定遍历次序下的前驱后继

这样一来,我们就把“废物”变成了“宝藏”💎,不仅节约了空间,还获得了一个超能力:无需递归或栈,就能光速找到节点的前驱和后继,并高效地遍历整棵树!

听起来是不是很棒?下面,就让我们一起揭开它的神秘面纱吧!

🧵 准备工作:节点的新装

为了区分指针是指向孩子(实线)还是线索(虚线),我们需要给节点结构加两个标志位:

  • LTag (Left Tag):
    • 0: 左指针指向左孩子 (Link)。
    • 1: 左指针指向前驱线索 (Thread)。
  • RTag (Right Tag):
    • 0: 右指针指向右孩子 (Link)。
    • 1: 右指针指向后继线索 (Thread)。

C++风格的伪代码结构看起来是这样的:

// C++ Style Pseudocode
struct ThreadNode {
    DataType data;
    ThreadNode* leftChild;
    ThreadNode* rightChild;
    int lTag; // 0 for link, 1 for thread
    int rTag; // 0 for link, 1 for thread
};

接下来,我们将分类型探讨最经典的中序线索化,以及相对复杂的前序和后序线索化。


🥇 中序线索二叉树:最经典、最和谐的“穿针引线”

中序遍历(左 → 根 → 右)的顺序与节点的前驱、后继关系最为自然,因此中序线索化是最常用、最重要的一种!

  • 线索化规则:
    • 若节点的左指针为空,则将其指向它的中序前驱
    • 若节点的右指针为空,则将其指向它的中序后继

🔍 如何寻找中序前驱 & 后继?

这正是中序线索树的魅力所在!

寻找后继 (Successor)
  1. 有线索吗? 如果节点的 rTag1,那么 rightChild 指针直接就指向了它的后继!一步到位!🚀
  2. 没线索怎么办? 如果 rTag0,说明它有右子树。根据中序遍历(左→根→右)的原则,它的后继就是其右子树中“最左边”的那个节点
// C++ Style Pseudocode
ThreadNode* findInOrderSuccessor(ThreadNode* node) {
    if (node->rTag == 1) {
        return node->rightChild; // Case 1: Thread exists!
    } else {
        // Case 2: Go to the right subtree and find its leftmost node.
        ThreadNode* p = node->rightChild;
        while (p->lTag == 0) {
            p = p->leftChild;
        }
        return p;
    }
}
寻找前驱 (Predecessor)

逻辑完全对称!

  1. 有线索吗? 如果节点的 lTag1,那么 leftChild 指针就指向了它的前驱。
  2. 没线索怎么办? 如果 lTag0,说明它有左子树。它的前驱就是其左子树中“最右边”的那个节点
// C++ Style Pseudocode
ThreadNode* findInOrderPredecessor(ThreadNode* node) {
    if (node->lTag == 1) {
        return node->leftChild; // Case 1: Thread exists!
    } else {
        // Case 2: Go to the left subtree and find its rightmost node.
        ThreadNode* p = node->leftChild;
        while (p->rTag == 0) {
            p = p->rightChild;
        }
        return p;
    }
}

🚶‍♂️ 如何遍历中序线索二叉树?

超级简单!我们再也不需要栈了!

  1. 从整棵树的“最左边”的节点开始。
  2. 循环执行:
    • 访问当前节点。
    • 通过我们刚才写的 findInOrderSuccessor 函数找到下一个节点。
  3. 直到遍历完所有节点。
// C++ Style Pseudocode
void inOrderTraversal(ThreadNode* root) {
    // 1. Find the very first node to visit (the leftmost one).
    ThreadNode* p = root;
    while (p->lTag == 0) {
        p = p->leftChild;
    }

    // 2. Loop through all nodes using successor pointers.
    while (p != nullptr) {
        visit(p); // Process the current node
        p = findInOrderSuccessor(p);
    }
}

🥈 前序线索二叉树:老大的“前后关系”

前序遍历(根 → 左 → 右)的线索化稍微有些不同。

  • 线索化规则:
    • 若节点的左指针为空,则将其指向它的前序前驱
    • 若节点的右指针为空,则将其指向它的前序后继

🔍 寻找前序前驱 & 后继?

寻找后继 (Successor)

这个比较简单!根据(根 → 左 → 右)的顺序:

  1. 如果它有左孩子,那么左孩子就是它的后继。
  2. 如果它没有左孩子,但有右孩子,那么右孩子就是后继。
  3. 如果它是个叶子节点,那么它的后继已经由 rightChild 这条线索指明了。
// C++ Style Pseudocode
ThreadNode* findPreOrderSuccessor(ThreadNode* node) {
    if (node->lTag == 0) {
        return node->leftChild;  // Has left child, it's the successor.
    } else {
        // No left child, the right pointer (either a link or a thread) points to the successor.
        return node->rightChild;
    }
}
寻找前驱 (Predecessor)

这个就复杂了!🤯 前序前驱可能是它的父节点,也可能是父节点的左兄弟子树中的某个节点。在没有父指针的情况下,仅通过当前节点和线索很难直接找到前驱。 这也是前序线索化不那么流行的原因之一。通常需要遍历或者借助父指针才能高效完成。

🚶‍♂️ 如何遍历前序线索二叉树?

遍历依然很高效!

  1. 从根节点开始。
  2. 循环执行:
    • 访问当前节点。
    • 通过 findPreOrderSuccessor 找到下一个。
// C++ Style Pseudocode
void preOrderTraversal(ThreadNode* root) {
    ThreadNode* p = root;
    while (p != nullptr) {
        visit(p);
        p = findPreOrderSuccessor(p);
    }
}

🥉 后序线索二叉树:压轴的“究极挑战”

后序遍历(左 → 右 → 根)的线索化是三者中最复杂的。

  • 线索化规则:
    • 若节点的左指针为空,则将其指向它的后序前驱
    • 若节点的右指针为空,则将其指向它的后序后继

🔍 寻找后序前驱 & 后继?

寻找后继 (Successor)

后序后继可能是它的父节点,或者是它父节点的右兄弟。和前序前驱一样,在没有父指针的情况下,后序找后继也非常困难!

寻找前驱 (Predecessor)

这个反而相对清晰!根据(左 → 右 → 根)的顺序:

  1. 如果它有右孩子,那么右孩子就是它的前驱。
  2. 如果它没有右孩子,但有左孩子,那么左孩子就是前驱。
  3. 如果它是个叶子节点,它的前驱已经由 leftChild 这条线索指明了。
// C++ Style Pseudocode
ThreadNode* findPostOrderPredecessor(ThreadNode* node) {
    if (node->rTag == 0) {
        return node->rightChild; // Has right child, it's the predecessor.
    } else if (node->lTag == 0) {
        return node->leftChild;  // No right child but has left child, it's the predecessor.
    } else {
        return node->leftChild;  // Is a leaf, leftChild is a thread to the predecessor.
    }
}

🚶‍♂️ 如何遍历后序线索二叉树?

由于找后继很困难,后序遍历通常是逆向的!即从最后一个节点开始,不断寻找前驱

  1. 找到整棵树的根节点(它是最后一个被访问的)。
  2. 找到它的前驱,然后是前驱的前驱……
  3. 这样得到的序列反过来就是后序遍历序列了。

因为这种复杂性,后序线索二叉树在实际应用中非常罕见。

🏁 横向大比拼 & 总结

特性中序线索树 (In-order)前序线索树 (Pre-order)后序线索树 (Post-order)
实现难度⭐⭐ (简单)⭐⭐⭐ (中等)⭐⭐⭐⭐⭐ (困难)
找后继✅ 非常容易✅ 比较容易❌ 非常困难 (需父指针)
找前驱✅ 非常容易❌ 比较困难 (需父指针)✅ 比较容易
遍历方式正向遍历,方便正向遍历,方便逆向遍历,不直观
应用广泛度最常用 👍较少使用几乎不用 👎

总而言之,线索二叉树的核心价值在于:

  1. 空间高效: 盘活了所有 NULL 指针,一滴都不浪费!
  2. 时间高效: 实现了非递归、无栈的快速遍历,以及对前驱/后继的快速定位(尤其在中序中)。

它就像是对普通二叉树的一次精巧的“魔改”,虽然增加了一点点实现的复杂性,却换来了巨大的便利。特别是中序线索二叉树,绝对是值得你掌握的经典数据结构!

希望这篇博客能让你对线索二叉树有一个清晰又有趣的认识!快去动手实现一个吧,你会爱上这种“穿针引线”的感觉的!

觉得有用别忘了点赞分享哦!下次见啦!💖

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

会有风

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

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

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

打赏作者

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

抵扣说明:

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

余额充值