- 导言
- 什么是树
- 树结构定义
- 树的结点
- 结点分类
- 结点的联系
- 结点的层次
- 有序树
- 森林
- 相比线性结构
- 树的存储结构
- 双亲表示法
- 结构体定义
- 描述法举例
- 孩子表示法
- 描述法举例
- 结构体定义
- 孩子兄弟表示法
- 描述法举例
- 双亲表示法
- 二叉树
- 堆
- 并查集
- 应用情景
- 表达式树
- 建树算法
- 伪代码
- 代码实现
- 计算表达式树
- 伪代码
- 代码实现
- 建树算法
- 目录树
- 表达式树
- AC 自动机
- 参考资料
导言
轩辕剑是一个经典的中文角色扮演游戏,通过对历史内容的考究,与精彩感人的剧情结合,使得这个系列被公认为华人世界的两大经典角色扮演游戏系列之一。我最为喜欢的两部是《轩辕剑叁:云和山的彼端》和《轩辕剑叁外传:天之痕》,剧情感人精彩、别有深意,2D的场景细致美观、独具特色……当然,我这次仍然不是来给你推荐游戏的,而是想对其中一个场景做点文章。
“建木”是上古先民崇拜的一种圣树,传说建木是沟通天地人神的桥梁,在《轩辕剑叁外传:天之痕》中的仙山岛,利用水墨画的风格进行了描绘,是我最喜欢的游戏场景之一。其中就有对“海中建木”的描绘。“海中建木”无疑是一颗巨大的树,这棵树也肯定是由无数的根、枝、叶组成的,如果我们把“海中建木”抽象成一个数据结构,那么这个结构就有一个根部,还有很多的分支即枝,还有很多的子叶,这就是我们要描述的树结构。
什么是树
树结构定义
树结构(Tree)是 n(n ≥ 0) 个结点的有限集,当 n = 0 时为空树,否则为非空树。对于非空树有如下特点:
- 有且仅有一个特定的根结点,不允许存在多个结点;
- 除根结点以外的其余结点可分为 m(m > 0) 个互不相交的有限集,其中每一个有限集本身还是一棵树,称为根的子树。子树的个数没有限制,但是一定不能有交集。
如图所示,分别是空树和非空树:
我们单独看一下图示非空树,这个树结构分别有 3 个子树:
树的结点
结点分类
树的结点包含存储数据的数据域和指向的分支,指向的分支可以是多个。我们用度来描述一个结点具有几个分支,结点的度的数值等于其子树的个数。没有分支的结点称为叶结点或终端结点,叶结点的度为 0,除了根结点的度不为 0 的结点称为内部结点,一个树结构的度为根结点和所有内部结点的度的最大值。例如:
结点的联系
结点的分支称为结点的孩子,该结点也被称为其孩子的双亲,同属于同一双亲的子树结点被称为兄弟,结点的祖先是从根结点到该结点的分支上的所有结点,以某一结点为根结点的分支都成为该结点的子孙。
结点的层次
层次表示从根结点开始,根结点的子结点属于第二层,根结点的子结点的子结点属于第三层,以此类推直到到达最底层的叶结点。通过这种方式推导的最大层次为该树结构的层次,其中双亲在同一层的结点互为堂兄弟结点。
有序树
若一个树结构中的结点的子树从左到右是有序的(不能互换),则称之为有序树,否则是无序树。对于有序树而言,最左边子树的根称为第一个孩子,最右边称为最后一个孩子。
森林
是 m(m ≥ 0) 棵互不相交的树的集合,对树中每个结点而言,其子树的集合即为森林,例如上文中的树的各个子树,就可以认为是一个森林,因此可以用森林和树相互递归的定义来描述树。
相比线性结构
线性结构 | 树结构 |
---|---|
第一个元素无前驱 | 有且仅有一个根结点,无前驱 |
最后一个元素无后继 | 叶结点无子结点,不唯一 |
中间元素有前驱和后继 | 中间结点可以有多个分支子结点 |
树的存储结构
对于线性表来说,我们只有两种描述——顺序存储和链式存储,但是对于树结构来说我们显然不能直接生搬硬套,这是因为树结构的数据是多对多的关系,这就说明了我们不能像线性表那样只做到把单个元素的前驱后继说明白,树结构中的结点是有辈分关系的,不能乱了套。因此当我们描述树结构的存储方式时,需要着重描述结点间的亲子关系,这就使得我们有:双亲表示法、孩子表示法和孩子兄弟表示法来描述。
双亲表示法
结构体定义
该表示法着重于描述各个结点与双亲的关系,在使用顺序存储描述时,结构体定义如下。
#define MAXSIZE 100
typedef struct PTNode
{
ElemType data; //数据域
int parent; //指向双亲的游标域
}PTNode; //定义结点结构体
typedef struct
{
PTNode nodes[MAXSIZE]; //结点数组
int root; //指向根结点的游标
int count; //结点数
}PTree; //定义树结构体
描述法举例
下标 | data | parent | firstchild | rightsib |
---|---|---|---|---|
0 | A | -1 | 1 | 3 |
1 | B | 0 | 4 | 6 |
2 | C | 0 | -1 | -1 |
3 | D | 0 | 7 | 7 |
4 | E | 1 | -1 | -1 |
5 | F | 1 | -1 | -1 |
6 | G | 1 | -1 | -1 |
7 | H | 3 | 8 | 9 |
8 | I | 7 | -1 | -1 |
9 | J | 7 | -1 | -1 |
我们观察到,除了上述结构体需要描述的父母位置,我还加了一个结点的第一个孩子结点和最后一个孩子结点的游标,分别是长子位和次子位。有些时候我们多设计一些游标会有助于我们实现功能,但是需要具体问题具体分析。
孩子表示法
该描述法着重于描述结点与其孩子结点的关系,通过多重链表来描述,也就是说每个结点都会根据其子结点的个数拥有一定数量的指针域。
我们需要考虑一个问题,就是一个结点要开多少个指针域合适?利用我们前面的度的概念,由于树结构的度是整个树中单个结点拥有的最多的分枝数,如果每个结点的指针域等于树的度当然可以解决问题,但是并不是所有结点都需要这么多指针域的。如果说我们用动态内存分配的想法,一个结点需要多少指针域就开多少空间也可以实现,但是我们就不得不使用类似柔性数组的机制,“杀鸡焉用牛刀”。
综上所述,我们选择的方式是将每个结点的孩子结点用单链表描述起来,每个结点都有一个属于自己的孩子单链表,描述每个结点时可以用顺序表去描述。这样讲还是有点抽象,我们看个例子。
描述法举例
结构体定义
从上面的例子可以看出,我们需要分别设计孩子链表的结点和表头数组的结点。
#define MAXSIZE 100
typedef struct CTNode
{
int child; //指向长子的游标域
struct CTNode *next; //指向下一个孩子的指针域
}*ChildPtr; //定义孩子结点结构体
typedef struct
{
ElemType data; //数据域
//同双亲表示法的拓展,这里可以开个指向双亲的指针域
ChildPtr *firstchild; //指向长子的指针域
}CTBox; //定义表头结构体
typedef struct
{
PTNode nodes[MAXSIZE]; //结点数组
int root; //指向根结点的游标
int count; //结点数
}CTree; //定义树结构体
孩子兄弟表示法
该表示法着重于对兄弟结点的描述,由于对于任意一结点而言,若该结点存在子结点,则长子结点和右结点等都是唯一确定的,结构体定义如下:
typedef struct CSNode
{
ElemType data; //数据域
struct CSNode *firstchild; //指向对应长子结点的指针域
struct CSNode *rightsib; //指向对应右兄弟结点的指针域
}CSNode,*CSTree;
描述法举例
二叉树
二叉树是树结构中极其重要的一种结构,请左转我的另一篇博客进行阅读——二叉树结构详解。
堆
左转我另一篇博客堆、优先级队列、堆排序
并查集
左转我另一篇博客并查集。
应用情景
表达式树
建树算法
伪代码
代码实现
void InitExpTree(BTree& T, string str)
{
stack forest; //森林栈
stack oper;
BTree a_node;
BTree num1, num2;
int idx = 0;
while (str[idx])//搞不定有 ‘#’ 的做法,害
{
if (In(str[idx]))
{
if (oper.empty() || Precede(oper.top(), str[idx]) == '<') //加一个空栈情况
{
oper.push(str[idx++]);
}
else if (Precede(oper.top(), str[idx]) == '>') //这个分支 idx 不用变
{
num2 = forest.top();
forest.pop();
num1 = forest.top();
forest.pop();
CreateExpTree(a_node, num1, num2, oper.top());
oper.pop();
forest.push(a_node);
}
else
{
oper.pop();
idx++; //别忘了加了
}
}
else //是数字
{
CreateExpTree(a_node, NULL, NULL, str[idx++]);
forest.push(a_node);
}
}
while (!oper.empty()) //剩余的符号要记得处理
{
num2 = forest.top();
forest.pop();
num1 = forest.top();
forest.pop();
CreateExpTree(a_node, num1, num2, oper.top());
oper.pop();
forest.push(a_node);
}
T = forest.top();
}
计算表达式树
伪代码
代码实现
double EvaluateExTree(BTree T)
{
double num1, num2;
if (!In(T->data))
{
return T->data - '0'; //别忘了减掉
}
num1 = EvaluateExTree(T->lchild);
num2 = EvaluateExTree(T->rchild);
switch (T->data)
{
case '+':
return num1 + num2;
case '-':
return num1 - num2;
case '*':
return num1 * num2;
case '/':
if (num2 == 0) //别忘了加了
{
cout << "divide 0 error!";
exit(0); //别忘了出错之后就啥都不干了
}
return num1 / num2;
}
}
目录树
左转我另一篇博客PTA习题解析——目录树。
AC 自动机
这是一个树结构和字符串匹配算法联合工作的算法,左转博客Aho-Corasick automaton
参考资料
《大话数据结构》—— 程杰 著,清华大学出版社
《数据结构教程》—— 李春葆 主编,清华大学出版社
《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社