终于到谈到树了,可以说数据结构最精彩的算法都出自这里 (但不是最复杂的,后面还有图..)。接下来的2篇文章会介绍有关树的一些操作和应用。
树的两个基本用途,可以用物质和精神来比喻。
一个用途是做为数据储存,储存具有树结构的数据——目录、族谱等等。为了在实际上是线性的储存载体上(内存、磁盘)储存非线性的树结构,必须有标志指示出树的结构。因此,只要能区分根和子树,树可以采取各种方法来储存——多叉链表、左孩子-右兄弟二叉链表、广义表、多维数组。由于操作的需求,储存方法并不是随意选取的。比如,在并查集的实现上,选取的是双亲链表。
一个用途是做为逻辑判断,此时会常常听到一个名词——判定树。最常用的结构是二叉树 (见下文),一个孩子代表true,一个孩子代表false。关于多叉判定树,有个例子是求8皇后的全部解——这个连高斯都算错了(一共是92组解,高斯最开始说76组解),我们比不上高斯,但是我们会让computer代劳。
就像哲学界到现在还纠缠于物质和精神本源问题,实际上在树这里也是如此。有些情况下,并不能区分是做为储存来用还是做为判断来用,比如搜索树,既储存了数据,还蕴涵着判断,这在后续的文章会提到。
和后面的图相比,树更基本,也更常用。你可以不知道最短路径怎么求,却每时每刻都在和树打交道——看看你电脑里的文件夹吧 ^_^
首先要介绍的是树形结构中最基础、最常用的所谓二叉树。二叉树可以说是人们假想的一个模型,因此,允许有空的二叉树是无争议的。二叉树是有序的,左边有一个孩子和右边有一个的是不同的两棵二叉树树。做这个规定,是因为人们赋予了左孩子和右孩子不同的意义。二叉树有多种存储方式,下面只讲解链式结构。看各种讲数据结构的书,你会发现一个有趣的现象:在二叉树这里,基本操作有计算树高、寻找节点以及各种遍历,但就是没有插入、删除操作。那树是怎么建立起来的?其实这很好理解,对于非线性的树形结构,插入删除操作不在一定的法则规定下,是毫无意义的。因此,只有在具体的应用中,才会有插入删除操作。
与之前的文章一样,这里不讨论各种基本操作,而是讲一些实际应用。我发现这样更有利于加深对数据结构的理解。这里讲书上的例子:二叉表达式树。但书上一个具体算法都没有给出,倒是随书光盘里给出了一个中缀表达式建树的演示,我就根据它编写了一个完整的表达式树类,下面以它来介绍二叉树的一些典型操作。包括:输入任意形式的表达式建立一棵树,表达式树的求值以及通过不同方式的遍历来生成不同形式的表达式。
首先是结点结构。与前面学过的线形链表一样,二叉树的结点也分为数据域与指针域,只不过这里有两个指针域分别指向左右孩子节点。有时为了操作方便还会附加一个双亲指针,这里就不需要了,下面是节点定义:
class
TNode {
public :
union { char optr; int opnd; };
TNode * left;
TNode * right;
TNode( char op, TNode * lef, TNode * rgt)
: optr(op), left(lef), right(rgt) {}
TNode( int num)
: opnd(num), left(NULL), right(NULL) {}
};
public :
union { char optr; int opnd; };
TNode * left;
TNode * right;
TNode( char op, TNode * lef, TNode * rgt)
: optr(op), left(lef), right(rgt) {}
TNode( int num)
: opnd(num), left(NULL), right(NULL) {}
};
在表达式中又分运算数与操作符,所以结点的数据域必须设成联合的形式。同时为了便于生成新结点而加了两个构造函数。因为这个表达式类只是为了介绍二叉树,所以为了简便起见规定操作数只能是 1~9的整数。
表达式类的定义很简单,只需要存储一个指向树根的指针就够了,并且不需要做任何的初始化操作。下面是类的定义及部分函数声明。
#include
"
../../线性表(链式存储)/栈/Stack.h
"
#include " ../../require.h "
#include < iostream >
#include < string >
class ExpTree {
TNode * m_pRoot;
public :
ExpTree() : m_pRoot(NULL) {}
~ ExpTree() { destroy(m_pRoot); }
// 求值
int value() { return val(m_pRoot); }
private :
// 销毁树
void destroy(TNode * cur) {
if (cur) {
destroy(cur -> left);
destroy(cur -> right);
delete cur;
}
}
// 计算
int cacul( int a, char op, int b) {
switch (op) {
case ' + ' : return a + b;
case ' - ' : return a - b;
case ' * ' : return a * b;
case ' / ' : return a / b;
}
}
// 求值函数
int val(TNode * cur) {
if (cur -> left == NULL && cur -> right == NULL)
return cur -> opnd;
else
#include " ../../require.h "
#include < iostream >
#include < string >
class ExpTree {
TNode * m_pRoot;
public :
ExpTree() : m_pRoot(NULL) {}
~ ExpTree() { destroy(m_pRoot); }
// 求值
int value() { return val(m_pRoot); }
private :
// 销毁树
void destroy(TNode * cur) {
if (cur) {
destroy(cur -> left);
destroy(cur -> right);
delete cur;
}
}
// 计算
int cacul( int a, char op, int b) {
switch (op) {
case ' + ' : return a + b;
case ' - ' : return a - b;
case ' * ' : return a * b;
case ' / ' : return a / b;
}
}
// 求值函数
int val(TNode * cur) {
if (cur -> left == NULL && cur -> right == NULL)
return cur -> opnd;
else