线索二叉树与线索二叉树的迭代遍历

线索二叉树(Threaded Binary Tree)

在孩子表示法二叉树中,任何节点的左右孩子指针都可能为空,事实上总空指针数目满足如下规律:

a) 设任意的孩子表示法二叉树有\(n\)个节点,该二叉树的空指针数目为\(n+1\);

证明:设度为0,1,2的节点数为\(n_0,n_1,n_2\),则空指针总数为\(x=2n_0+n_1\)。之前讨论二叉树性质时证明了\(n_0=n_2+1\)。由于二叉树节点数\(n=n_0+n_1+n_2\),那么\(n=2n_0+n_1-1\),所以\(x=n+1\)。

上述性质表明二叉树有大量空指针,若让空指针指向其他节点以记录额外信息,就能得到线索二叉树。线索二叉树节点需额外维护2个变量描述指针是否表示孩子,这里计为\(leftType,rightType\)。\(leftType=0\)表示指向原左孩子,\(leftType=1\)表示指向其他节点,此时的左孩子指针称为线索,\(rightType\)同理。将普通二叉树变成线索二叉树的过程称为线索化。一般来说线索二叉树特指“用线索记录节点在遍历序列前驱后继的线索二叉树”,所以本文讨论中/前/后序线索二叉树。中/前/后序线索二叉树可按如下方式通过原二叉树来定义,其中\(x\)是“原二叉树”任意节点:

1) 若\(x.left\)为空则置\(x.leftType=1\)并置\(x.left\)为\(x\)的中/前/后序前驱(无前驱指向空),否则置\(x.leftType=0\);

2) 若\(x.right\)为空则置\(x.rightType=1\)并置\(x.right\)为\(x\)的中/前/后序后继(无后继指向空),否则置\(x.rightType=0\);

先给出线索二叉树节点程序如下,之后要讨论的线索化算法会把“原二叉树节点”转换成这种节点。

 

中序线索二叉树

对于任意节点数为\(n>0\)的普通二叉树,其中序序列首节点\(head\)无左子树,末节点\(tail\)无右子树,当\(n=1\)时\(head\)和\(tail\)是同个节点。所以将其中序线索化后,\(head.left\)是空线索,\(tail.right\)是空线索,且其他线索都非空。而根据定义其他非线索指针也都非空。所以中序线索二叉树中除\(head.left\)和\(tail.right\)外无其他空指针

实现中序线索化算法的思路是以中序遍历算法为框架,在遍历刚“访问”节点(计为\(node\))时确保指针\(prev\)此时指向“上次访问的\(node\)”,进而在中序遍历每次访问的时候,仅通过当前的\(node,prev\)就可以建立线索。需注意的是,在具体实现时要保证这个“一边遍历一边线索化”的过程不影响原本的遍历次序。另外由于中序线索二叉树一般用于实现更高效的中序遍历,所以规定线索化算法要顺便找到\(head,tail\)节点。下面给出中序线索化步骤,设树根为\(root\):

1) 创建全局变量\(prev=null,head=null,tail=null\);

2) 定义递归函数\(F(node)\)如下:

2.1) 若\(node\)为空则退出递归;

2.2) 递归\(F(node.left)\);

2.3) 若\(node.left\)为空则置\(node.leftType=1,node.left=prev\),否则置\(node.leftType=0\);

2.4) 若\(prev\)非空,此时若\(prev.right\)为空则置\(prev.rightType=1,prev.right=node\)否则置\(prev.rightType=0\);

2.5) 若\(prev\)为空则置\(head=node\);

2.6) 置\(prev=node\);

2.7) 递归\(F(node.right)\);

3) 执行\(F(root)\);

4) 若\(prev\)非空则置\(prev.rightType=1\);

5) 置\(tail=prev\);

上述步骤的核心是\(F\),(2.3)到(2.6)对应递归中序遍历的“访问\(node\)的时刻”。(2.3)显然不影响遍历次序,(2.4)仅在\(prev.right\)为空时修改\(prev.right\),若\(prev.right\)为空则说明此时\(prev.right\)的子递归早已直接退出,此时修改\(prev.right\)不影响遍历次序,既然(2.3)(2.4)不影响遍历次序,(2.6)就能在每个“访问\(node\)的时刻”保证\(prev\)指向“上次访问的\(node\)”,步骤(2.5)则单纯用于得到正确的\(head\)。\(F(root)\)结束时\(prev\)指向遍历最后“访问”的节点,其无右子树且右孩子指针未“线索化”,所以额外置\(prev.rightType=1\)。最后置\(tail\)为\(prev\)。其代价和额外空间与递归中序遍历相同。

结合之前文章对“普通二叉树节点的中序前驱后继”的讨论,中序线索二叉树任意节点\(x\)的中序前驱后继满足:

b) 若\(x.right\)非线索则\(x\)的中序后继是\(x\)右子树的中序序列首节点,若\(x.left\)非线索则\(x\)的中序前驱是\(x\)左子树的中序序列末节点。若\(x.right\)为线索则\(x\)的中序后继是\(x.right\)(为空无后继),若\(x.left\)为线索则\(x\)的中序前驱是\(x.left\)(为空无前驱);

