二叉树
二叉树是一种特殊的树形结构,其每个节点恰好有两颗可以为空的子树(左子树和右子树),子树左右顺序不能颠倒。
1.二叉树的性质
- 一颗二叉树有\(n\)个元素,\(n>0\),它有\(n-1\)条边。
- 在二叉树的第\(i\)层上,至多有\(2^{i-1}\)个元素。
- 一颗高度为\(h\)的树,其最少有\(h\)个元素,最多有\(2^h-1\)个元素。
- 一颗二叉树有\(n\)个元素,则它的高度最大为n,最小为\(\lceil\log_2(n+1)\rceil\)。
- 对于任何一颗二叉树,如果其终端节点数为\(n_0\),度为2的节点数为\(n_2\),则\(n_0=n_2+1\)。
2.完全二叉树
如上图所示,当一颗高度为h的二叉树恰好有\(2^h-1\)个元素时,称其为满二叉树。我们对高度为\(h\)的满二叉树,从上到下,从左往右依次排序并编号,从\(1\)一直到\(2^h-1\)。假设从满二叉树中删除\(k\)个编号为\(2^h-i\)的元素,\((1\leq i\leq k<2^h)\),所得到的二叉树称为完全二叉树。
满二叉树是完全二叉树的一个特例
对于一个有\(n\)个元素的完全二叉树,其第\(i\)个元素\((1\leq i\leq n)\)有以下关系成立:
- 如果\(i=1\),则该元素为二叉树的跟。若\(i>1\),则其父节点的编号为\(\lfloor i/2\rfloor\)。
- 如果\(2i>n\),则该元素无左子树。否则,其左子树的编号为\(2i\)。
- 如果\(2i+1>n\),则该元素无右子树。否则,其右子树的编号为\(2i+1\)。
3.二叉树的存储结构
对二叉树最常用的描述方法就是指针描述。由二叉树的性质可知,二叉树有两颗子树,用两个指针相连。
//树节点的声明
typedef struct{
}Item; //二叉树的元素类型,由用户自定义
typedef struct tree_node {
Item elem; //节点的数据域
struct tree_node * left; //左子树
struct tree_node * right; //右子树
}TreeNode;
typedef struct {
TreeNode * root; //根节点
unsigned size; //树的大小
}BinaryTree;
这样的二叉树节点用图可表示为:
二叉树还可以用数组描述。我们可以把二叉树当作缺少部分元素的完全二叉树,对每个元素进行编号,然后放入相应的数组位置中
//数组形式的二叉树
typedef struct {
int a;
}Item; //二叉树的元素类型,由用户自定义
typedef struct{
Item elem; //节点的数据域
}BinaryTreeNode;
typedef struct {
BinaryTreeNode * tree;
unsigned size; //树的大小
}BinaryTree;
但这样的二叉树留出没有使用的元素的空间,如果缺少的元素过多,则会浪费大量的空间。但我们可以再在节点中添加左右子树的信息,使得数组二叉树具备紧凑储存的能力
如果节点有左右子树,则将left和right设为对应的下标;如果没有,则设为0。
typedef struct {
}Item; //二叉树的元素类型,由用户自定义
typedef struct{
Item elem; //节点的数据域
int left; //左子树
int right; //右子树
}BinaryTreeNode;
typedef struct {
BinaryTreeNode * tree;
int root; //根节点的位置
unsigned size; //树的大小
}BinaryTree;
4.二叉树的遍历
二叉树的遍历即按固定路径访问树中的每个节点,使每个节点被访问一次,且仅被访问一次。
二叉树的遍历有4种常用的方法:
先序遍历
中序遍历
后序遍历
层次遍历
二叉树的先序遍历使用递归函数定义的话十分简单
//先序遍历
void pre_order(BinaryTreeNode * pbtn, void(*pfun)(Item *))
{
if (pbtn != NULL)
{
pfun(&pbtn->elem); //访问前先节点
pre_order(pbtn->left, pfun); //递归访问左子树
pre_order(pbtn->right, pfun); //递归访问右子树
}
}
中序遍历仅仅是把访问当前节点的次序放在了访问左子树之后
//中序遍历
void in_order(BinaryTreeNode * pbtn, void(*pfun)(Item *))
{
if (pbtn != NULL)
{
in_order(pbtn->left, pfun); //递归访问左子树
pfun(&pbtn->elem); //访问当前节点
in_order(pbtn->right, pfun); //递归访问右子树
}
}
后序遍历同先序和中序
//后序遍历
void post_order(BinaryTreeNode * pbtn, void(*pfun)(Item *))
{
if (pbtn != NULL)
{
post_order(pbtn->left, pfun); //递归访问左子树
post_order(pbtn->right, pfun); //递归访问右子树
pfun(&pbtn->elem); //访问当前节点
}
}
在层次遍历中,树被一层一层地访问,在同一层中,访问顺序从左到右。
void level_order(BinaryTreeNode * pbtn, void(*pfun)(Item *))
{
queue q; //创建一个队列,使用队列辅助层次遍历
while (pbtn != NULL)
{
pfun(&pbtn->elem); //访问当前节点
if (pbtn->left != NULL)
EnQueue(q, pbtn->left); //如果当前节点的左子树不为空,便加入队列
if (pbtn->right != NULL)
EnQueue(q, pbtn->right); //如果当前节点的右子树不为空,便加入队列
if (QueueIsEmpty(q))
pbtn = NULL;
else
pbtn = DeQueue(q);
}
}
对于上图所示的二叉树,它的遍历结果为:
- 先序遍历:R A C F D B G E
- 中序遍历:C F A D R B E G
- 后序遍历:F C D A E G B R
- 层次遍历:R A B C D G F E
5.构造二叉树
二叉树可以用形如\(root(leftChild, rightChild)\),的字符串构造。
bool BuildBinaryTree(BinaryTree * bt, const char * scr)
{
bool isLeft = false;
BinaryTreeNode * root = NULL;
stack s;
if (*scr == '\0')
return false;
else
root = make_node(scr, NULL, NULL);
++scr;
while (*scr != '\0')
{
switch (*scr)
{
case '(':
StackPush(s, root);
isLeft = true;
break;
case ')':
root = StackPop(s);
break;
case ',':
isLeft = false;
break;
default:
root = make_node(scr, NULL, NULL);
if (isLeft)
StackTop(s)->left = root;
else
StackTop(s)->right = root;
break;
}
++scr;
}
destroy_node(bt->root);
bt->root = root;
return true;
}
在上述程序中传入字符串"A(B(C,D(E,)),F(,G(H,I)))",可以完整构建一颗二叉树: