概述
1,什么是线索二叉树?
用自己的话说就是:原来我们求一棵二叉树的前序、中序、后序序列的时候,都必须用到递归遍历相应的二叉树,否则也得借助栈等结构来记录。这样的话,如果我们想很快的找到某一个节点在某种序列下的前驱或后继,每次都要遍历,这显然十分浪费时间。很自然,要是想避免这个重复工作,那么我们就需要把所有节点的前驱和后继记录下来,这样每次查找相应的记录就行了。于是就引出了——‘线索’一词。没错,真的是‘线索’,由线索找到相应节点的前驱和后继,很形象!
2,怎么用线索实现?
这就涉及到两块内容,“建立二叉树的时候装上线索”和“遍历二叉树的时候找到线索”。这两个主要问题解决了,其他在线索二叉树上的操作便显得顺理成章。线索是什么?只要能让我们通过他找到前驱和后继就行,管他是指针、表格、普通数据……,例如用指针,我们开始的时候把每个节点的前驱和后继(前驱和后继指的是在中序、前序或后序的序列中某个节点的前一个和后一个,与左右孩子区分!)用一个个指针都给存储好,然后把相应的指针绑定在相应的节点上,这样想要哪个节点的前驱和后继直接在记录中取就可以了!
初步想法: 原来的二叉树节点都是由:数据域+左孩子指针+右孩子指针构成,那么我们再加两个指针,分别是前驱指针和后继指针不就可以了吗。(与做孩子有孩子实现类似)就像下图展示的那样:
存在的问题:没错,这很自然,我们用另外两个指针’前驱‘和’后继‘绑定在每个节点上来记录此节点的前驱个后继。但是这个时候不妨暂且停一停。我好像折本了!找前驱和后继的确方便找到了,但是我却又开辟了两个指针域,这显然是用空间换取时间的买卖,没什么便宜可赚!
改进办法:不过我注意到不是每个节点都有左右孩子指针的,也就是说左右孩子指针有时候是空闲的,不用白不用,那就把空间充分利用起来吧。约定:前驱装到空闲的(只有空闲的才敢用!)leftChild,后继装到空闲的rightChild里。(你可能有疑惑,接着往下看!)但是这样又会有问题,我怎么知道你的孩子指针是不是空闲的,所以我们只能做出让步,加两个bool(或int)型的标志位(毕竟要比复杂类型的指针占空间少!),当tag 是0代表装的孩子指针,是1代表装的前驱或后继指针,借以判断当前节点的左右孩子释放空闲。那么我们继续改进的如下图:
这样我们对每个线索二叉树的节点结构就设计完成了!下面通过一个简单的线索二叉树例子整体上理解,为设计代码做好准备。
额……原谅我用画图的随手涂鸦,不过相信你可以看得懂!
部分解释:
1,这个简单的二叉树的中序序列(这里我们拿中序线索二叉树举例)是:BDAEC,所以图中用相应的箭头标定了他们的前驱与后继,因为B没有前驱,C没有后继,所以相应的指针域是nullptr。
2,只有tag是1的时候才可以用来标定前驱和后继,是0的时候正常指向自己的左右孩子。
好了,下面解决刚才那个疑惑,如果不是1(也就是对应的节点不是空闲的)相应的前驱和后继应该怎么的到呢?下面看两个表格:
这张表很重要,前面所有的废话都蕴含在这张表里面了!希望你能看懂他!
表中解释了当不空闲的时候,也就是我们的前驱或后继指针没地放存放的时候,我们应该经过怎样的运算才可以依旧准确的找到相应节点的前驱和后继。
简单解释:
例如在节点图中,虽然B的右孩子节点不是空闲的(没有存储到B后继节点的信息),但是我们可以确定B的后继一定是B的所有右子树中中序下第一个节点(也就是D)。找C的前驱的过程与之类似。因为图示二叉树相对简单,不能显示出这一表格的强大,你可以写一个稍微复杂的二叉树自行测试!)
书写代码
关键代码解释:
对原有的二叉树进行线索化函数如下:
template<class T>
void ThreadBinaryTree<T>::createInThread() {
//利用中序遍历对二叉树中序线索化
ThreadNode<T>* pre = nullptr; //前驱节点指针
if (root != nullptr) { //非空二叉树进行线索化
createInThread(root, pre); //调用中序遍历线索化二叉树函数
pre->rightChild = nullptr; //处理最后一个节点
pre->rtag = 1;
}
}
template<class T>
void ThreadBinaryTree<T>::createInThread(ThreadNode<T>* current,
ThreadNode<T>* &pre) {
//通过中序遍历对二叉树进行线索化
if (nullptr == current) {
return;
}
createInThread(current->leftChild, pre); //递归左子树进行线索化
if (nullptr == current->leftChild) { //建立当前节点的前驱线索
current->leftChild = pre;
current->ltag = 1;
}
if (pre != nullptr && nullptr == pre->rightChild) { //建立当前节点的后继线索
pre->rightChild = current;
pre->rtag = 1;
}
pre = current; //前驱跟上,当前指针向前遍历
createInThread(current->rightChild, pre); //递归右子树线索化
}
上面是个重载函数,其中第二个函数中运用了递归,第一个函数只是对根检查是否是nullptr,和做了最后一个节点的收尾,真正的线索化的实现是在函数2当中。
其中使用了一个指针pre,他在遍历过程中总是指向遍历指针current在中序下的前驱节点,即在中序遍历过程中刚刚访问过的节点,在做中序遍历时,只要一遇到空闲指针域就立即填入前驱或后继线索。
遍历的实现函数
template<class T>
ThreadNode<T>* ThreadBinaryTree<T>::First(ThreadNode<T>* current) {
//函数返回以current为根的中序线索二叉树中中序序列下的第一个节点
ThreadNode<T>* ptr = current;
while (0 == ptr->ltag) {
ptr = ptr->leftChild;
}
return ptr;
}
template<class T>
ThreadNode<T>* ThreadBinaryTree<T>::Next(ThreadNode<T>* current) {
//函数返回在中序线索二叉树中节点current在中序下的后继节点
ThreadNode<T>* ptr = current->rightChild;
if (0 == current->rtag) {
return First(ptr);
}
else {
return ptr;
}
}
template<class T>
void ThreadBinaryTree<T>::InOrderTreeByThread() {
//运用线索中序遍历一棵二叉树
ThreadNode<T>* ptr;
//通过循环初始化定位到中序的第一个节点,然后循环调用寻找后继的函数
for (ptr = First(root); ptr != nullptr; ptr = Next(ptr)) {
std::cout << ptr->data;
}
}
前两个函数的相互配合实现中序顺序下节点的定位,并在最后一个函数中进行数据域的输出。注释中对函数的功能已经阐述,只要你理解了上面的理论(尤其是张表格!),看懂这几个函数并不是难事。而我多说无益~
下面 是完整的代码及测试主函数(调试编译器:Visual Studio 2013)
/*
*中序线索二叉树类定义
*/
#include <iostream>
char data[100]; //建立二叉树的数据
int cnt; //建立二叉树的计数器
template<class T>
struct ThreadNode { //线索二叉树节点结构
int ltag, rtag; //线索标志
ThreadNode<T> *leftChild, *rightChild; //左右孩子指针
T data; //节点数据域
ThreadNode(const T item) : data(item), leftChild(nullptr),
rightChild(nullptr), ltag(0), rtag(0)
{}
};
template<class T>
class ThreadBinaryTree {
protected:
ThreadNode<T> *root;
char defaultEndChar;
//中序遍历建立线索二叉树
void createInThread(ThreadNode<T> *current, ThreadNode<T> *&pre);
ThreadNode<T> * parent(ThreadNode<T> * t); //寻找节点t的父节点
public:
ThreadBinaryTree() : root(nullptr), defaultEndChar('#') {} //构造函数
void createInThread(); //建立中序线索二叉树
ThreadNode<T>* First(ThreadNode<T>* current); //寻找中序下第一个节点
ThreadNode<T>* Last(ThreadNode<T>* current); //寻找中序下最后一个节点
ThreadNode<T>* Next(ThreadNode<T>* current); //寻找节点在中序下的后继节点
ThreadNode<T>* Prior(ThreadNode<T>* current); //寻找节点在中序下的前驱节点
void InOrder(void(* visit)(ThreadNode<T> *p)); //中序遍历
void PreOrder(void(*visit)(ThreadNode<T> *p)); //前序遍历
void PostOrder(void(*visit)(ThreadNode<T> *p)); //后序遍历
void InOrderTreeByThread(); //线索二叉树的中序遍历
void CreateBinaryTreeByInOrder(ThreadNode<T>* ¤t); //前序递归建立一棵二叉树
void CreateBinaryTreeByInOrderEx(ThreadNode<T>* ¤t); //前序递归建立一棵二叉树扩展
ThreadNode<T>* GetRoot(); //取根函数
void ModifyRoot(ThreadNode<T>* ptr); //修改根函数
void PrintBinaryTreeByInOrder(ThreadNode<T>* root); //通过中序打印二叉树
};
//函数定义
template<class T>
ThreadNode<T>* ThreadBinaryTree<T>::First(ThreadNode<T>* current) {
//函数返回以current为根的中序线索二叉树中中序序列下的第一个节点
ThreadNode<T>* ptr = current;
while (0 == ptr->ltag) {
ptr = ptr->leftChild;
}
return ptr;
}
template<class T>
ThreadNode<T>* ThreadBinaryTree<T>::Next(ThreadNode<T>* current) {
//函数返回在中序线索二叉树中节点current在中序下的后继节点
ThreadNode<T>* ptr = current->rightChild;
if (0 == current->rtag) {
return First(ptr);
}
else {
return ptr;
}
}
template<class T>
ThreadNode<T>* ThreadBinaryTree<T>::Last(ThreadNode<T>* current) {
//函数返回以current为根的中序线索二叉树中中序序列下的最后一个节点
ThreadNode<T>* ptr = current;
if (0 == ptr->rtag) {
ptr = ptr->rightChild;
}
return ptr;
}
template<class T>
ThreadNode<T>* ThreadBinaryTree<T>::Prior(ThreadNode<T>* current) {
//函数返回中序线索二叉树中节点current在中序下的前驱节点
ThreadNode<T>* ptr = current->leftChild;
if (0 == ptr->ltag) {
return Last(ptr);
}
else {
return ptr;
}
}
template<class T>
void ThreadBinaryTree<T>::InOrderTreeByThread() {
//运用线索中序遍历一棵二叉树
ThreadNode<T>* ptr;
//通过循环初始化定位到中序的第一个节点,然后循环调用寻找后继的函数
for (ptr = First(root); ptr != nullptr; ptr = Next(ptr)) {
std::cout << ptr->data;
}
}
template<class T>
void ThreadBinaryTree<T>::createInThread() {
//利用中序遍历对二叉树中序线索化
ThreadNode<T>* pre = nullptr; //前驱节点指针
if (root != nullptr) { //非空二叉树进行线索化
createInThread(root, pre); //调用中序遍历线索化二叉树函数
pre->rightChild = nullptr; //处理最后一个节点
pre->rtag = 1;
}
}
template<class T>
void ThreadBinaryTree<T>::createInThread(ThreadNode<T>* current,
ThreadNode<T>* &pre) {
//通过中序遍历对二叉树进行线索化
if (nullptr == current) {
return;
}
createInThread(current->leftChild, pre); //递归左子树进行线索化
if (nullptr == current->leftChild) { //建立当前节点的前驱线索
current->leftChild = pre;
current->ltag = 1;
}
if (pre != nullptr && nullptr == pre->rightChild) { //建立当前节点的后继线索
pre->rightChild = current;
pre->rtag = 1;
}
pre = current; //前驱跟上,当前指针向前遍历
createInThread(current->rightChild, pre); //递归右子树线索化
}
template<class T>
void ThreadBinaryTree<T>::CreateBinaryTreeByInOrder(
ThreadNode<T>* ¤t) {
char element = data[cnt++];
//通过递归算法前序建立一棵二叉树
if (',' == element){
current = nullptr;
}
else {
current = new ThreadNode<T>(element);
CreateBinaryTreeByInOrder(current->leftChild);
CreateBinaryTreeByInOrder(current->rightChild);
}
}
template<class T>
void ThreadBinaryTree<T>::CreateBinaryTreeByInOrderEx(
ThreadNode<T>* ¤t) {
//递归前序建立二叉树,包含输入数据
char item;
if (std::cin >> item) {
if (item != defaultEndChar) {
current = new ThreadNode<T>(item);
if (nullptr == current) {
std::cerr << "内存分配错误!" << std::endl;
exit(1);
}
CreateBinaryTreeByInOrderEx(current->leftChild); //递归建立左子树
CreateBinaryTreeByInOrderEx(current->rightChild); //递归建立右子树
}
else {
current = nullptr;
}
}
}
template<class T>
ThreadNode<T>* ThreadBinaryTree<T>::GetRoot() {
//返回二叉树的根指针
return root;
}
template<class T>
void ThreadBinaryTree<T>::ModifyRoot(ThreadNode<T>* ptr) {
//将ptr赋值给当前的根
root = ptr;
}
template<class T>
void ThreadBinaryTree<T>::PrintBinaryTreeByInOrder(
ThreadNode<T>* root) {
//通过递归算法,中序打印以root为根节点的二叉树
if (root != nullptr) {
PrintBinaryTreeByInOrder(root->leftChild); //递归打印左子树
std::cout << root->data;
PrintBinaryTreeByInOrder(root->rightChild); //递归打印右子树
}
}
测试主函数:(对两种遍历方式做了对比)
#include "ThreadBinaryTree.h" //上面的代码包含在此头文件中
int main()
{
std::cin >> data;
ThreadBinaryTree<char> tree;
ThreadNode<char>* root;
tree.CreateBinaryTreeByInOrder(root);
tree.ModifyRoot(root);
std::cout << "递归算法中序输出:";
tree.PrintBinaryTreeByInOrder(tree.GetRoot());
std::cout << std::endl;
tree.createInThread(); //线索化
std::cout << "线索化方式中序输出:";
tree.InOrderTreeByThread(); //线索化输出
std::cout << std::endl;
system("pause");
return 0;
}
水平毕竟有限,代码不够简洁,希望通过与大家一块交流改进,线索二叉树的前期内容就那么多,后面还有对线索二叉树的插入删除等操作。代码不是那么容易理解,但是自己理解绝对比他人讲给你更有价值!
参考教材:数据结构C++实现殷人昆著作