diff --git a/src/blog/en/post-11.md b/src/blog/en/post-11.md index 4d04bfb..2101996 100644 --- a/src/blog/en/post-11.md +++ b/src/blog/en/post-11.md @@ -9,6 +9,8 @@ image: tags: ["Study Notes", "Algorithms"] --- +

Content translated by ChatGPT.

+ ## Preface A mysterious alumnus who had already graduated shoved a hard drive into your hands. Stored inside were nearly ten years’ worth of original final exam papers! Unfortunately, however, the files were encrypted, and the study materials could only be viewed by entering the correct password. diff --git a/src/blog/en/threading-a-binary-tree.md b/src/blog/en/threading-a-binary-tree.md new file mode 100644 index 0000000..865d866 --- /dev/null +++ b/src/blog/en/threading-a-binary-tree.md @@ -0,0 +1,481 @@ +--- +title: "[Study Notes] Patching the Native Binary Tree—-Threading a Binary Tree" +pubDate: 2026-08-04 +description: 'Searching forward and backward in a binary tree—the solution programmers came up with is...?!' +author: "Cloverta" +image: + url: "https://files.seeusercontent.com/2026/08/04/fkE6/pasted-image-1785836458506.webp" + alt: "bintree" +tags: ["Study Notes", "Data Structures"] +--- + +

Content translated by ChatGPT.

