一、树的介绍
1. 树的定义
树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。
把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
(1) 每个节点有零个或多个子节点;
(2) 没有父节点的节点称为根节点;
(3) 每一个非根节点有且只有一个父节点;
(4) 除了根节点外,每个子节点可以分为多个不相交的子树。
2. 树的基本术语
若一个结点有子树,那么该结点称为子树根的"双亲",子树的根是该结点的"孩子"。有相同双亲的结点互为"兄弟"。一个结点的所有子树上的任何结点都是该结点的后裔。从根结点到某个结点的路径上的所有结点都是该结点的祖先。
结点的度:结点拥有的子树的数目。
叶子:度为零的结点。
分支结点:度不为零的结点。
树的度:树中结点的最大的度。
层次:根结点的层次为1,其余结点的层次等于该结点的双亲结点的层次加1。
树的高度:树中结点的最大层次。
无序树:如果树中结点的各子树之间的次序是不重要的,可以交换位置。
有序树:如果树中结点的各子树之间的次序是重要的, 不可以交换位置。
森林:0个或多个不相交的树组成。对森林加上一个根,森林即成为树;删去根,树即成为森林。
二、二叉树的介绍
1. 二叉树的定义
二叉树是每个节点最多有两个子树的树结构。它有五种基本形态:二叉树可以是空集;根可以有空的左子树或右子树;或者左、右子树皆为空。
2. 二叉树的性质
二叉树有以下几个性质:
性质1:二叉树第i层上的结点数目最多为 2{i-1} (i≥1)。
性质2:深度为k的二叉树至多有2{k}-1个结点(k≥1)。
性质3:包含n个结点的二叉树的高度至少为log2 (n+1)。
性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1
3. 满二叉树,完全二叉树和二叉查找树
3.1 满二叉树
定义:高度为h,并且由2{h} –1个结点的二叉树,被称为满二叉树。
3.2 完全二叉树
定义:一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下一层的叶结点集中在靠左的若干位置上。这样的二叉树称为完全二叉树。
特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。
3.3 二叉查找树
定义:二叉查找树, 即二叉查找树(Binary Search Tree),又被称为二叉搜索树。设x为二叉查找树中的一个结点,x节点包含关键字key,节点x的key值记为key[x]。如果y是x的左子树中的一个结点,则key[y] <= key[x];如果y是x的右子树的一个结点,则key[y] >= key[x]。
在二叉查找树中:
(1) 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2) 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3) 任意节点的左、右子树也分别为B树。
(4) 没有键值相等的节点(no duplicate nodes)。
其具体结构如下图所示:
二叉查找树的搜索,从根结点开始,如果查询的关键字与结点的关键字相等,那么就命中;否则,如果查询关键字比结点关键字小,就进入左儿子;如果比结点关键字大,就进入右儿子;如果左儿子或右儿子的指针为空,则报告找不到相应的关键字;
如果二叉查找树的所有非叶子结点的左右子树的结点数目均保持差不多(平衡),那么二叉查找树的搜索性能逼近二分查找;但它比连续内存空间的二分查找的优点是,改变二叉查找树结构(插入与删除结点)不需要移动大段的内存数据,甚至通常是常数开销;如:
但二叉查找树在经过多次插入与删除后,有可能导致不同的结构:
右边也是一个二叉查找树,但它的搜索性能已经是线性的了;同样的关键字集合有可能导致不同的树结构索引;所以,使用B树还要考虑尽可能让二叉查找树保持左图的结构,和避免右图的结构,也就是所谓的“平衡”问题;
实际使用的二叉查找树都是在原二叉查找树的基础上加上平衡算法,即“平衡二叉树”;如何保持B树结点分布均匀的平衡算法是平衡二叉树的关键;平衡算法是一种在二叉查找树中插入和删除结点的策略;
三、二叉查找树的实现(C++)
1、二叉查找树的定义
(1)节点定义
typedef int DataType;
struct BSTNode
{
DataType data;
BSTNode *lchild;
BSTNode *rchild;
BSTNode *parent;
BSTNode(DataType x)
:data(x), lchild(NULL), rchild(NULL), parent(NULL){};
};
(2) 二叉查找树定义
class BSTree {
public:
BSTree();//构造函数
~BSTree();//析构函数
//定义外部接口函数
//前序遍历
void preOrder();
//中序遍历
void inOrder();
//后序遍历
void postOrder();
//分层遍历
void levelOrder();
//查找键值为x的结点
BSTNode *searchNode(DataType x);
//将键值为x的结点插入二叉树
void insertNode(DataType x);
//删除键值为x的结点
void removeNode(DataType x);
//销毁二叉树
void destroyBSTree();
//打印二叉树
void printBSTree();
//查找键值最小的结点
DataType minMem();
//查找键值最大的结点
DataType maxMem();
private://定义内部数据和接口函数
BSTNode *root; // 根结点
// 前序遍历"二叉树"
void preOrder(BSTNode *root);
// 中序遍历"二叉树"
void inOrder(BSTNode *root);
// 后序遍历"二叉树"
void postOrder(BSTNode *root);
//分层遍历
void levelOrder(BSTNode *root);
//查找
BSTNode * searchNode(BSTNode *root, DataType key);
//插入
void insertNode(BSTNode * &root, BSTNode *x);
//删除
BSTNode * removeNode(BSTNode *root, BSTNode *x);
//销毁
void destroyBSTree(BSTNode *root);
//打印
void printBSTree(BSTNode *root, DataType data, int direction);
//查找最小结点
BSTNode * minMem(BSTNode *root);
//查找最大结点
BSTNode * maxMem(BSTNode *root);
//查找前驱
BSTNode * predecessorode(BSTNode *x);
//查找后继
BSTNode * successorNode(BSTNode *x);
};
二叉查找树的常见操作:
【插入】
如果一个节点小于根节点,且根节点的左孩子为空的话,那么直接将该节点作为根节点的左孩子;
如果一个节点小于根节点,但是根节点的左孩子不为的话,那么我们将根节点的左孩子作为新的根节点,递归进行1、2两步。
同理,我们可以知道该怎么处理一个节点大于根节点的情况。
在这里有一个特殊的情况需要说明一下,那就是如果碰到重复的节点,本文采用软插入和软删除的办法。即我们设置一个变量用来表示该节点出现的次数,如果要插入的节点已经在二叉搜索树中存在的话,那么我们只需要把这个节点出现的次数变量加1。
//插入新结点 - 内部函数
void BSTree::insertNode(BSTNode * &bstnode, BSTNode *x)
{
BSTNode *y = NULL;
BSTNode *z = bstnode;
//查找x的插入位置
while(z != NULL)
{
y = z;
if(x->data < z->data)
z = z->lchild;
else
z = z->rchild;
}
x->parent = y;
if(y == NULL)
{
bstnode = x;
}
else if(x->data < y->data)
{
y->lchild = x;
}
else
{
y->rchild = x;
}
}
//插入新结点 - 外部函数
void BSTree::insertNode(DataType x)
{
BSTNode *temp = new BSTNode(x);
insertNode(root, temp);
}
【最大值&最小值】
找一个二叉树的最大值和最小值比较简单,因为二叉树的最小值肯定在二叉树的最左边,最大值肯定在二叉树的最右边。我们只需要使用一个循环,一直分别循环到节点的左节点或右节点为空,则可以得到二叉树的最大值和最小值了。
//查找最小结点 - 内部函数
BSTNode * BSTree::minMem(BSTNode *root)
{
if(root == NULL)
return NULL;
while(root->lchild != NULL)
root = root->lchild;
return root;
}
//查找最小结点 - 内部函数
DataType BSTree::minMem()
{
BSTNode *p = minMem(root);
if(p != NULL)
return p->data;
return (DataType)NULL;
}
//查找最大结点 - 内部函数
BSTNode * BSTree::maxMem(BSTNode *root)
{
if(root == NULL)
return NULL;
while(root->rchild != NULL)
root = root->rchild;
return root;
}
//查找最大结点 - 外部函数
DataType BSTree::maxMem()
{
BSTNode *p = maxMem(root);
if(p != NULL)
return p->data;
return (DataType)NULL;
}
【前驱节点】
找一个节点的前驱节点,我们分为两种情况
若该节点有左孩子,那么前驱节点则为以该节点的左孩子为根节点所寻找到的最大值;
若该节点没有左孩子,那么前驱节点需要往上寻找节点为右孩子。
//查找结点x的前驱结点,即查找二叉树中数据值小于该结点的最大值
BSTNode * BSTree::predecessorode(BSTNode *x)
{
//如果x存在左孩子,则x的前驱结点就是以其左孩子为根的子树的最大结点
if(x->lchild != NULL)
return maxMem(x->lchild);
//如果x没有左孩子,则x有以下两张可能
//(1)x是一个右孩子,则x的前驱结点为它的父节点
//(2)x是一个左孩子,则查找x的最低父节点,并且该父节点具有右孩子,
//找到这个最低的父节点就是x的前驱结点
BSTNode *y = x->parent;
while((y != NULL) && (x == y->lchild))
{
x = y;
y = y->parent;
}
return y;
}
【后继节点】
找一个节点的后继节点,我们分为两种情况
若该节点有右孩子,那么前继节点则为以该节点的左孩子为根节点所寻找到的最小值;
若该节点没有右孩子,那么前继节点需要往上寻找节点为左孩子。
//查找结点x的后继结点,即查找二叉树中数据值大于该结点的最小值
BSTNode * BSTree::successorNode(BSTNode *x)
{
//如果x存在右孩子, 则x的后继结点为“以其右孩子为根的子树的最小结点
if(x->rchild != NULL)
return minMem(x->rchild);
//如果x没有右孩子,则x有以下两种可能:
//(1)x是”一个左孩子“,则”x的后继结点“为”它的父结点“
//(2)x是一个右孩子,则查找x的最低父结点,并且该父节点要有左孩子,
//找到这个最低父节点就是x的后继结点
BSTNode *y = x->parent;
while((y != NULL) && (x == y->rchild))
{
x = y;
y = y->parent;
}
return y;
}
【删除】
删除一个节点,我们也分为三种情况
如果待删除的节点即没有左孩子,也没有右孩子的话,则直接删除该节点,但是请注意需要将相应的父节点的修改。如:如果待删除的节点是父节点的左孩子的话,那么需要将父节点的左孩子节点置为空;
如果待删除的节点只有一个孩子,那么直接用该孩子代替这个节点。这里,同样需要注意修改父节点。即,如果待删除是父节点的左孩子,且它只有一个右孩子,那么删除这个节点需要将父节点的左节点指向待删除节点的右孩子;
如果待删除的节点有两个孩子的话,我们可以选择使用该节点的前继节点或者后继节点来代替该节点。大家可以在笔下试一试,就会发现两种替代方式,都能保证更新之后的二叉树依然满足二叉搜索树的定义。
//删除键值为x的结点,并返回该结点 - 内部函数
BSTNode * BSTree::removeNode(BSTNode *root, BSTNode *x)
{
BSTNode *y = NULL;
BSTNode *z = NULL;
//如果待删除结点即没有左孩子也没有右孩子,则直接删除该结点,当需要对待删除结点的父节点进行修改
if((x->lchild == NULL) || (x->rchild == NULL))
z = x;
else
z = successorNode(x);
if(z->lchild != NULL)
y = z->lchild;
else
y = z->rchild;
if(y != NULL)
y->parent = z->parent;
if(z->parent == NULL)
root = y;
else if(z == z->parent->lchild)//入股待删除结点是其父节点的左孩子
z->parent->lchild = y;
else
z->parent->rchild = y;
if(z != x)
x->data = z->data;
return z;
}
//删除键值为x的结点,并返回该结点 - 外部函数
void BSTree::removeNode(DataType x)
{
BSTNode *z, *node;
if((z = searchNode(root, x)) != NULL)
if((node = removeNode(root, z)) != NULL)
delete node;
}
四、二叉查找树的时间复杂度分析
对于一颗二叉查找树,如果是平衡二叉查找树,则n个结点的二叉查找树的高度为log2(n+1),其查找效率为O(log2n),近似与二分查找。如果是完全不平衡的二叉查找树,则其深度可达到n,查找效率为O(n),退化为顺序查找。一般的,二叉查找树的查找性能在O(log2n)和O(n)之间,因此,为了获取更好的性能,就要构造一颗平衡的二叉查找树。
二叉查找树的完事示例代码