17. 二叉树
一、什么是二叉树
二叉树 T 是一个有穷的结点集合。这个结合可以为空。若不为空,则它是由 根结点 和称为其 左子树 \(T_{L}\) 和 右子树 $T_{R} $ 的两个不相交的二叉树组成。二叉树是一颗度为 2 的树,并且二叉树的子树有左右之分。
一些比较常见的特殊二叉树如下:
二叉树具有以下几个重要性质:
- 一个二叉树第 i 层的最大结点数为:\(2^{i-1}, i ≥ 1\)。
- 深度为 k 的二叉树有最大结点总数为:\(2^{k} - 1, k ≥ 1\)。
- 对任何非空二叉树 T,若 \(n_{0}\) 表示叶节点的个数、\(n_{2}\) 是度为 2 的非叶节点个数,那么两者满足关系 \(n_{0} = n_{2} + 1\)。
ADT BinaryTree
{
Data:
二叉树T∈BinaryTree, Item∈ElementType,
Operation:
void PreOrderTraverse(BinaryTree T); // 先序遍历
void InOrderTraverse(BinaryTree T); // 中序遍历
void PostOrderTraverse(BinaryTree T); // 后序遍历
void LevelOrderTraverse(BinaryTree T); // 层次遍历
} ADT BinaryTree;
二、二叉树的存储结构
2.1、顺序存储实现
一般的二叉树,我们可以将其补齐为完全二叉树。完全二叉树按从上至下、从左到右顺序存储,n 个结点的完全二叉树的结点父子关系如下:
- 非根结点(序号 i>1)的父结点的序列是 \(\lfloor i/2 \rfloor\)。
- 结点(序号为 i)的左孩子结点的序号是 2i(若 2i ≥ n,没有左孩子)。右孩子结点的序号是 2i+1(若 2i+1 ≥ n,没有右孩子)。
用顺序存储的方式存储一般的二叉树时,会造成大量的空间浪费。
2.2、链式存储实现
#include <stdio.h>
#include <stdlib.h>
typedef int ElementType;
typedef struct TreeNode
{
ElementType Data;
struct TreeNode * Left;
struct TreeNode * Right;
} TreeNode, *BinaryTree;
三、二叉树的遍历
先序、中序和后序遍历过程中经过结点的路线一样,只是访问各结点的时机不同。
3.1、先序遍历
先序遍历过程如下:
- 访问根结点。
- 先序遍历其左子树。
- 先序遍历其右子树。
/**
* @brief 先序遍历二叉树
*
* @param T 二叉树
*/
void PreOrderTraversal(BinaryTree T)
{
if (T)
{
printf("%d ", T->Data);
PreOrderTraversal(T->Left);
PreOrderTraversal(T->Right);
}
}
我们也可以使用栈来模拟先序遍历的递归过程:
- 遇到一个结点就直接访问它,然后把它压栈,并去遍历它的左子树。
- 当左子树遍历结束后,从栈顶弹出结点。
- 然后按其右指针再去先序遍历该结点的右子树。
/**
* @brief 中序遍历二叉树
*
* @param T 二叉树
*/
void InOrderTraversal(BinaryTree T)
{
BinaryTree Tree = T;
Stack S = CreateStack(); // 创建栈
while (Tree || !IsEmpty(S))
{
while (Tree) // 一直向左并将沿途结点压入栈中
{
printf("%d ", Tree->Data);
Push(S, Tree);
Tree = Tree->Left;
}
if (!IsEmpty(S))
{
Tree = Pop(S); // 弹出栈顶结点,访问该结点
Tree = Tree->Right; // 转向右子树
}
}
}
3.2、中序遍历
中序遍历过程如下:
- 中序遍历其左子树。
- 访问根结点。
- 中序遍历其右子树。
/**
* @brief 中序遍历二叉树
*
* @param T 二叉树
*/
void InOrderTraversal(BinaryTree T)
{
if (T)
{
InOrderTraversal(T->Left);
printf("%d ", T->Data);
InOrderTraversal(T->Right);
}
}
我们也可以使用栈来模拟中序遍历的递归过程:
- 遇到一个结点,就把它压栈,并去遍历它的左子树。
- 当左子树遍历结束后,从栈顶弹出结点并访问它。
- 然后按其右指针再去中序遍历该结点的右子树。
/**
* @brief 中序遍历二叉树
*
* @param T 二叉树
*/
void InOrderTraversal(BinaryTree T)
{
BinaryTree Tree = T;
Stack S = CreateStack(); // 创建栈
while (Tree || !IsEmpty(S))
{
while (Tree) // 一直向左并将沿途结点压入栈中
{
Push(S, Tree);
Tree = Tree->Left;
}
if (!IsEmpty(S))
{
Tree = Pop(S); // 弹出栈顶结点,访问该结点
printf("%d ", Tree->Data);
Tree = Tree->Right; // 转向右子树
}
}
}
3.3、后序遍历
后序遍历过程如下:
- 后序遍历其左子树。
- 后序遍历其右子树。
- 访问根结点。
/**
* @brief 后序遍历二叉树
*
* @param T 二叉树
*/
void PostOrderTraversal(BinaryTree T)
{
if (T)
{
PostOrderTraversal(T->Left);
PostOrderTraversal(T->Right);
printf("%d ", T->Data);
}
}
我们也可以使用栈来模拟后序遍历的递归过程:
- 遇到一个结点,就把它压栈,并去遍历它的左子树。
- 当左子树遍历结束后,从获取栈顶元素,并判断它的右子树是否处理完成。
- 如果右子树没有处理完成,按其右指针再去后序遍历该结点的右子树。
- 如果右子树处理完成,则访问该结点。
/**
* @brief 后序遍历二叉树
*
* @param T 二叉树
*/
void PostOrderTraversal(BinaryTree T)
{
BinaryTree Tree = T;
BinaryTree pop = NULL, peek = NULL;
Stack S = CreateStack(); // 创建栈
while (Tree || !IsEmpty(S))
{
while (Tree) // 一直向左并将沿途结点压入栈中
{
Push(S, Tree);
Tree = Tree->Left;
}
if (!IsEmpty(S))
{
peek = Peek(S); // 访问栈顶结点
if (peek->Right == NULL || peek->Right == pop) // 没有右子树或者右子树处理完毕
{
pop = Pop(S);
printf("%d ", pop->Data);
}
else
{
Tree = peek->Right; // 转向右子树
}
}
}
}
3.4、层序遍历
层序遍历可以用队列实现,遍历从根结点开始,首先将根结点入队,然后开始执行循环,结点出队、访问该结点、其左右儿子入队。
/**
* @brief 层序遍历二叉树
*
* @param T 二叉树
*/
void LevelOrderTraversal(BinaryTree T)
{
BinaryTree Tree = T;
Queue Q = NULL;
if (Tree)
{
return;
}
Q = CreateQueue();
Enqueue(Q, Tree);
while (!IsEmpty(Q))
{
Tree = Dequeue(Q);
printf("%d ", Tree->Data);
if (Tree->Left)
{
Enqueue(Q, Tree->Left);
}
if (Tree->Right)
{
Enqueue(Q, Tree->Right);
}
}
}
四、遍历的应用
4.1、先序和中序确定二叉树
- 根据先序遍历序列的第一个结点确定根结点。
- 根据根结点在中序遍历序列中分割处左右两个子序列。
- 对左子树和右子树分别递归使用相同的方法继续分解。
/**
* @brief 根据先序和中序序列创建二叉树
*
* @param PreOrder 先序序列
* @param InOrder 中序序列
* @param Length 序列的长度
* @return BinaryTree 指向二叉树的指针
*/
BinaryTree CreakeBinaryTreeByPreOrderAndInOrder(ElementType * PreOrder, ElementType * InOrder, int Length)
{
if (Length == 0)
{
return NULL;
}
// 先序遍历的第一个结点就是根结点
ElementType RootData = PreOrder[0];
BinaryTree Root = (BinaryTree)malloc(sizeof(TreeNode));
Root->Data = RootData;
for (int i = 0; i < Length; i++) // 区分左右子树
{
if (InOrder[i] == RootData)
{
// Inorder序列中从0到i-1是左子树,从i+1的InOrderLength-1是右子树
Root->Left = CreakeBinaryTreeByPreOrderAndInOrder(PreOrder + 1, InOrder, i);
Root->Right = CreakeBinaryTreeByPreOrderAndInOrder(PreOrder + i + 1, InOrder + i + 1, Length - i - 1);
break;
}
}
return Root;
}
4.2、中序和后序确定二叉树
- 根据后序遍历序列的最后一个结点确定根结点。
- 根据根结点在中序遍历序列中分割处左右两个子序列。
- 对左子树和右子树分别递归使用相同的方法继续分解。
/**
* @brief 根据中序和后序序列创建二叉树
*
* @param InOrder 中序序列
* @param PostOrder 后序序列
* @param Length 序列的长度
* @return BinaryTree 指向二叉树的指针
*/
BinaryTree CreakeBinaryTreeByInOrderAndPostOrder(ElementType * InOrder, ElementType * PostOrder, int Length)
{
if (Length == 0)
{
return NULL;
}
// 后序遍历的最后一个结点就是根结点
ElementType RootData = PostOrder[Length - 1];
BinaryTree Root = (BinaryTree)malloc(sizeof(TreeNode));
Root->Data = RootData;
for (int i = 0; i < Length; i++) // 区分左右子树
{
if (InOrder[i] == RootData)
{
// Inorder序列中从0到i-1是左子树,从i+1的InOrderLength-1是右子树
Root->Left = CreakeBinaryTreeByInOrderAndPostOrder(InOrder, PostOrder, i);
Root->Right = CreakeBinaryTreeByInOrderAndPostOrder(InOrder + i + 1, PostOrder + i, Length - i - 1);
break;
}
}
return Root;
}