+Good morning, afternoon, and evening, everyone. + +Summer has already arrived, bringing us into the hottest season of the year. For Hainan Island, where the author lives, however, this is merely another ordinary day between March and December. The year-round heat feels like an endless August. (lol + +I changed the article's URL to a summary of the article title (just like many of my friends' blog settings), I hope this will help others get a better sense of the article's overview:) + +## Before We Begin, Here Is What You Need to Know—Binary Tree Traversal + +Putting facts aside, suppose there happens to be a binary tree right in front of you. How should you obtain all of its nodes? + +Untitled drawing.drawio.png + +The answer is to count them one by one from top to bottom. (gets beaten up + +From a computer's perspective, however, for a binary tree defined like this: + +``` +#define ElemType int +typedef struct BiTNode{ + ElemType data; + struct BiTNode *lchild, *rchild; // Left and right children +}BiTNode, *BiTree; +``` + +After you give the program a root node, all it knows is this **root node**, along with the **address of its left child** and the **address of its right child**. + +From a brute-force-solves-everything perspective, we could indeed read every left and right subtree one by one, store each of them separately in memory, and then use other variables to specify their relationships... But! That would be extremely inelegant. Besides, if we are going to go through all that trouble anyway, then what was the point of designing this binary tree to optimize performance in the first place? ( + +Therefore, we first need to learn the elegant traversal methods that real programmers should use—preorder, inorder, and postorder traversal. + +### Traversal + +Imagine that we first obtain node $A$. There are three things we can do: **read its value**, **explore its left child**, and **explore its right child**. + +For preorder traversal, we first read the value and then explore the left and right children. + +``` +void PreOrder(BiTree T) { + if (T!=NULL) { // When the passed-in node T is not empty + visit(T); // Visit this node + PreOrder(T->lchild); // Enter ♂ its left child + PreOrder(T->rchild); // Enter the right child + } +} + +void visit(BiTree T) { + print(T->data); +} +``` + +This is obviously a [recursive](https://zh.wikipedia.org/zh-cn/递归) approach. Explaining this thing is rather tedious, and it is not the topic of this article. If you have questions about the code, you can refer to this video: + + + +It is definitely not because I am lazy. Nope. + +Similarly, inorder traversal simply places `visit()` in the middle. + +``` +void PreOrder(BiTree T) { + if (T!=NULL) { // When the passed-in node T is not empty + PreOrder(T->lchild); // Enter the left child + visit(T); // Visit this node + PreOrder(T->rchild); // Enter the right child + } +} +``` + +There is no need for me to elaborate on postorder traversal either. + +``` +void PreOrder(BiTree T) { + if (T!=NULL) { // When the passed-in node T is not empty + PreOrder(T->lchild); // Enter the left child + PreOrder(T->rchild); // Enter the right child + visit(T); // Visit this node + } +} +``` + +## Threading a Binary Tree + +We are now faced with a new problem: once we obtain a node, how do we get the node that comes before it in the traversal sequence? + +Surely we cannot traverse the entire tree from the beginning every single time. That would be far too troublesome. + +Look, the left or right children of some nodes in the tree are still empty. Could we perhaps make use of them? + +### The Idea + +We use the unused pointers in the binary tree to establish indexes, making them point to their own predecessor or successor nodes. + +Convention: In a threaded binary tree, the **left thread pointer points to the predecessor node**, while the **right thread pointer points to the successor node**. + +For this tree: + +Untitled drawing.drawio.png + +Its preorder sequence is: $ABDEFGC$ + +We can therefore establish thread pointers so that the left child of $D$ points to $B$, while its right child points to $E$. Similarly, the left child of $F$ points to $E$, while its right child points to $G$. + +In the end, we obtain the following diagram: + +Preorder-threaded binary tree.drawio.png + +Since there is no node after $C$, the right child of $C$ points to `NULL`. + +Pretty simple, right? Let us consider how to implement it in code. + +### Code + +#### Threading a Preorder Traversal + +We need to maintain two additional variables in the binary tree structure. + +``` +typedef struct ThreadNode { + ElemType data; + struct ThreadNode *lchild, *rchild; + int ltag, rtag; +}ThreadNode, *ThreadTree; +``` + +If `ltag` is `0`, it means that the left child is an ordinary binary-tree node. If it is `1`, it means that the pointer is a thread. + +Suppose we have now reached node $D$. From the program's perspective, how can we obtain its predecessor and successor nodes? + +For the predecessor node, we need to define an additional global pointer variable. This way, when we move to the next node, we know who its predecessor is. + +``` +ThreadNode *pre = NULL; +``` + +Then it is very simple. The predecessor of this node is obviously `pre`. + +``` +void visit(ThreadNode p){ + if (p->lchild == NULL) { // If the left child of this node is empty + p->lchild = pre; // Point the left pointer to pre, indicating that the predecessor of p is the node pointed to by pre + p->ltag = 1; // Set ltag to 1, indicating that this is a thread pointer + } + pre = p; // Move pre to the current p, and continue exploring the next node +} +``` + +How do we find the successor? + +It is very simple. Since the predecessor of `p` is `pre`, then the successor of `pre` must be `p`. Therefore: + +``` +void visit(ThreadNode p){ + if (p->lchild == NULL) { + p->lchild = pre; + p->ltag = 1; + } + if (pre != NULL && pre->rchild == NULL){ // If pre is not empty and the right child of pre is empty + pre->rchild = p; // Point the right pointer of pre to p, indicating that the successor of the node pointed to by pre is p + pre->rtag = 1; // Set rtag to 1, indicating that this is a thread pointer + } + pre = p; +} +``` + +After all nodes have been traversed, we arrive at the final node, $C$. + +Obviously, both `pre` and `p` now point to $C$. We can directly set the `rtag` of $C$ to `1`, indicating that its successor node is `NULL`. + +The recursive code is very similar to ordinary preorder traversal, but there is one point that requires attention. When we visit the left child of a node, we need to check whether that left child is already a defined thread pointer. + +``` +void PreThread(ThreadTree T) { + if (T != NULL) { + visit(T); // Visit T. If the left child of T is NULL, point it to its predecessor. If the right child of T's predecessor is NULL, point the right pointer of pre to T. + if (T->ltag != 1) PreThread(T->lchild); // Enter the left child only if it is not a thread pointer + PreThread(T->rchild); // There is no need to worry about the right child, because we have not yet processed the successor of this node + } +} +``` + +Putting the code together gives us: + +``` +typedef struct ThreadNode { + ElemType data; + struct ThreadNode *lchild, *rchild; + int ltag, rtag; // A value of 0 indicates that the corresponding left/right pointer points to an ordinary node, while 1 indicates that the corresponding pointer is a thread pointer. +}ThreadNode, *ThreadTree; + +ThreadNode *pre = NULL; + +void visit(ThreadNode p){ + if (p->lchild == NULL) { // If the left child of this node is empty + p->lchild = pre; // Point the left pointer to pre, indicating that the predecessor of p is the node pointed to by pre + p->ltag = 1; // Set ltag to 1, indicating that this is a thread pointer + } + if (pre != NULL && pre->rchild == NULL){ // If pre is not empty and the right child of pre is empty + pre->rchild = p; // Point the right pointer of pre to p, indicating that the successor of the node pointed to by pre is p + pre->rtag = 1; // Set rtag to 1, indicating that this is a thread pointer + } + pre = p; // Move pre to the current p, and continue exploring the next node +} + +void PreThread(ThreadTree T) { + if (T != NULL) { + visit(T); // Visit T. If the left child of T is NULL, point it to its predecessor. If the right child of T's predecessor is NULL, point the right pointer of pre to T. + if (T->ltag != 1) PreThread(T->lchild); // Enter the left child only if it is not a thread pointer + PreThread(T->rchild); // There is no need to worry about the right child, because we have not yet processed the successor of this node + } +} + +void CreatePreThread(ThreadTree T){ + pre = NULL; // Initialize the pre pointer + if (T != NULL) { + PreThread(T); // Begin recursion + if (pre->rchild==NULL) + // After the preorder traversal recursion ends, pre should point to the final node C, which will never have a right child under any circumstances. + // However, in a postorder traversal, pre would actually point to A rather than C, and the right child of A is not empty, so it cannot be used directly as a thread. + // Therefore, for the sake of code reusability, we add this check. + pre->rtag=1; // If it has no right child, directly indicate that its successor node is NULL + } +} +``` + +#### Threading Inorder and Postorder Traversals + +The underlying idea is actually very similar. Only the visiting order differs, while the specific `visit()` code remains the same, so I will not elaborate further here. + +**Inorder threading**: + +``` +void InThread(ThreadTree T){ + if (T!=NULL){ + InThread(T->lchild); + visit(T); + InThread(T->rchild); + } +} +``` + +**Postorder threading**: + +``` +void PostThread(ThreadTree T) { + if (T!=NULL) { + PostThread(T->lchild); + PostThread(T->rchild); + visit(T); + } +} +``` + + + +## Using a Threaded Binary Tree to Find Predecessors and Successors + +Ahem... What was our original goal again...? Right, given a node, find its predecessor and successor! + +### Inorder-Threaded Binary Tree + +Here, we will first introduce how to find predecessors and successors in an **inorder-threaded binary tree**. Preorder and postorder have certain special characteristics, which we will discuss later. + +Inorder threads.drawio.png + +For convenience, I have gone ahead and posted the inorder-threaded binary tree here. Those who are interested can work it out for themselves. ( + +Obviously, if a node has both a left thread and a right thread, then the left thread is its predecessor and the right thread is its successor. For example, we can tell at a glance that the predecessor of $D$ is `NULL`, while its successor is $B$. Absolutely poggers! + +But what about a node without any thread pointers? For example, how do we find the predecessor and successor of $A$? + +#### The Successor + +It is actually very simple. Since the order of inorder traversal is: + +$$ +Left \ Root \ Right +$$ + +The successor of node $A$ must be located in its right subtree. Expanding the right subtree gives us: + +$$ +Left \ Root \ (Left \ Root \ Right) +$$ + +The highest-priority candidate for the successor of this node must therefore be the leftmost leaf node in the right subtree. + +If no left leaf node exists, then we have: + +$$ +Left \ Root \ (Root \ Right) +$$ + +In other words, the root node of that subtree is the successor. + +What? You are asking about the right node? If the root node does not exist, how could there possibly be a right node? Therefore, we do not need to consider the right node of the citrus tree. + +Thus: + +``` +ThreadNode *FirstNode(ThreadNode *p) { // This is the second step; the first step is the function below + while(p->ltag==0) p = p->lchild; // Keep going deeper into the left subtree until the leftmost node is found—in other words, a node without a left child + return p; // This node is the successor we are looking for +} + +ThreadNode *NextNode(ThreadNode *p) { + if (p->rtag==0) return Firstnode(p->rchild); // If a right child exists, enter the right subtree + else return p->rchild; // If the right pointer is a thread pointer, then the node it points to is the successor +} +``` + +#### The Predecessor + +The idea is similar to that of finding the successor. The predecessor must be in the left subtree of node $A$. Expanding the left subtree gives us: + +$$ +(Left \ Root \ Right) \ Root \ Right +$$ + +As we can see, the highest-priority candidate for the predecessor is the rightmost leaf node in the left subtree. + +If no right leaf node exists, then we have: + +$$ +(Left \ Root) \ Root \ Right +$$ + +Similarly, our approach is to find the rightmost node in the left subtree: + +``` +ThreadNode *LastNode(ThreadNode *p){ // This is the second step; the first step is the function below + while(p->rtag==0) p = p->rchild; // Keep going deeper into the right subtree until the rightmost node is found—in other words, a node without a right child + return p; +} + +ThreadNode *PreNode(ThreadNode *p){ + if (p->ltag==0) return LastNode(p->lchild); // If there is a left child, enter the left subtree + else return p->lchild; // Is there a thread on the left? Then it has already told us the predecessor +} +``` + +### Preorder-Threaded Binary Tree + +#### The Successor + +This is actually similar to inorder traversal. We know that the order of preorder traversal is: + +$$ +Root \ Left \ Right +$$ + +Therefore, the successor in preorder traversal should be found in the left subtree. Expanding the left subtree gives us: + +$$ +Root \ (Root \ Left \ Right) \ Right +$$ + +In other words, when a left child exists, the successor is simply the left child! Pretty simple, right? + +What if there is no left child? + +$$ +Root \ Right +$$ + +In this case, we look at its right child: + +$$ +Root \ (Root \ Left \ Right) +$$ + +Obviously, its successor is the right child. + +What if there is no right child either? + +Even better. Would that not mean its right pointer is a thread? The right pointer points directly to its successor node. + +#### The Predecessor + +If the node has no left child, then its left pointer points to its predecessor. This is very simple. + +But what if it has a left child? + +Oh dear, now things get troublesome. Since the preorder traversal order is: + +$$ +Root \ Left \ Right +$$ + +The elements in the left and right subtrees must be visited after the root. Unless we traverse the entire tree again from the beginning, there is no way to directly find the predecessor of the root using the current data structure. + +But rules are made to be broken! (booming voice + +We can modify this binary tree into a ternary tree. + +> **Ternary tree**: Compared with an ordinary binary tree, it has one additional pointer that points to the parent node. + +If we know its parent node, the situation becomes different. + +##### The Root Node Is the Left Child of Its Parent + +In this case, the traversal order is: + +$$ +Parent \ Root \ Right\ Sibling +$$ + +Therefore, the predecessor of the root is its parent node. + +##### The Root Is the Right Child of Its Parent + +The traversal order then becomes: + +$$ +Parent \ Left\ Sibling \ Root +$$ + +Expanding the left subtree of the parent node gives us: + +$$ +Parent \ (Left\ Sibling \ Left's\ Left \ Left's\ Right) \ Root +$$ + +Does this look familiar? We need to find the rightmost node within the left sibling's subtree. The process is similar to inorder traversal, so I will not elaborate on it here. + +I hope the word “left” still looks familiar to you. + +### Postorder-Threaded Binary Tree + +It is exactly the opposite of preorder. + +#### The Predecessor + +Since the traversal order is: + +$$ +Left \ Right \ Root +$$ + +You know what I am about to say. Give it a try yourself. + +#### The Successor + +We also need to turn it into a ternary tree, and we need to know its parent node in advance. + +##### The Root Is the Parent's Left Child + +I trust that you understand what this subheading means. (trying not to laugh + +The traversal order is: + +$$ +Root \ Right\ Sibling \ Parent +$$ + +Obviously, we need to expand the subtree of the right sibling: + +$$ +Root\ (Right's\ Left \ Right's\ Right \ Right\ Sibling) \ Parent +$$ + +That is, we need to find the leftmost node in the right sibling's subtree. + +##### The Root Is the Parent's Right Child + +$$ +Left\ Sibling \ Root \ Parent +$$ + +Then it is obvious that the successor of the root node is its parent node. \ No newline at end of file diff --git a/src/blog/zh/threading-a-binary-tree.md b/src/blog/zh/threading-a-binary-tree.md new file mode 100644 index 0000000..3ef6cd5 --- /dev/null +++ b/src/blog/zh/threading-a-binary-tree.md @@ -0,0 +1,463 @@ +--- +title: "[学习笔记]给原生二叉树打上补丁——二叉树的线索化" +pubDate: 2026-08-04 +description: '二叉树的向前和向后搜索,程序员们给出的解决方法是……?!' +author: "三叶" +image: + url: "https://files.seeusercontent.com/2026/08/04/fkE6/pasted-image-1785836458506.webp" + alt: "bintree" +tags: ["学习笔记", "数据结构"] +--- + +各位早上中午晚上好。 + +时间已然进入夏天,进入了全年最炎热的季节。而对于笔者所在的海南岛来说则又是3月至12月的寻常一天罢了,全年的高温就好似漫无止境的八月一般(笑 + +我把文章的url改为了文章标题的概括(和许多朋友的博客设定一样),希望这有助于其他人更好的了解文章概览:) + +## 在开始之前,你需要知道的——二叉树的遍历 + +抛开事实不谈,假如你面前正好有一颗二叉树,你应该怎么获取它全部的节点呢? + +未命名绘图.drawio.png + +答案是从上到下一个一个数(挨打。 + +但是从计算机的角度来看,对于一个如此定义的二叉树: + +```c +#define ElemType int +typedef struct BiTNode{ + ElemType data; + struct BiTNode *lchild, *rchild; // 左右孩子 +}BiTNode, *BiTree; +``` + +当你给了程序一个根节点之后,它所知道的只有这个**根节点**还有它的**左孩子的地址**与**右孩子的地址**。 + +从某种力大砖飞的角度来说,我们确实可以一个一个读它所有的左右子树并且把它们都单独存在内存中并再存其他变量来指定它们的关系……但是!这非常的不优雅,而且既然都大费周章的这样搞了那我们设计这个二叉树来优化性能的目的是什么( + +所以我们需要先了解一下真正程序员应该做的优雅遍历方式——先序、中序和后序遍历。 + +### 遍历 + +试想当我们首先拿到A节点,我们可以做的事情有3个:**读取它的值**、**探索它的左孩子**、**探索它的右孩子**。 + +对于先序遍历来说,我们首先读取值,再探索左右孩子 + +```c +void PreOrder(BiTree T) { + if (T!=NULL) { // 当传入的节点T非空时 + visit(T); // 访问这个节点 + PreOrder(T->lchild); // 进入♂它的左孩子 + PreOrder(T->rchild); // 进入右孩子 + } +} + +void visit(BiTree T) { + print(T->data); +} +``` + +这显然是一种[递归](https://zh.wikipedia.org/zh-cn/%E9%80%92%E5%BD%92)的思想。这玩意儿讲解起来挺繁琐的,而且本文章的主题并不是这个,如果你对代码有疑惑可以参考这个视频: + + + + +绝对不是因为我懒,嗯。 + +同理中序遍历就是把`visit()`放在中间 + +```c +void PreOrder(BiTree T) { + if (T!=NULL) { // 当传入的节点T非空时 + PreOrder(T->lchild); // 进入左孩子 + visit(T); // 访问这个节点 + PreOrder(T->rchild); // 进入右孩子 + } +} +``` + +后序遍历也就不用我赘述 + +```c +void PreOrder(BiTree T) { + if (T!=NULL) { // 当传入的节点T非空时 + PreOrder(T->lchild); // 进入左孩子 + PreOrder(T->rchild); // 进入右孩子 + visit(T); // 访问这个节点 + } +} +``` + +## 二叉树的线索化 + +现在我们面临了一个新的难题:拿到了一个节点,我们该如何获取遍历序列中它的上一个节点? + +我们总不能每一次都从头再遍历一遍吧,这太麻烦了。 + +你看,树上有些节点的左孩子/右孩子还是空的,那我们是不是可以对它稍加利用? + +### 思想 + +我们利用二叉树空出来的指针建立起索引,让它们指向自己的前驱/后继节点。 + +规定:线索二叉树的**左索引指针指向前驱节点**,**右索引指针指向后继节点**。 + +对于这颗树来说 + +未命名绘图.drawio.png + +它的先序序列为:$ABDEFGC$ + +那么我们可以建立起索引指针,让 $D$ 的左孩子指向 $B$ ,右孩子指向 $E$ 。同理,$F$ 的左孩子指向 $E$,右孩子指向 $G$。 + +最终我们可以得到如下的图 + +先序线索二叉树.drawio.png + +由于 $C$ 之后没有节点了,所以 $C$ 的右孩子指向`NULL`。 + +很简单对吧?我们来考虑下代码怎么实现。 + +### 代码 + +#### 先序遍历的线索化 + +我们需要在二叉树的结构体中额外维护两个变量 + +```c +typedef struct ThreadNode { + ElemType data; + struct ThreadNode *lchild, *rchild; + int ltag, rtag; +}ThreadNode, *ThreadTree; +``` + +`ltag`若为`0`则代表左孩子是普通的二叉树节点,如果为`1`则代表是线索节点。 + +假如我们现在拿到了 $D$ 节点。试想一下从程序的角度我们怎么获取它的前驱和后继节点? + +对于前驱节点来说,我们需要额外定义一个全局的指针变量,这样我们移动到下一个节点时就知道下个节点的前驱是谁了。 + +```c +ThreadNode *pre = NULL; +``` + +那么很简单,这个节点的前驱显而易见就是`pre`了。 + +```c +void visit(ThreadNode p){ + if (p->lchild == NULL) { // 如果这个节点的左孩子空着 + p->lchild = pre; // 把左指针指向pre,表示p的前驱是pre指向的节点 + p->ltag = 1; // 把ltag设置为1,表示这是个索引指针 + } + pre = p; // pre指向下一位p,我们继续探索下一个节点 +} +``` + +怎么寻找后继呢? + +很简单,既然`p`的前驱是`pre`,那`pre`的后继就肯定是`p`呀,那么就有 + +```c +void visit(ThreadNode p){ + if (p->lchild == NULL) { + p->lchild = pre; + p->ltag = 1; + } + if (pre != NULL && pre->rchild == NULL){ // 如果pre非空且pre的右孩子为空 + pre->rchild = p; // 把pre的右指针指向p,表示pre指向的节点的后继是p + pre->rtag = 1; // rtag设置为1,表示这是个索引指针 + } + pre = p; +} +``` + +当所有的节点遍历结束,我们来到了最后一个节点 $C$。 + +很显然此时`pre`和`p`都指向 $C$,我们直接将 $C$ 的`rtag`标为`1`即可,表明它的后继节点是`NULL`。 + +递归代码和普通先序遍历很接近,但是有一个点需要注意一下。当我们访问节点的左孩子时,我们需要检查一下这个左孩子是不是一个已经定义好的线索指针。 + +```c +void PreThread(ThreadTree T) { + if (T != NULL) { + visit(T); // 访问T,如果T的左孩子为NULL则将其指向前驱,如果T的前驱的右孩子为NULL则将pre的右指针指向T。 + if (T->ltag != 1) PreThread(T->lchild); // 如果左孩子不是线索指针才进入左孩子 + PreThread(T->rchild); // 右孩子不必担心,因为此时我们并未处理该节点的后继 + } +} +``` + +整理一下代码可以得到 + +```c +typedef struct ThreadNode { + ElemType data; + struct ThreadNode *lchild, *rchild; + int ltag, rtag; // 为0则表明对应的左/右指针指向普通节点,为1则表明对应指针是线索指针。 +}ThreadNode, *ThreadTree; + +ThreadNode *pre = NULL; + +void visit(ThreadNode p){ + if (p->lchild == NULL) { // 如果这个节点的左孩子空着 + p->lchild = pre; // 把左指针指向pre,表示p的前驱是pre指向的节点 + p->ltag = 1; // 把ltag设置为1,表示这是个索引指针 + } + if (pre != NULL && pre->rchild == NULL){ // 如果pre非空且pre的右孩子为空 + pre->rchild = p; // 把pre的右指针指向p,表示pre指向的节点的后继是p + pre->rtag = 1; // rtag设置为1,表示这是个索引指针 + } + pre = p; // pre指向下一位p,我们继续探索下一个节点 +} + +void PreThread(ThreadTree T) { + if (T != NULL) { + visit(T); // 访问T,如果T的左孩子为NULL则将其指向前驱,如果T的前驱的右孩子为NULL则将pre的右指针指向T。 + if (T->ltag != 1) PreThread(T->lchild); // 如果左孩子不是线索指针才进入左孩子 + PreThread(T->rchild); // 右孩子不必担心,因为此时我们并未处理该节点的后继 + } +} + +void CreatePreThread(ThreadTree T){ + pre = NULL; // 初始化pre指针 + if (T != NULL) { + PreThread(T); // 开始递归 + if (pre->rchild==NULL) + // 先序遍历递归结束后,pre应当指向的是最后一个节点C,它在任何情况下都不会有右孩子。 + // 但若是后序遍历,那么pre指向的其实是A而不是C,而A的右孩子非空,不可直接用作索引。 + // 所以为了代码的可复用性我们加上这一个判断。 + pre->rtag=1; // 若它没有右孩子,我们直接表示它的后继节点是NULL + } +} +``` + +#### 中序、后序遍历的线索化 + +其实思路非常相近,只是访问次序不一样,且具体的`visit()`代码是一样的,在这里我就不过多赘述。 + +**中序线索化**: + +```c +void InThread(ThreadTree T){ + if (T!=NULL){ + InThread(T->lchild); + visit(T); + InThread(T->rchild); + } +} +``` + +**后序线索化**: + +```c +void PostThread(ThreadTree T) { + if (T!=NULL) { + PostThread(T->lchild); + PostThread(T->rchild); + visit(T); + } +} +``` + + + +## 利用线索二叉树寻找前驱后继 + +咳咳……我们最初的目的是什么来着……!对,给定一个节点找它的前驱和后继! + +### 中序线索二叉树 + +这里我们先介绍**中序线索二叉树**寻找前驱后继的方法,先序和后序具有特殊性,这一点我们后面再讲。 + +中序线索.drawio.png + +那么为了方便,我就先把中序线索的二叉树贴出来了,感兴趣的可以去计算一下( + +显而易见地,如果一个节点存在左线索和右线索,那么左线索就是前驱,右线索就是后继,就像我们一眼就能看出 $D$ 的前驱是`NULL`,后继是 $B$ ,蒸蚌! + +但对于一个没有线索指针的节点呢?比如说 $A$,我们怎么寻找它的前驱后继? + +#### 对于后继 + +其实很简单,由于我们中序遍历的顺序是 +$$ +左 \ \ 根 \ \ 右 +$$ +那 $A$ 节点的后继必然是在它的右子树中,把右子树展开来看 +$$ +左 \ \ 根 \ \ (左\ \ 根\ \ 右) +$$ +该节点的后继必然最优先是右子树中的最左叶子节点。 + +如果左叶子节点不存在,则是 +$$ +左\ \ 根\ \ ( 根\ \ 右) +$$ +即该子树的根节点。 + +什么?你问右节点?根节点如果不存在,怎么可能会有右节点嘛,所以柚子树的右节点我们不纳入考虑。 + +于是乎有 + +```c +ThreadNode *FirstNode(ThreadNode *p) { // 这是第二步,第一步是下面那个函数 + while(p->ltag==0) p = p->lchild; // 不断向左子树深入,直到找到最左的节点,换言之不存在左孩子的节点 + return p; // 这个节点就是我们要找的后继 +} + +ThreadNode *NextNode(ThreadNode *p) { + if (p->rtag==0) return Firstnode(p->rchild); // 如果存在右孩子,则进入右子树 + else return p->rchild; // 如果右指针是个索引指针,那它指向的就是后继 +} +``` + +#### 对于前驱 + +和后继思想相似,前驱肯定在 $A$ 节点的左子树中,那我们把左子树展开 +$$ +(左\ \ 根\ \ 右)\ \ 根\ \ 右 +$$ +可以看到,该节点的前驱最优先是左子树中的右叶子节点。 + +如果右叶子节点不存在则有 +$$ +(左\ \ 根 )\ \ 根\ \ 右 +$$ +同样,我们的思路就是在左子树中找到最右节点: + +```c +ThreadNode *LastNode(ThreadNode *p){ // 这是第二步,第一步是下面那个函数 + while(p->rtag==0) p = p->rchild; // 不断向右子树深入,直到找到最靠右的节点,即不存在右孩子的节点 + return p; +} + +ThreadNode *PreNode(ThreadNode *p){ + if (p->ltag==0) return LastNode(p->lchild); // 有左孩子就进入左子树 + else return p->lchild; // 左边有索引?那前驱已经告诉我们了 +} +``` + +### 先序线索二叉树 + +#### 对于后继 + +和中序其实类似,我们知道先序遍历的顺序是 +$$ +根\ \ 左\ \ 右 +$$ +那么先序的后继其实要从左子树中找,我们把左子树展开 +$$ +根\ \ (根\ \ 左\ \ 右)\ \ 右 +$$ +即当左孩子存在时,它的后继其实就是左孩子!很简单吧? + +如果没有左孩子呢? +$$ +根\ \ 右 +$$ +这时候我们看他的右孩子 +$$ +根\ \ (根\ \ 左\ \ 右) +$$ +显而易见,它的后继就是右孩子。 + +如果没有右孩子呢? + +那太好了,那它的右指针不就是索引嘛?右指针指向的就是后继节点。 + +#### 对于前驱 + +如果节点没有左孩子,那么左指针指向的就是前驱。这很简单。 + +但如果有左孩子呢? + +啊呀呀这下子麻烦了,既然先序遍历的顺序是 +$$ +根\ \ 左\ \ 右 +$$ +那么左右子树中的元素必定在根之后被访问。除非我们从头来遍历一遍,否则无法在现有数据结构内直接找到根的前驱。 + +但是规则就是用来打破的!(震声 + +我们可以把这个二叉树修改为三叉树。 + +> **三叉树**:比普通二叉树多了一个指向父节点的指针。 + +如果我们知道它的父节点,那么情况就不一样了。 + +##### 根节点是父节点的左孩子 + +在此情况下,遍历顺序是 +$$ +父 \ \ 根\ \ 右兄弟 +$$ +那么根的前驱就是父节点 + +##### 根是父节点的右孩子 + +那么遍历顺序变成了 +$$ +父\ \ 左兄弟\ \ 根 +$$ +把父节点的左子树展开 +$$ +父\ \ (左兄弟\ \ 左的左\ \ 左的右)\ \ 根 +$$ +是不是很眼熟?那我们就需要找左兄弟节点中的最右节点,过程和中序遍历相似,在这里我就不展开赘述。 + +希望你还认识“左”这个字。 + +### 后序线索二叉树 + +和先序刚好相反。 + +#### 对于前驱 + +因为遍历顺序是 +$$ +左\ \ 右\ \ 根 +$$ +你知道我要说什么。动手来试试吧。 + +#### 对于后继 + +我们也需要将其设置为三叉树。并且需要提前知道它的父节点。 + +##### 根是父的左 + +我相信你能看懂这个小标题是什么意思(憋笑) + +那遍历顺序就是 +$$ +根\ \ 右兄弟\ \ 父 +$$ +显然需要展开右兄弟的子树 +$$ +根\ \ (右的左\ \ 右的右\ \ 右兄弟)\ \ 父 +$$ +那也就是在右兄弟的子树中找到最靠左的节点。 + +##### 根是父右 + +$$ +左兄弟\ \ 根\ \ 父 +$$ + +那显而易见根节点的后继就是它的父节点。 \ No newline at end of file