线索二叉树概述

  二叉树虽然是非线性结构,但二叉树的遍历却为二又树的结点集导出了一个线性序列。希望很快找到某一结点的前驱或后继,但不希望每次都要对二叉树遍历一遍,这就需要把每个结点的前驱和后继信息记录下来。为了做到这一点,可在原来的二叉链表中增加一个前驱指针域(pred)和一个后继指针域(succ),分别指向该结点在某种次序下的前驱结点和后继结点。
  以中序遍历为例:
线索二叉树的详细实现(C++)-LMLPHP

  有许多指针是空指针又没有利用。为了不浪费存储空间,利用空的leftChild域存放结点的前驱结点指针,利用空的rightChild域存放结点的后继结点指针。
  为了区别线索和子女指针,在每个结点中设置两个标志ltag和rtag。以中序线索二叉树为例,如果ltag==0,标明leftChild域中存放的是指向左子女结点的指针,否则leftChild域中是指向该结点中序下的前驱的线索;如果rtag==0,标明rightChild域中存放的是指向右子女结点的指针,否则rightChild域中是指向该结点中序下的后继的线索。
由于它们只需占用一个二进位,每个结点所需存储空间节省得多。
 线索二叉树的详细实现(C++)-LMLPHP

寻找当前结点在中序下的后继

 线索二叉树的详细实现(C++)-LMLPHP

寻找当前结点在中序序列下的前驱

 线索二叉树的详细实现(C++)-LMLPHP

线索二叉树的结点类

 //线索二叉树结点类
template<typename T>
struct ThreadNode //结点类
{
int ltag, rtag; //左右子树标志位
ThreadNode<T> *leftChild, *rightChild; //左孩子和右孩子
T data; //结点存储的值
ThreadNode(const T item) :data(item), leftChild(NULL), rightChild(NULL), ltag(), rtag() {} //结点类的构造函数
};

线索二叉树的创建

  对一个已存在的二又树按中序遍历进行线索化的算法中用到了一个指针pre,它在遍历过程中总是指向遍历指针p的中序下的前驱结点,即在中序遍历过程中刚刚访问过的结点。在做中序遍历时,只要一遇到空指针域,立即填入前驱或后继线索。

     //使用前序遍历创建二叉树(未线索化)
void CreateTree(ThreadNode<T>* &subTree)
{
T item;
if (cin >> item)
{
if (item != RefValue)
{
subTree = new ThreadNode<T>(item); //构造结点
if (subTree == NULL)
{
cout << "空间分配错误!" << endl;
exit();
}
CreateTree(subTree->leftChild); //递归创建左子树
CreateTree(subTree->rightChild); //递归创建右子树
}
else
{
subTree == NULL;
}
}
} //中序遍历对二叉树进行线索化
void createInThread(ThreadNode<T> *current, ThreadNode<T> * &pre)
{
if (current == NULL)
{
return;
}
createInThread(current->leftChild, pre); //递归左子树的线索化
if (current->leftChild == NULL) //建立当前结点的前驱结点
{
current->leftChild = pre;
current->ltag = ;
}
if (pre != NULL&&pre->rightChild == NULL) //建立当前结点的后继结点
{
pre->rightChild = current;
pre->rtag = ;
}
pre = current; //用前驱记住当前的结点
createInThread(current->rightChild, pre); //递归右子树的线索化
} //中序遍历对创建好的普通二叉树进行中序线索化
void CreateInThread()
{
ThreadNode<T> *pre = NULL; //第一个结点的左子树置为NULL
if (root != NULL) {
createInThread(root, pre);
//处理中序遍历的最后一个结点,最后一个结点的右子树置为空
pre->rightChild = NULL;
pre->rtag = ;
}
}

中序线索化二叉树的成员函数

     //寻找中序下第一个结点
ThreadNode<T> * First(ThreadNode<T> *current)
//返回以*current为根的中序线索二叉树中序遍历的第一个结点
{
ThreadNode<T> *p = current;
while (p->ltag == )
{
p = p->leftChild; //循环找到最左下角结点
}
return p;
} //寻找中序下的后继结点
ThreadNode<T>* Next(ThreadNode<T>* current)
{
ThreadNode<T>* p = current->rightChild;
if(current->rtag==)
{
return First(p);
}
else
{
return p;
}
} //寻找中序下最后一个结点
ThreadNode<T> * Last(ThreadNode<T> *current)
//返回以*current为根的中序线索二叉树中序遍历的最后一个结点
{
ThreadNode<T> *p = current;
while (p->rtag==)
{
p = p->rightChild;
}
return p;
} //寻找结点在中序下的前驱结点
ThreadNode<T>* Prior(ThreadNode<T>* current)
{
ThreadNode<T>* p = current->leftChild;
if (current->ltag==)
{
return Last(p);
}
else
{
return p;
}
}