基于上述性质和线索化时得到的\(head,tail\),就能通过迭代的寻找\(head\)的中序后继\(tail\)的中序前驱实现遍历逆序遍历。之前的文章讨论过普通二叉树如何借助父亲指针实现迭代遍历,而这两种遍历的唯一区别是后者“通过回溯父亲节点找到后继”的分支被替换为“通过线索直接找到后继”,所以该遍历的代价也是\(O(n)\)且隐藏常数更小。逆序遍历与之对称代价相同。中序线索化基于该线索的中序遍历的程序如下,其中线索化程序输入的二叉树是只有左右孩子指针的普通二叉树,这里利用Python的可动态修改对象的特性直接在节点上加了\(leftType,rightType\)对象属性(变量)。

 

前序线索二叉树

二叉树前序序列首节点\(head\)可能有左子树末节点\(tail\)一定无右子树。将其前序线索化后,\(head.left\)是空线索当且仅当\(head\)在原二叉树中无左子树,\(tail.right\)是空线索。利用和中序线索化相同的思路,可得前序线索化步骤,设树根为\(root\):

1) 创建全局变量\(prev=null,head=null,tail=null\);

2) 定义递归函数\(F(node)\)如下:

2.1) 若\(node\)为空则退出递归;

2.2) 若\(node.left\)为空则置\(node.leftType=1,node.left=prev\),否则置\(node.leftType=0\);

2.3) 若\(node.right\)为空则置\(node.rightType=1\),否则置\(node.rightType=0\);

2.4) 若\(prev\)非空且\(prev.rightType=1\)则置\(prev.right=node\);

2.5) 置\(prev=node\);

2.6) 若\(node.leftType=0\),则递归\(F(node.left)\);

2.7) 若\(node.rightType=0\),则递归\(F(node.right)\);

3) 执行\(F(root)\);

4) 置\(tail=prev, head=root\);

上述步骤和中序线索化不太一样,因为照搬中序线索化步骤会破坏前序遍历的次序。其分2步完成,第1步的(2.2)和(2.3)是确定\(node\)的左孩子指针是否为线索并确定其指向,确定\(node\)的右孩子指针是否为线索但暂不确定其指向,此时不知道其后继是什么。然后在第2步的(2.4)才确定“上次遍历到的\(node\)”(即当前\(prev\))的右孩子指针的指向。最后(2.6)和(2.7)为避免对线索执行子递归,需预先判断是否为线索,如果是线索则说明对应子树为空,无需递归。另外上述步骤无需像中序线索化那样在递归结束时额外置\(prev.rightType=1\),因为在\(node\)中已经处理过。其代价和额外空间与递归前序遍历相同。

结合之前文章对“普通二叉树节点的前序前驱后继”的讨论,前序线索二叉树任意节点\(x\)的前序前驱后继满足:

c) 若\(x.left\)非线索则\(x\)的前序后继是\(x.left\),否则\(x\)的前序后继是\(x.right\)(无论\(x.right\)是否为线索是否为空)。注意到在\(x.left\)非线索时,寻找\(x\)的前序前驱只能通过回溯\(x\)的父亲节点来完成;

上述性质表明通过线索很容易实现前序遍历,但无法在不借助父亲节点的情况下实现逆序前序遍历,所以前序线索二叉树的逆序前序遍历意义不太大。下面的程序实现了前序线索化和基于该线索的遍历,遍历的代价也是\(O(n)\)。

 

后序线索二叉树

二叉树后序序列首节点\(head\)一定无左子树末节点\(tail\)可能有右子树。将其后序线索化后,\(head.left\)是空线索,\(tail.right\)是空线索当且仅当\(tail\)在原二叉树中无右子树。利用和前序线索化相同的思路,可得后序线索化步骤,设树根为\(root\):

1) 创建全局变量\(prev=null,head=null,tail=null\);

2) 定义递归函数\(F(node)\)如下:

2.1) 若\(node\)为空则退出递归;

2.2) 递归\(F(node.left)\);

2.3) 递归\(F(node.right)\);

2.4) 若\(node.left\)为空则置\(node.leftType=1,node.left=prev\),否则置\(node.leftType=0\);

2.5) 若\(node.right\)为空则置\(node.rightType=1\),否则置\(node.rightType=0\);

2.6) 若\(prev\)非空且\(prev.rightType=1\)则置\(prev.right=node\);

2.7) 若\(prev\)为空则置\(head=node\);

2.8) 置\(prev=node\);

3) 执行\(F(root)\);

4) 置\(tail=root\);

上述步骤基本照搬了前序线索化的步骤,由于递归左右孩子在“访问当前节点”前发生,故递归前无需像前序线索化那样先做判断。多出来的步骤(2.7)用于将\(head\)指向后序序列的首节点。

结合之前文章对“普通二叉树节点的后序前驱后继”的讨论,后序线索二叉树任意节点\(x\)的后序前驱后继满足:

d) 若\(x.right\)非线索则\(x\)的后序前驱是\(x.left\),否则\(x\)的后序前驱是\(x.left\)(无论\(x.left\)是否为线索是否为空)。注意到在\(x.right\)非线索时,寻找\(x\)的后序后继只能通过回溯\(x\)的父亲节点来完成;

上述性质表明后序线索和前序线索存在对称性,通过线索很容实现逆序后序遍历,但无法在不借助父亲节点的情况下实现正序后序遍历,所以其正序后序遍历也意义不太大。下面给出相关程序实现,遍历的代价也是\(O(n)\)。

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部