二叉树结点ADT
template <typename E> class BinNode
{
public:
virtual ~BinNode() {}
/* 返回元素的值 */
virtual E& element() = 0;
virtual void setElement(const E&) = 0;
/* 返回左右孩子结点的指针 */
virtual BinNode* left() const = 0;
virtual void setLeft(BinNode*) = 0;
virtual BinNode* right() const = 0;
virtual void setRight(BinNode*) = 0;
/* 是否为叶结点 */
virtual bool isLeaf() = 0;
};
二叉树链式实现
二叉树结点的实现
通常,二叉树结点都包含一个数据区,数据区所需空间大小根据需要而定。最常见的结点实现方法包含一个数据区和两个指向子结点的指针。以下为BinNode抽象类的实现:
template <typename Key, typename E>
class BSTNode : public BinNode<E>
{
private:
Key k; // 为了实现二叉检索树,需要添加一个新的域和对应的访问方式,以存储关键码值
E it;
BSTNode* lc;
BSTNode* rc;
public:
BSTNode() { lc = rc = NULL; }
BSTNode(Key K, E e, BSTNode* l = NULL, BSTNode* r = NULL)
{ k = K; it = e; lc = l; rc = r; }
~BSTNode() {}
E& element() { return it; }
void setElement(const E& e) { it = e; }
Key& key() { return k; }
void setKey(const Key& K) { k = K; }
inline BSTNode* left() const { return lc; }
void setLeft(BinNode<E>* b) { lc = (BSTNode*)b; } //注意这里的强转,博主其实不是很理解为什么
inline BSTNode* right() const { return rc; }
void setRight(BinNode<E>* b) { rc = (BSTNode*)b; }
bool isLeaf() { return (lc == NULL) && (rc == NULL); }
};
注: 我们还可以对此类重载new和delete操作符以便支持可利用空间表技术。
叶结点和分支结点
在利用指针实现的二叉树中,叶结点与分支结点是否使用相同的类定义十分重要。
使用相同的类可以简化实现,但是可能导致空间上的浪费,根据定义,只有分支结点有非空子结点,因此,分别定义分支结点与叶结点将节省存储空间。
有一些应用只需要用叶结点存储数据,还有一些应用要求分支结点与叶结点存储不同类型的数据(如:PR四分树、Huffman树、表达式树)。我们以表达式树为例,表达式树中,分支结点存储元素数目很少的操作符集合中的一个操作符,因此分支结点可以存储标识该操作符的代码或者用一个字节存储其图形符号。叶结点则存储不同的变量名或数值,所以叶结点必须有足够大的数据区来存储各种可能的值。同时,叶结点不必存储子结点的指针。
一、继承派生实现方案
在C++中,我们采用类继承的办法实现区分分支结点和叶结点的要求,只需要给BinNode定义一个基类,以便为对象提供一个通用的定义,并定义一个子类,以便修改基类。基类用来声明一般意义上的结点,而子类用来定义分支结点和叶结点。
以下是基类VarBinNode的定义:
class VarBinNode
{
public:
virtual ~VarBinNode() {}
virtual bool isLeaf() = 0;
};
以下是从基类派生的两个子类LeafNode和IntlNode的定义:
class LeafNode : public VarBinNode
{
private:
Operand var;
public:
LeafNode(const Operand& val) { var = val; }
bool isLeaf() { return true; }
Operand value() { return var; }
};
class IntlNode : public VarBinNode
{
private:
VarBinNode* left;
VarBinNode* right;
Operator opx;
public:
IntlNode(const Operator& op, VarBinNode* l, VarBinNode* r)
{ opx = op; left = l; right = r; }
bool isLeaf() { return false; }
VarBinNode* leftchild() { return left; }
VarBinNode* rightchild() { return right; }
Operator value() { return opx; }
};
对于上面两个代码中莫名其妙冒出来的Operand和Operator,博主是这样猜测的,根据英文意思,operand是操作数,operator是操作符,所以代码的作者是想让用户根据自己的分支结点和叶结点中存储的元素类型,自行决定替换这两个类型。
具体使用上,IntlNode类通过VarBinNode类的指针指向其子结点。我们下面给出的函数traverse说明这些类的用法。当traverse调用方法isLeaf时,C++的运行环境判断当前结点root所属的子类,并调用该子类的isLeaf函数。而后,isLeaf函数给出结点的真正结点类型。通过把基类指针强制转换为合适的指针,可以访问这两个派生子类的其他成员函数。
// preorder traversal
void traverse(VarBinNode *root)
{
if(root == NULL) return;
if(root->isLeaf())
cout << "Leaf: " << ((LeafNode *)root)->value() << endl;
else
{
cout << "Internal: " << ((IntlNode *)root)->value() << endl;
traverse(((IntlNode *)root)->leftchild());
traverse(((IntlNode *)root)->rightchild());
}
}
二、复合设计模式实现方案
可用来区分叶结点和分支结点的另一种方法是使用一个虚基类和两个独立的结点类。这是一种复合设计模式。
class VarBinNode
{
public:
virtual ~VarBinNode() {}
virtual bool isLeaf() = 0;
virtual void traverse() = 0;
};
class LeafNode : public VarBinNode
{
private:
Operand var;
public:
LeafNode(const Operand& val) { var = val; }
bool isLeaf() { return true; }
Operand value() { return var; }
void traverse() { cout << "Leaf: " << value() << endl; }
};
class IntlNode : public VarBinNode
{
private:
VarBinNode* lc;
VarBinNode* rc;
Operator opx;
public:
IntlNode(const Operator& op, VarBinNode* l, VarBinNode* r)
{ opx = op; lc = l; rc = r; }
bool isLeaf() { return false; }
VarBinNode* left() { return lc; }
VarBinNode* right() { return rc; }
Operator value() { return opx; }
void traverse()
{
cout << "Internal: " << value() << endl;
if(left() != NULL) left()->traverse();
if(right() != NULL) right()->traverse();
}
};
void traverse(VarBinNode *root)
{ if(root != NULL) root->traverse(); }
两种方法的比较
两种方法各有优缺点:
第一种方法不要求结点类明确支持traverse函数。使用这种方法很容易给树类添加新的操作方法,例如遍历,或者其他对树结点的操作。但是,可以发现其traverse方法的实现需要对其他子类很熟悉,并且添加新子类时需要修改traverse代码。
第二种方法则使得traverse函数不必了解结点子类独特的功能细节——子类自己负责遍历的处理工作。这样traverse函数不需要明确地枚举所有不同的结点子类,就能做合适的操作(子类多时这一优势很明显)。但是这种方法的缺点是遍历操作一定不能用NULL指针来调用,因为没有办法来捕获到这个调用(???),这个问题可以通过空结点的轻量级实现来避免。
复合设计模式的另一个优点是使得实现每种类型的功能更加容易。这是因为你可以只着眼于单个结点类型所需要的信息传递,以及其他行为。从而降低了递归处理复杂信息时的复杂性。
完全二叉树数组实现
在链式二叉树中,有很大比例的空间被结构性开销所占用,而不是用于存储有用的数据。下面将介绍一种简单、紧凑的实现完全二叉树的方法,即数组实现完全二叉树。其物理存储上呈现序列的形式,而逻辑结构则为完全二叉树。这是一个典型的物理结构与逻辑结构分离的例子。
具体来看,假设在完全二叉树中,逐层而下、从左到右,结点的位置完全由其序号确定。数组可以有效的存储二叉树的数据,把每一个数据存放在其结点对应序号的位置上。
事实上,另一种更为常见的数组实现的二叉树的方案是将数组的第一个位置空出来,这样,就可以将二叉树结点标号和数组下标对应起来。设结点位置为k,则其左右孩子分别位于2k和2k+1位置,其父结点位于k/2位置。
但是,当二叉树的层数很偏大时,用数组给二叉树结点编号的方式来存储二叉树就不可行了,有兴趣的读者可以尝试一下这道题Trees on the level。
目录