中序线索化二叉树上执行中序遍历的算法

  先利用First()找到二又树在中序序列下的第一个结占,抑它作为当前结点,然后利用求后继结点的运算Next()按中序次序逐个访问,直到二叉树的最后一个结点。

     //中序线索化二叉树上执行中序遍历的算法
void InOrder(ThreadNode<T>* p)
{
for (p=First(root);p!=NULL;p=Next(p))
{
cout << p->data<<" ";
}
cout << endl;
}

中序线索化二叉树上实现前序遍历的算法

  前序序列中的第一个结点即二又树的根,因此从根结点开始前序遍历二叉树。若当前结点有左子女,则前序下的后继结点即为左子女结点,否则,若当前结点有右子女,则前序后继即为右子女结点。对于叶结点,则沿着中序后继线索走到一个有右子女结点的结点,这个右子女结点就是当前结点的前序后继结点。

     void PreOrder(ThreadNode<T>* p)
{
while (p!=NULL)
{
cout << p->data<<" "; //先访问根节点
if (p->ltag==)
{
p = p->leftChild; //有左子树,即为后继
}
else if(p->rtag==) //否则,有右子树,即为后继
{
p = p->rightChild;
}
else //无左右子树
{
while (p!=NULL&&p->rtag==) //检测后继线索
{
p = p->rightChild; //直到找到有右子树的结点
}
if (p!=NULL)
{
p = p->rightChild; //该结点的右子树为后继
}
}
}
cout << endl;
}

中序线索化二叉树后序遍历的算法

  首先从根结点出发,寻找在后序序列中的第一个结点。寻找的方法是从根出发,沿着左子女链一直找下去,找到左子女不再是左子女指针的结点,再找到该结点的右子女,在以此结点为根的子树上再重复上述过程,直到叶结点为止。接着,从此结点开始后序遍历中序线索二又树。在遍历过程中,每次都先找到当前结点的父结点,如果当前结点是父结点的右子女,或者虽然当前结点是父结点的左子女,但这个父结点没有右子女,则后序下的后继即为该父结点;否则,在当前结点的右子树(如果存在)上重复执行上面的操作。这种后序遍历过程必须搜寻父结点,并确定当前结点与其父结点的关系,即是左子女还是右子女。

     //中序线索二叉树的后序遍历算法
void PostOrder(ThreadNode<T>* p)
{
ThreadNode<T>* t = p;
while (t->ltag==||t->rtag==) //寻找后续第一个结点
{
if(t->ltag==)
{
t = t->leftChild;
}
else if(t->rtag==)
{
t = t->rightChild;
}
}
cout << t->data<<" "; //访问第一个结点
while ((p=Parent(t))!=NULL) //每次都先找到当前结点的父结点
{
//若当前结点是父节点的右子树或者当前结点是左子树,但是这个父节点没有右子树,则后续下的后继为改父节点
if (p->rightChild==t||p->rtag==)
{
t = p;
}
//否则,在当前结点的右子树(如果存在)上重复执行上面的操作
else
{
t = p->rightChild;
while (t->ltag==||t->rtag==)
{
if (t->ltag==)
{
t = t->leftChild;
}
else if (t->rtag==)
{
t = t->rightChild;
}
}
}
cout << t->data << " ";
}
}

在中序线索二叉树中求父节点

  中序线索化二叉树后序遍历的算法中用到了求父节点的算法,程序中包括两条查找父结点的路径。第一种选择是从当前结点走到树上层的一个中序前驱(不一定是直接前驱),然后向右下找父结点。第二种选择是从当前结点走到树上层的一个中序后继(不一定是直接后继),然后向左下找父结点。以下通过一个具体的例子来说明为什么不可以只采用一种方法。
 线索二叉树的详细实现(C++)-LMLPHP

  例如上图寻找结点’*’的父结点的两条路径。一条路径是从结点’*’沿左子女链走到’b',然后顺中序前驱线索走到’+’,而’+”就是’*’的父结点。另一条路径是从结点’*’沿右子女链走到’d',然后顺中序后继线索走到’一’,再向左子女方向走到结点’+’,找到结点’*’的父结点。对于此例,无论第一条路径还是第二条路径都可以找到父结点。但情况不总是这样。例如:在找结点’+’的父结点,从’+’沿左子女链将走到结点’a',而’a'无中序前驱线索,因此这条路径失败,但通过另一条路径找到了结点’+”的父结点’一’。说明了只采用一种方法是不行的。
  程序实现是先试探第一条路径,如果走到中序序列的第一个结点而告失败,则改换后一条路径寻找父结点。只有找根结点的父结点,这两种方法才都会失败。因为从根结点沿左子女链一定走到中序序列的第一个结点,沿右子女链一定走到中序序列的最后一个结点。然而,根结点根本就无父结点,所以这种特例在开始就排除了。

 //在中序线索化二叉树中求父节点
