声明:本文为学习数据结构与算法分析(第三版) Clifford A.Shaffer 著的学习笔记,代码有参考该书的示例代码。
定义与术语
这没什么好说的,照搬书上的吧。
一棵树 T 是由一个或一个以上结点组成的有限集,其中有一个特定的结点 R 称为 T 的根结点。如果集合 (T -{R}) 非空,那么集合中的这些结点被划分为 n 个不相交的子集 T0, T1, ……, Tn, 其中每个子集都是树,并且其相应的根结点 R0, R1, ……, Rn是 R 的子结点。
结点的出度(out degree)定位该结点的子结点数目。
结点的ADT
//General Tree Node
template<typename E>
class GTNode
{
protected:
virtual void removeFirst();
virtual void removeNext();
public:
virtual ~GTNode() {}
virtual E value() const;
virtual GTNode* leftMostChild() const;
virtual GTNode* rightSibling() const;
virtual void setValue(const E& e);
virtual void insertFirst(GTNode* infirst);
virtual void insertNext(GTNode* innext);;
};
*由于removeFirst
和removeNext
函数在考虑整颗树的情况下,比较复杂,所以暂时将其访问权限限制为 protected
树的遍历
类似二叉树,树的遍历有前序,中序,后序三种。
前序遍历和后序遍历很好定义,而中序遍历却并不是确定的。常用的定义是先遍历最左子结点,然后遍历根结点,然后遍历其他子结点。
实现:
template<typename E>
void preorderTree(E* root, void (*visit)(E*) )
{
if(root==nullptr) return;
visit(root);
auto temp = root->leftMostChild();
while(temp!=nullptr)
{
preorderTree(temp, visit);
temp = temp->rightSibling();
}
}
template<typename E>
void inorderTree(E* root, void(*visit)(E*) )
{
if(root==nullptr) return;
auto temp = root->leftMostChild();
inorderTree(temp, visit);
visit(root);
while(temp!=nullptr)
{
temp = temp->rightSibling();
inorderTree(temp, visit);
}
}
template<typename E>
void postorderTree(E* root, void(*visit)(E*) )
{
if(root==nullptr) return;
auto temp = root->leftMostChild();
while(temp!=nullptr)
{
postorderTree(temp, visit);
temp = temp->rightSibling();
}
visit(root);
}
树的回收其实是后序遍历的一种,实现如下:
template<typename E>
void clear(E* root)
{
if(root == nullptr) return;
auto temp = root->leftMostChild();
while(temp!=nullptr)
{
auto next = temp->rightSibling();
clear(temp);
temp = next;
}
delete root;
}
一个简单的例子
按照书上的这颗树,建立如下:
#include<iostream>
#include"Tree.h"
using namespace std;
template<typename E>
void visit(E* root)
{
cout<<root->value()<<" ";
}
int main()
{
char c[] = { 'R', 'A', 'B', 'C', 'D', 'E', 'F'};
int i=0;
GTNode<char>* root = new GTNode<char>(c[i++]);//R
root->insertFirst( new GTNode<char>(c[i++]));
root->leftMostChild()->insertNext( new GTNode<char>(c[i++]));
auto temp = root->leftMostChild();
temp->insertFirst( new GTNode<char>(c[i++]));
temp = temp->leftMostChild();
temp->insertNext( new GTNode<char>(c[i++])); temp = temp->rightSibling();
temp->insertNext( new GTNode<char>(c[i++])); temp = temp->rightSibling();
temp = root->leftMostChild()->rightSibling();
temp->insertFirst(new GTNode<char>(c[i++]));
preorderTree(root, visit); cout<<endl;
inorderTree(root, visit); cout<<endl;
postorderTree(root, visit);
cout<<endl;
clear(root); root = nullptr;
}
输出:
R A C D E B F
C A D E R F B
C D E A F B R
树的另一种表示:父指针表示法
父指针表示树,即结点只存储父结点的指针。
但是很明显,这样的表示对于一些常用的操作,如查找左兄弟结点并不支持。
这样的表示是有特殊的目的的,它能解答以下的问题:
“给出两个结点,它们是否在同一棵树中?”
父指针表示法常常用来维护由一些不相交子集构成的集合。对于不相交的集合,希望提供如下两种操作:
- 判断两个节点是否在同一集合中
- 归并两个集合。(UNION)
归并两个集合的过程常常称为”UNION”,且整个操作旨在通过归并找出两个结点是否在同一个集合中,因此以“并查算法”(UNION/FIND,也称并查集)命令。
下面是实现:
class ParPtrTree
{
int* array;
int len;
enum{ ROOT = -1 };
int find(int curr)
{
if(array[curr] == ROOT) return curr;
array[curr] = find(array[curr]);
return array[curr];
}
int getChildNum(int curr)
{
int sum = 0;
for(int i = 0;i<len;++i)
if(array[i] == curr) ++sum;
return sum;
}
public:
ParPtrTree(int n)
{
array = new int[n];
len = n;
for(int i=0;i<len;++i)
array[i] = ROOT;
}
virtual ~ParPtrTree()
{
delete[] array;
}
void Union(int a, int b)
{
a = find(a);
b = find(b);
if(getChildNum(b) > getChildNum(a) )
array[a] = b;
else
array[b] = a;
}
bool differ(int a, int b)
{
return find(a)!=find(b);
}
};
*在课本上找不到完整的实现代码,这里的实现仅是我个人的实现,仅供参考
array
数组存储的是相应结点的父结点的下标。
find
函数使用了递归。这是一种路径压缩的方法,查找到当前结点的父结点,并把当前结点的所有祖先结点的父指针都指向根结点。
并查算法的输入是一系列等价对,考虑以下包含两个连通分支的图:
等价对可以是 A-H 或者 C-H .
因为传递性, A-C 也是等价对。
树的实现
树的实现有几种形式,这里只讲方法(将用到课本的图),并不给出代码实现。
子结点表表示法
子结点表表示法在数组中存储树的结点,每个结点包括结点值,一个父指针(或索引)及一个指向子结点链表的指针,顺序是从左到右。
左子结点/右兄弟结点表示法
左子结点/右兄弟结点表示法,顾名思义,就是表中记录了一个结点的左子结点和右兄弟
动态结点表示法
每个结点存储了结点的值和一个动态数组,动态数组中存储的是子节点的指针域和数组的大小。
另外一种利用链表的实现更加灵活:
动态左子结点/右兄弟结点表示法
在二叉树中,有左右子树。扩展到树的表示,即可变成左子节点表示树的左子节点,右子结点表示树的右兄弟结点。
这个表示方法只有两个指针域。
还可以扩展的表示森林。即根结点互为兄弟。
一般的表示如下:
树的顺序表示法
存储一系列结点的值,其中包含了尽可能少,但是对于重建树结构必不可少的信息,这种方法称为顺序树表示法。它的优点是节省空间。
考虑如下二叉树,可以根据先序遍历记录下来,其中 / 表示NULL
那么这棵树的顺序表示如下:
AB/D//CEG///FH//I//
上面的表示并不区分内部结点和叶子结点。
我们可以用另外一种方法表示,标记处内部结点,用以区分叶子结点。
A’B’/DC’E’G’/F’HI
这样的表示更省空间,但是我们还要额外记录一个标识信息。
标识信息可以使用位来记录,假如存储结点值的是 int 整型字段,而结点的值是正数,那么我们可以使用最高位的符号位来表示内部结点。
我们还可以在书中额外记录一个位向量。上面的位向量如下:
11001110100
然而使用顺序表示法来表示树的话,还需要记录结构更多的显示信息,如子结点的数目。
作为一种替代方法,可以在记录子结点表结束的位置。
下面使用了特殊的标记来表示子结点表的结束”)”.
考虑如下的树:
它的顺序表示法是:
RAC)D)E))BF)))
其中,F 后面跟了三个括号,因此表示 F 子结点、B 子结点、R 子结点的结束。
则重建树的代码可以如下:
GTNode<char>* rebuildTree(const char* str, int& i)
{
if(str[i] == ')' || str[i] == '\0') return nullptr;
GTNode<char>* root = new GTNode<char>(str[i]);
root->insertFirst(rebuildTree(str, ++i));
root->insertNext(rebuildTree(str, ++i));
return root;
}
所有实现均可在github上找到
其实并没有什么内容的github
–END–