首先提出一个问题 为什么要线索化二叉树?
二叉树遍历的本质上是将一个复杂的非线性结构转换为线性结构,使每个节点都有了唯一前驱和后继(第一个节点无前驱,最后一个节点无后继)。对于一个节点,查找其左右子树是方便的,但是查找其前驱和后继只能在遍历中找到。
线索化二叉树解决了无法直接找到该节点在某种遍历序列中的前驱和后继节点的问题。
可以在节点结构中增加前驱和后继的指针,但是增加了存储开销。另一种方法是利用二叉树的空链指针。在这里我们使用第二种方法。
同二叉树的遍历相同,二叉树的线索化也可分为前序线索化,中序线索化,后序线索化,每一种线索化同时对应着一种遍历方式。
前序线索化
线索化即找到它的前驱和后继,我们要利用二叉树中的空链,但是还要和原来的非空节点加以区分,所以在这里给出一个新的结构,里面标识了其左右子树是否被标记为前驱或后继。
enum Tag //标记
{
Link,
Thread
};
template<class T>
struct BinaryTreeNode
{
public:
BinaryTreeNode()
:_data(NULL)
{}
BinaryTreeNode(T &t){ //
_data = t;
}
public:
T _data;
BinaryTreeNode<T> *leftChild; //左节点
BinaryTreeNode<T> *rightChild; //右节点
Tag leftTag = Link; //左标记
Tag rightTag = Link; //右标记
};
在创建好二叉树,每个节点的左右标记都应该为 Link,此时对二叉树进行遍历,如果左节点为空,则让它的左节点指向他的前驱,如果右节点为空,则让他的右节点指向他的后继。
前驱我们在遍历的时候可以很好的找到,但是后继我们却无法直接访问到。所以我们需要一个节点来记录下来此节点的前驱 prev,如果前驱的右节点为空,则让前驱 prev 的右节点指向此节点。
实现:
enum Tag
{
Link,
Thread
};
template<class T>
struct BinaryTreeNode
{
public:
BinaryTreeNode()
:_data(NULL)
{}
BinaryTreeNode(T &t)
{
_data = t;
}
public:
T _data;
BinaryTreeNode<T> *leftChild; //左节点
BinaryTreeNode<T> *rightChild; //右节点
Tag leftTag = Link; //左标记
Tag rightTag = Link; //右标记
};
template<class T>
class BinaryTree
{
typedef BinaryTreeNode<T> Node;
public:
BinaryTree()
:_root(NULL)
{}
BinaryTree(T* a, size_t n, const T &invalid)
{
size_t index = 0; //从根节点开始传入
_root = CheckTree(a, n, invalid, index); // m 数组大小 invalid非法值 index 构建的位置
}
protected:
Node * CheckTree(T *a, size_t n, const T& invalid, size_t &index) { //二叉树初始化
Node *t = NULL;
if (a[index] != invalid && index < n) { // 如果不是# 或者未到达边界
t = new Node(a[index]);
t->leftChild = CheckTree(a, n, invalid, ++index); //左子树
t->rightChild = CheckTree(a, n, invalid, ++index); //右子树
}
return t;
}
public:
void PrevOrderTherading() //前序线索化
{
Node *prev = NULL;
_PrevOrderTherading(_root, prev);
}
private:
void _PrevOrderTherading(BinaryTreeNode<T> *root, Node*& prev)
{
if (root == NULL)
return;
if (root->leftChild == NULL)
{
root->leftChild = prev;
root->leftTag = Thread;
}
if (prev != NULL&&prev->rightChild == NULL)
{
prev->rightChild = root;
prev->rightTag = Thread;
}
prev = root;
if (root->leftTag == Link)
_PrevOrderTherading(root->leftChild, prev);
if (root->rightTag == Link)
_PrevOrderTherading(root->rightChild, prev);
}
void _InOrderTherading(BinaryTreeNode<T> *root, Node*& prev)
{
if (root == NULL) {
return;
}
_InOrderTherading(root->leftChild, prev);// 左子树 prev前驱
if (root->leftChild == NULL) {
root->leftTag = Thread; //如果左为空,需要前驱
root->leftChild = prev;
}
if (prev != NULL&&prev->rightChild == NULL)
{
prev->rightChild = root; //如果右为空 需要后继
prev->rightTag = Thread;
}
prev = root;
_InOrderTherading(root->rightChild, prev);//右树
}
private:
Node *_root;
};
此时二叉树已经前序线索化完成,但是线索化之后,我们还需要对其进行遍历。在遍历中,由于我们是依次向后遍历,所以在这里我们不需要对前驱节点进行访问。
可以看出,我们可以直接访问到根结点的左子树,之后利用其后继节点依次访问就可以了。
void PreOrder() //先序遍历线索化二叉树
{
if (_root == NULL) return;
Node *cur = _root;
while (cur->leftTag != Thread) //先遍历到左子树
{
cout << cur->_data << " ";
cur = cur->leftChild;
}
while (cur) //后继节点访问
{
cout << cur->_data << " ";
cur = cur->rightChild;
}
}
中序线索化
与前序线索化相同,也是利用前驱和后继来对节点的空节点进行处理。
enum Tag
{
Link,
Thread
};
template<class T>
struct BinaryTreeNode
{
public:
BinaryTreeNode()
:_data(NULL)
{}
BinaryTreeNode(T &t)
{
_data = t;
}
public:
T _data;
BinaryTreeNode<T> *leftChild; //左节点
BinaryTreeNode<T> *rightChild; //右节点
Tag leftTag = Link; //左标记
Tag rightTag = Link; //右标记
};
template<class T>
class BinaryTree
{
typedef BinaryTreeNode<T> Node;
public:
BinaryTree()
:_root(NULL)
{}
BinaryTree(T* a, size_t n, const T &invalid)
{
size_t index = 0; //从根节点开始传入
_root = CheckTree(a, n, invalid, index); // m 数组大小 invalid非法值 index 构建的位置
}
protected:
Node * CheckTree(T *a, size_t n, const T& invalid, size_t &index) { //二叉树初始化
Node *t = NULL;
if (a[index] != invalid && index < n) { // 如果不是# 或者未到达边界
t = new Node(a[index]);
t->leftChild = CheckTree(a, n, invalid, ++index); //左子树
t->rightChild = CheckTree(a, n, invalid, ++index); //右子树
}
return t;
}
public:
//线索化 前驱后继
//中序线索化
void InOrderTherading() {
BinaryTreeNode<T> *prev = NULL;
_InOrderTherading(_root,prev);
}
private:
void _InOrderTherading(BinaryTreeNode<T> *root,Node*& prev)
{
if (root == NULL) {
return;
}
_InOrderTherading(root->leftChild,prev);// 左子树 prev前驱
if (root->leftChild == NULL) {
root->leftTag = Thread; //如果左为空,需要前驱
root->leftChild = prev;
}
if (prev != NULL&&prev->rightChild == NULL)
{
prev->rightChild = root; //如果右为空 需要后继
prev->rightTag = Thread;
}
prev = root;
_InOrderTherading(root->rightChild, prev);//右树
}
private:
Node *_root;
};
中序线索化遍历
同前序线索化遍历相同,我们不需要前驱节点。但是如果使用和前序遍历相同的方法,会造成死循环。所以此时我们需要节点的左右标记来进行区分是否需要访问它的左树或右树。
void InOrder()
{// 中序线索化遍历
if (_root == NULL)
return;
Node* cur = _root;
while (cur)
{
while (cur->leftTag == Link) //先遍历到左树
cur = cur->leftChild;
cout << cur->_data << " ";
while (cur&&cur->rightTag == Thread)
{
cur = cur->rightChild;
cout << cur->_data << " ";
}
cur = cur->rightChild; // 没有后继,则存在右子树
}
}
后序线索化
与之前相类似。
实现:
enum Tag
{
Link,
Thread
};
template<class T>
struct BinaryTreeNode
{
public:
BinaryTreeNode()
:_data(NULL)
{}
BinaryTreeNode(T &t)
{
_data = t;
}
public:
T _data;
BinaryTreeNode<T> *leftChild; //左节点
BinaryTreeNode<T> *rightChild; //右节点
Tag leftTag = Link; //左标记
Tag rightTag = Link; //右标记
};
template<class T>
class BinaryTree
{
typedef BinaryTreeNode<T> Node;
public:
BinaryTree()
:_root(NULL)
{}
BinaryTree(T* a, size_t n, const T &invalid)
{
size_t index = 0; //从根节点开始传入
_root = CheckTree(a, n, invalid, index); // m 数组大小 invalid非法值 index 构建的位置
}
protected:
Node * CheckTree(T *a, size_t n, const T& invalid, size_t &index) { //二叉树初始化
Node *t = NULL;
if (a[index] != invalid && index < n) { // 如果不是# 或者未到达边界
t = new Node(a[index]);
t->leftChild = CheckTree(a, n, invalid, ++index); //左子树
t->rightChild = CheckTree(a, n, invalid, ++index); //右子树
}
return t;
}
public:
//线索化 前驱后继
//后序线索化
void PostOrderTherading() {
BinaryTreeNode<T> *prev = NULL;
_PostOrderTherading(_root, prev);
}
private:
void _PostOrderTherading(BinaryTreeNode<T> *root, Node*& prev)
{
if (root == NULL) {
return;
}
_PostOrderTherading(root->leftChild, prev);// 左子树 prev前驱
_PostOrderTherading(root->rightChild, prev);//右树
if (root->leftChild == NULL) {
root->leftTag = Thread; //如果左为空,需要前驱
root->leftChild = prev;
}
if (prev != NULL&&prev->rightChild == NULL)
{
prev->rightChild = root; //如果右为空 需要后继
prev->rightTag = Thread;
}
prev = root;
}
private:
Node *_root;
};
后序线索化遍历
同样先去掉前驱节点,但是此时我们无法直接依靠节点去依次访问后继节点,在这里我们需要一个栈来保存未被访问的根节点,如果某个节点的后继与栈顶元素相同,则可以出栈,如果后继节点与根节点 _root 相同,则输出根节点后便可以退出程序。
实现:
void PostOrder()
{
stack<Node *> s;
Node *cur = _root;
do
{
while (cur->leftTag == Link) // 先遍历到左树
{
s.push(cur);
cur = cur->leftChild;
}
while (cur->rightTag == Thread) //如果右树是后继则依靠后继遍历
{
cout << cur->_data << " ";
cur = cur->rightChild;
if (cur->_data == s.top()->_data) // 如果与栈顶相同 说明需要出栈
{
cout << cur->_data<<" ";
s.pop();
if (cur->rightChild == _root) // 如果此时节点的后继为根节点 则可以直接输出并结束
{
cout << cur->rightChild->_data << " ";
return;
}
}
}
if (!s.empty()) //循环走出后取栈顶元素
{
cur = s.top();
cur = cur->rightChild;
}
} while (cur != _root);
}