ThreadNode<T>* Parent(ThreadNode<T>* t)
{
ThreadNode<T>* p;
if(t==root) //根节点无父节点
{
return NULL;
}
for (p = t; p->ltag == ; p = p->leftChild); //求*t为根的中序下的第一个结点p
//情况1
if (p->leftChild!=NULL) //当p左子树指向不为空
{
//令p为p的左子树指向的结点,判断此结点是否并且此节点的左右子树结点的指向都不为t,再将p为p的右孩子结点
for (p = p->leftChild; p != NULL&&p->leftChild != t&&p->rightChild != t; p = p->rightChild);
}
//情况2
//如果上面的循环完了,由于是p==NULL结束的循环,没有找到与t相等的结点,就是一直找到了中序线索化的第一个结点了,这时候这种就要用到情况2的方法
if (p==NULL||p->leftChild==NULL)
{
//找到*t为根的中序下的最后一个结点
for (p = t; p->rtag == ; p = p->rightChild);
//让后让他指向最后一个结点指向的结点,从这个结点开始,以此判断它的左孩子孩子和右孩子是否和t相等
for (p = p->rightChild; p != NULL&&p->leftChild != t&&p->rightChild != t; p = p->leftChild);
}
return p;
}

完整代码

线索二叉树

 //线索二叉树
