1、二叉树得存储与遍历
1.1二叉树的存储
1.1.1二叉树的顺序存储
顺序存储指的是按照满二叉树的结点层次编号,依次存放二叉树中的数据元素。如下图所示
可以看到二叉树的顺序存储非常浪费空间,如左图,二叉树第五个结点虽然没有存储元素,但是这个结点依然要被占据。右图的单支树就是一种更加极端的情况。
1.1.2二叉树的链式存储
为了改进顺序存储浪费空间的问题,二叉树的存储可以使用链式存储,即使用二叉链表来表示:
左图的二叉树可以由右图的二叉链表表示。此时对每一个结点来说,它包含了左右两个指针域分别指向左孩子和右孩子。如果没有孩子,指针就指向空。接下来我们用代码实现:
typedef struct BiNode
{
TElemType data; // 节点存储的数据
struct BiNode* lchild, * rchild; // 左右孩子指针
} BiNode, * BiTree;
这段代码定义了二叉树的结点。这是一个结构体类型,包含结点存储的数据和左右孩子指针。
使用二叉链表存储二叉树可以有效解决空间浪费问题,因为每个节点只需要两个指针,分别指向左孩子和右孩子。然而,这种存储方式存在一个明显的局限:它无法直接进行回溯操作,即在遍历过程中,我们不知道每个节点的父节点是谁。
为了解决这一问题,我们可以使用三叉链表来存储二叉树。三叉链表在节点中引入了一个额外的指针,用于指向该节点的父节点。这样,每个节点包含三个指针:左孩子、右孩子和父节点,从而在遍历过程中,可以直接回溯到父节点,简化了操作,提升了效率。通过以下图示可以更直观地理解
依然是之前的二叉树,用三叉链表可以将子结点与父结点联系起来。如上图,B结点分别有两个指针指向子节点C和D,同时还有一个指针指向A。下面用代码表示:
typedef struct TriTNode
{
TelemType data; // 节点存储的数据
struct TriTNode* lchild, * parent, * rchild; // 三个指针:左孩子指针、父节点指针、右孩子指针
} TriTNode, * TriTree;
与二叉链表存储的代码很相近,这里只添加了一个指向父结点的指针。
1.2二叉树的遍历
我们先来回顾遍历的含义:
先序遍历:ABDEC
中序遍历:DBEAC
后序遍历:DEBCA
1.2.1遍历算法的实现
以该图为例,先序遍历:ABDC
若二叉树为空则不操作,否则
1.先访问根结点A 2.先遍历左子树B,此时再以B为根结点遍历左子树,然而B的左子树为空,所以遍历B的右子树D 3.最后遍历A的右子树C
中序遍历:BDAC
若二叉树为空,则不操作,否则
1.中序遍历根结点A的左子树B 2.此时以B为根结点,中序遍历B结点的左子树,由于左子树为空,所以第一个结点为B,再遍历右子树D 3.遍历根结点A 4.再遍历A的右子树C
后序遍历:DBCA
若二叉树为空,则不操作,否则
1.后续遍历根结点A的左子树B 2.再以B为根结点,后续遍历左子树和右子树 3.遍历A的右子树C
4.最后遍历根结点A
接下来我们用编程实现先序遍历:
Status PreOrderTraverse(BiTree T)
{
if(T==NULL) return OK;
else
{
visit(T);
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
}
if(T == NULL) return OK;
:这是递归的基准条件。若当前传入的树 T
为空(即为 NULL
),表示该子树不存在,此时递归终止,直接返回 OK
。
visit(T);
:访问当前节点。通常,这里会包含对节点值的操作,比如打印节点值或者执行其他逻辑。PreOrderTraverse(T->lchild);
:递归调用自身,先访问当前节点的左子树。PreOrderTraverse(T->rchild);
:递归调用自身,随后访问当前节点的右子树。- 递归的核心:递归的终止条件是当前子树为空。当遍历到某个节点时,程序会调用
visit(T)
访问该节点,然后递归地遍历它的左子树和右子树。
例题:求二叉树最大深度的递归遍历算法
int deepth(TreeNode* root) {
if(root == NULL)
return 0;
int left = deepth(root->left);
int right = deepth(root->right);
return 1 + max(left, right);
}
-
递归计算左右子树的深度:
int left = deepth(root->left);
:递归计算左子树的深度,函数会沿着左子树的路径继续调用自身,直到叶节点。int right = deepth(root->right);
:递归计算右子树的深度,函数会沿着右子树的路径递归调用自身。
-
返回当前节点的深度:
return 1 + max(left, right);
:当前节点的深度等于左右子树中较大深度的值加上 1。max(left, right)
表示选择左右子树中的最大深度,而加1
表示把当前节点的深度也算入。
对于如下二叉树:
1
/ \
2 3
/
4
对于根节点 1
,递归会计算左右子树:
- 左子树深度为 2(节点 2, 4),右子树深度为 1(节点 3)。
- 根节点的深度是
1 + max(2, 1) = 3
。
例题:写出求二叉树叶子结点个数的递归遍历算法
-
基准条件:
if(T == NULL) return 0;
:如果当前树是空的(即节点为NULL
),返回 0,因为空树没有叶子节点。
-
判断叶子节点:
if (T->lchild == NULL && T->rchild == NULL) return 1;
:如果当前节点没有左子树和右子树,即为叶子节点,则返回 1,表示找到一个叶子节点。
-
递归计算左右子树的叶子节点:
else return LeadCount(T->lchild) + LeadCount(T->rchild);
:如果当前节点不是叶子节点,递归计算其左子树和右子树的叶子节点数量,将结果相加。
1
/ \
2 3
/
4
- 对于根节点
1
,不是叶子节点,递归计算左右子树:- 左子树有叶子节点
4
,返回 1。 - 右子树有叶子节点
3
,返回 1。
- 左子树有叶子节点
最终,返回 1 + 1 = 2
,表示这棵树中有 2 个叶子节点。