图1
存储结构
顺序存储
依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适;但对于一般的二叉树,为让数组和下标与二叉树中结点之间的逻辑关系,只能添加空结点,使用顺序存储会造成大量的空间浪费。
图2
/* 顺序存储代码 */
struct TreeNode{
ElemType value; // 结点中数据元素
bool isEmpty; //结点是否为空
}
链式存储
顺序存储空间利用率低,因此一般采用链式存储.
二叉树的链式存储称为二叉链表,二叉链表至少包含3个域:数据域data、左指针域lchild、右指针域rchild.
图3
/* 链式存储代码 */
typedef struct BiTNode{
ElemTyle data; // 数据域
struct BiTNode *lchild, *rchild; // 左指针域、右指针域
}BiTNode, *BiTree;
补:n个结点的二叉链表共有n+1个空链域
二叉树的遍历
遍历的基本知识
根据根结点、左子树结点、右子树结点的遍历顺序(重要),可分为:
- 先序遍历: 根左右(NLR)
- 中序遍历: 左根右(LNR)
- 后序遍历: 左右根(LRN)
二叉树的遍历没有明确说明,一般是针对二叉链表的遍历
递归遍历
二叉树递归遍历特性:
- 要么是空树
- 要么是二叉树
遍历时要考虑空树的情况
先序递归遍历(PreOrder)
先序递归遍历的思路:
- 若二叉树为空,则什么也不做
- 若二叉树非空,先访问根结点,其次是遍历左子树,最后是遍历右子树
/* 二叉链表先序递归遍历代码 */
void PreOrder(BiTree T){
if (T != NULL){ // 判空操作
visit(T); // 访问根结点
PreOrder(T->lchild); // 遍历左子树
PreOrder(T->rchild); // 遍历右子树
}
}
中序递归遍历(InOrder)
中序递归遍历的思路:
- 若二叉树为空,则什么也不做
- 若二叉树非空,先遍历左子树,其次是访问根结点,最后是遍历右子树
/* 二叉链表中序递归遍历代码 */
void InOrder(BiTree T){
if (T != NULL){ // 判空操作
InOrder(T->lchild); // 遍历左子树
visit(T); // 访问根结点
InOrder(T->rchild); // 遍历右子树
}
}
后序递归遍历(PostOrder)
后序递归遍历的思路:
- 若二叉树为空,则什么也不做
- 若二叉树非空,先遍历左子树,其次是遍历右子树,最后是访问根结点
/* 二叉链表后序递归遍历代码 */
void PostOrder(BiTree T){
if (T != NULL){ // 判空操作
PostOrder(T->lchild); // 遍历左子树
PostOrder(T->rchild); // 遍历右子树
visit(T); // 访问根结点
}
}
三种递归遍历方法总结
- 以上三种遍历方法模式基本一样,无非是访问的顺序不同
- 每个结点有且仅被访问一次
- 时间复杂度为O(n)
非递归遍历
除了递归遍历外,还可以采用非递归的方法遍历二叉链表。通常通过栈来实现非递归的二叉树遍历
先序非递归遍历
先序非递归遍历思路:
- 沿着根的左孩子,依次访问左孩子并入栈,直到左孩子为空
- 栈顶元素出栈:若其右孩子空,则继续执行2;若其右孩子不空,将右子树转执行1
/* 二叉链表先序非递归遍历代码 */
void PreOrder2(BiTree T){
InitStack(S); // 初始化栈S
BiTree p = T; // p是遍历指针
while( p || !IsEmpty(S)){ // 栈不空或p不空时循环
if (p){ // 判空操作 // p指针为非空,一路向左走
visit(p); // 访问当前结点
Push(S, p); // 当前结点入栈
p = p -> lchild; // p指针赋值当前结点的左孩子
} else { // p指针为空,向右子树走
Pop(S, p); // 栈顶元素出栈
p = p -> rchild; // p指针赋值当前结点的右孩子
}
}
}
中序非递归遍历
中序非递归遍历思路:
- 沿着根的左孩子,依次入栈,直到左孩子为空
- 栈顶元素出栈并访问:若其右孩子空,则继续执行2;若其右孩子不空,将右子树转执行1
/* 二叉链表中序非递归遍历代码 */
void InOrder2(BiTree T){
InitStack(S); // 初始化栈S
BiTree p = T; //p是遍历指针
while( p || !IsEmpty(S)){ // 栈不空或p不空时循环
if (p){ // 判空操作 // p指针为非空,一路向左走
Push(S, p); // 当前结点入栈
p = p -> lchild; // p指针赋值当前结点的左孩子
} else { // p指针为空,向右子树走
Pop(S, p); // 栈顶元素出栈
visit(p); // 访问当前结点
p = p -> rchild; // p指针赋值当前结点的右孩子
}
}
}
后序非递归遍历
后序非递归遍历实现是三种非递归遍历中最难的。因为在后序遍历中,要保证左孩子和有孩子的已被访问且左孩子在右孩子前访问,才能访问根结点,这就给流程控制带来了难题。
非递归后序遍历的思路:
- 沿着根的左子树,依次入栈,直到左孩子为空
- 读取栈顶元素;若其右孩子不空且未被访问过,将右子树转执行1;否则,栈顶元素出栈并访问
在上述思路2. 中,必须分清是从左子树返回还是从右子树返回。因此设定一个辅助指针r,指向最近被访问过的结点。
/* 二叉链表后序非递归遍历代码 */
void PostOrder(BiTree T){
InitStack(S); // 初始化栈S
p = T;
r = NULL; // 辅助指针r
while( p || !IsEmpty(S)){ // 栈不空或p不空时循环
if (p){ // 判空操作 // p指针为非空,一路向左走
Push(S, p); // 当前结点入栈
p = p -> lchild; // p指针赋值当前结点的左孩子
} else { // p指针为空,向右子树走
GetTop(S, p); // 读栈顶元素,不是出栈操作
if (p->rchild && p->rchild != r){ // 当前结点的右孩子不空且右孩子未遍历过
p = p -> rchild; // p指针指向当前结点的右孩子
Push(S, p); // 当前结点入栈
p = p -> lchild; // p指针指向当前结点的左孩子
} else { // 当前结点的右孩子为空或右孩子被遍历过
Pop(S, p); // 当前结点出栈
visit(p -> data); // 访问当前结点
r = p; // 辅助指针记录访问过的结点
p = NULL; // 结点访问完后,重置p指针
}
}
}
}
层序遍历
图 6
有时候我们希望能一层一层的遍历二叉树,实现这个需求需要一个队列做为辅助
层序遍历的思路:
- 初始化一个辅助队列
- 根结点入队
- 若队列非空,则队头结点出队,访问该结点,并且其左右孩子插入队尾
- 重复3. 直到队列为空
/* 二叉链表的层序遍历 */
void LevelOrder(BiTree T){
InitQueue(Q); // 初始化队列
BiTree p;
EnQueue(Q, T); // 根结点入队列
while( !IsEmpty(Q)){ // 判断队列是否为空
DeQueue(Q, p); // 队头结点出队列
visit(p); // 访问队头元素
if ( p->lchild != NULL) // 判断左孩子是否不为空
EnQueue(Q, p->lchild); // 左孩子不空,入队列
if ( p->rchild != NULL) // 判断右孩子是否不为空
EnQueue(Q, p->rchild); // 右孩子不空,入队列
}
}
由遍历序列—构造二叉树
若只能给出一棵二叉树的前/中/后/序遍历序列中的一种,不能唯一确定一棵二叉树。如果给出两种遍历序列,就有可能确定一棵二叉树
前序 + 中序遍历序列
后序 + 中序遍历序列
层序 + 中序遍历序列