template<typename T>
struct ThreadNode //结点类
{
int ltag, rtag; //左右子树标志位
ThreadNode<T> *leftChild, *rightChild; //左孩子和右孩子
T data; //结点存储的值
ThreadNode(const T item) :data(item), leftChild(NULL), rightChild(NULL), ltag(), rtag() {} //结点类的构造函数
}; template<typename T>
class ThreadTree
{ public:
//构造函数(普通)
ThreadTree() :root(NULL) {} //指定结束标志RefValue的构造函数
ThreadTree(T value) :RefValue(value), root(NULL) {} //使用前序遍历创建二叉树(未线索化)
void CreateTree() { CreateTree(root); } //中序遍历对创建好的普通二叉树进行中序线索化
void CreateInThread()
{
ThreadNode<T> *pre = NULL; //第一个结点的左子树置为NULL
if (root != NULL) {
createInThread(root, pre);
//处理中序遍历的最后一个结点,最后一个结点的右子树置为空
pre->rightChild = NULL;
pre->rtag = ;
}
}
//线索化二叉树上执行中序遍历的算法
void InOrder() { InOrder(root); }
//中序线索化二叉树上实现前序遍历的算法
void PreOrder() { PreOrder(root); }
//中序线索二叉树的后序遍历算法
void PostOrder() { PostOrder(root); }
private:
//使用前序遍历创建二叉树(未线索化)
void CreateTree(ThreadNode<T>* &subTree)
{
T item;
if (cin >> item)
{
if (item != RefValue)
{
subTree = new ThreadNode<T>(item); //构造结点
if (subTree == NULL)
{
cout << "空间分配错误!" << endl;
exit();
}
CreateTree(subTree->leftChild); //递归创建左子树
CreateTree(subTree->rightChild); //递归创建右子树
}
else
{
subTree == NULL;
}
}
}
//中序遍历对二叉树进行线索化
void createInThread(ThreadNode<T> *current, ThreadNode<T> * &pre)
{
if (current == NULL)
{
return;
}
createInThread(current->leftChild, pre); //递归左子树的线索化
if (current->leftChild == NULL) //建立当前结点的前驱结点
{
current->leftChild = pre;
current->ltag = ;
}
if (pre != NULL&&pre->rightChild == NULL) //建立当前结点的后继结点
{
pre->rightChild = current;
pre->rtag = ;
}
pre = current; //用前驱记住当前的结点
createInThread(current->rightChild, pre); //递归右子树的线索化
} //寻找中序下第一个结点
ThreadNode<T> * First(ThreadNode<T> *current) //返回以*current为根的中序线索二叉树中序遍历的第一个结点
{
ThreadNode<T> *p = current;
while (p->ltag == )
{
p = p->leftChild; //循环找到最左下角结点
}
return p;
} //寻找中序下的后继结点
ThreadNode<T>* Next(ThreadNode<T>* current)
{
ThreadNode<T>* p = current->rightChild;
if(current->rtag==)
{
return First(p);
}
else
{
return p;
}
} //寻找中序下最后一个结点
ThreadNode<T> * Last(ThreadNode<T> *current) //返回以*current为根的中序线索二叉树中序遍历的最后一个结点
{
ThreadNode<T> *p = current;
while (p->rtag==)
{
p = p->rightChild;
}
return p;
}
//寻找结点在中序下的前驱结点
ThreadNode<T>* Prior(ThreadNode<T>* current)
{
ThreadNode<T>* p = current->leftChild;
if (current->ltag==)
{
return Last(p);
}
else
{
return p;
}
}
//在中序线索化二叉树中求父节点
ThreadNode<T>* Parent(ThreadNode<T>* t)
{
ThreadNode<T>* p;
if(t==root) //根节点无父节点
{
return NULL;
}
for (p = t; p->ltag == ; p = p->leftChild); //求*t为根的中序下的第一个结点p
//情况1
if (p->leftChild!=NULL) //当p左子树指向不为空
{
//令p为p的左子树指向的结点,判断此结点是否并且此节点的左右子树结点的指向都不为t,再将p为p的右孩子结点
for (p = p->leftChild; p != NULL&&p->leftChild != t&&p->rightChild != t; p = p->rightChild);
}
//情况2
//如果上面的循环完了,由于是p==NULL结束的循环,没有找到与t相等的结点,就是一直找到了中序线索化的第一个结点了,这时候这种就要用到情况2的方法
if (p==NULL||p->leftChild==NULL)
{
//找到*t为根的中序下的最后一个结点
for (p = t; p->rtag == ; p = p->rightChild);
//让后让他指向最后一个结点指向的结点,从这个结点开始,以此判断它的左孩子孩子和右孩子是否和t相等
for (p = p->rightChild; p != NULL&&p->leftChild != t&&p->rightChild != t; p = p->leftChild);
}
return p;
} //中序线索化二叉树上执行中序遍历的算法
void InOrder(ThreadNode<T>* p)
{
for (p=First(root);p!=NULL;p=Next(p))
{
cout << p->data<<" ";
}
cout << endl;
}
//中序线索化二叉树上实现前序遍历的算法
void PreOrder(ThreadNode<T>* p)
{
while (p!=NULL)
{
cout << p->data<<" "; //先访问根节点
if (p->ltag==)
{
p = p->leftChild; //有左子树,即为后继
}
else if(p->rtag==) //否则,有右子树,即为后继
{
p = p->rightChild;
}
else //无左右子树
{
while (p!=NULL&&p->rtag==) //检测后继线索
{
p = p->rightChild; //直到找到有右子树的结点
}
if (p!=NULL)
{
p = p->rightChild; //该结点的右子树为后继
}
}
}
cout << endl;
}
//中序线索二叉树的后序遍历算法
void PostOrder(ThreadNode<T>* p)
{
ThreadNode<T>* t = p;
while (t->ltag==||t->rtag==) //寻找后续第一个结点
{
if(t->ltag==)
{
t = t->leftChild;
}
else if(t->rtag==)
{
t = t->rightChild;
}
}
cout << t->data<<" "; //访问第一个结点
while ((p=Parent(t))!=NULL) //每次都先找到当前结点的父结点
{
//若当前结点是父节点的右子树或者当前结点是左子树,但是这个父节点没有右子树,则后续下的后继为改父节点
if (p->rightChild==t||p->rtag==)
{
t = p;
}
//否则,在当前结点的右子树(如果存在)上重复执行上面的操作
else
{
t = p->rightChild;
while (t->ltag==||t->rtag==)
{
if (t->ltag==)
{
t = t->leftChild;
}
else if (t->rtag==)
{
t = t->rightChild;
}
}
}
cout << t->data << " ";
}
} private:
//树的根节点
ThreadNode<T> *root;
T RefValue;
};

测试函数

主函数

 int main(int argc, char* argv[])
{
//abc##de#g##f###
ThreadTree<char> tree('#');
tree.CreateTree();
tree.CreateInThread();
tree.InOrder();
tree.PreOrder();
tree.PostOrder();
}

05-11 22:21