6-9 二叉树的遍历
- 1 题目原文
- 2 思路解析
- 2.1 层序遍历
- 2.2 先序遍历
- 2.2.1 递归实现
- 2.2.2 使用栈将递归转为迭代
- 2.2.3 Morris 前序遍历
- 2.3 中序遍历
- 2.3.1 递归实现
- 2.3.2 使用栈将递归转为迭代
- 2.3.3 Morris 中序遍历
- 2.4 后序遍历
- 2.4.1 递归实现
- 2.4.2 使用栈将递归转为迭代
- 2.4.3 Morris 后序遍历
- 2.4.4 使用栈的序列转换
- 3 代码实现
- 3.1 层序遍历
- 3.2 前中后序遍历
- 3.2.1 递归
- 3.2.2 使用栈的迭代法
- 3.2.3 Morris遍历
- 4 总结
1 题目原文
题目链接:6-9 二叉树的遍历
本题要求给定二叉树的 4
种遍历。
函数接口定义:
void InorderTraversal( BinTree BT );
void PreorderTraversal( BinTree BT );
void PostorderTraversal( BinTree BT );
void LevelorderTraversal( BinTree BT );
其中 BinTree
结构定义如下:
typedef struct TNode *Position;
typedef Position BinTree;
struct TNode{ElementType Data;BinTree Left;BinTree Right;
};
要求 4
个函数分别按照访问顺序打印出结点的内容,格式为一个空格跟着一个字符。
裁判测试程序样例:
#include <stdio.h>
#include <stdlib.h>typedef char ElementType;
typedef struct TNode *Position;
typedef Position BinTree;
struct TNode{ElementType Data;BinTree Left;BinTree Right;
};BinTree CreatBinTree(); /* 实现细节忽略 */
void InorderTraversal( BinTree BT );
void PreorderTraversal( BinTree BT );
void PostorderTraversal( BinTree BT );
void LevelorderTraversal( BinTree BT );int main()
{BinTree BT = CreatBinTree();printf("Inorder:"); InorderTraversal(BT); printf("\n");printf("Preorder:"); PreorderTraversal(BT); printf("\n");printf("Postorder:"); PostorderTraversal(BT); printf("\n");printf("Levelorder:"); LevelorderTraversal(BT); printf("\n");return 0;
}
/* 你的代码将被嵌在这里 */
输出样例(对于图中给出的树):
Inorder: D B E F A G H C I
Preorder: A B D F E C G H I
Postorder: D E F B H G I C A
Levelorder: A B C D F G I E H
代码长度限制 16 KB
时间限制 400 ms
内存限制 64 MB
2 思路解析
二叉树的遍历是二叉树中最基本最重要的知识之一,很多关于二叉树的题目都可以转化为二叉树的遍历来解决,这里总结一下二叉树的 4
种基本遍历。
我们知道,图有两种基本的遍历方式:广度优先 BFS
和深度优先 DFS
,而树是特殊的图,自然也有这两种遍历方式,二叉树是特殊的树,也有这两种遍历方式,甚至因为其特殊性,还在这两种遍历方式的基础上衍生出了其它的的遍历方式(先序,中序,后序),甚至你想怎么遍历,就自己定义一种顺序,比如先遍历右子树,再遍历根结点,再遍历左子树,等等,但是这些遍历方式都可以根据基本的 4
种遍历演变而来。
2.1 层序遍历
层序遍历属于广度优先搜索。其思想就是一层一层地遍历树的所有结点,这需要另一种数据结构【队列】的辅助,具体步骤如下:
1. 将根结点放入队列中;
2. 如果队列不为空,做下列操作:
2.1 取出队首元素,记录或输出(这一步就是遍历到该元素时对该元素的操作)
2.2 如果队首元素有左右孩子,则将其左右孩子加入队列中,转 2
上面的步骤是简化后的算法步骤,只适用于得到层序遍历的顺序,但不能体现出“层序”关系,实用性更好一些的是在文章【函数题】6-8 求二叉树高度中介绍的广度优先搜索,其算法步骤如下:
1. 将根结点放入队列中;
2. 如果队列不为空,做下列操作:
2.1 获取队列元素个数 n
2.2 对这 n
个元素做以下操作:
2.2.1 取出队首元素,记录或输出(这一步就是遍历到该元素时对该元素的操作)
2.2.2 如果队首元素有左右孩子,则将其左右孩子加入队列中
2.3 转 2
这便是二叉树的层序遍历,伪代码和文章【函数题】6-8 求二叉树高度中的伪代码类似,这里不赘述。
2.2 先序遍历
先序遍历的定义是:先遍历根,其次是左子树,最后遍历右子树。
根据其定义,一个很直观的解法就浮现出来了:递归;
然而人们普遍认为递归的效率不高,所以一般会将其转为迭代的方式,用栈模拟递归过程,这需要用到栈作为辅助空间;
然而,有人认为用栈开辟了辅助空间,还是不够优秀,如果能将二叉树中空闲的指针利用起来,那该多好啊,所以一种空间复杂度为 O ( 1 ) O(1) O(1) 的 Morris
遍历算法就出现了。
这里介绍以上 3
种先序遍历算法。
2.2.1 递归实现
这种方法相当直观,这里直接给出伪代码:
f(A):if A == NULLreturn对 A 进行操作f(A的左子树)f(A的右子树)
2.2.2 使用栈将递归转为迭代
递归和迭代大多数情况下都可以相互转换,用递归法对二叉树进行先序遍历也可以转化为使用栈的迭代法。其具体做法如下:
1. 根结点入栈;
2. 如果栈不为空,进行以下操作:
2.1 取出栈顶元素,记录或输出栈顶元素;
2.2 如果栈顶元素有右儿子,将其右儿子入栈;
2.3 如果栈顶元素有左儿子,将其左儿子入栈;
伪代码如下:
f(A):if A == NULL:return# 定义一个栈,记为stackstack = (stack)stack.push(A)while stack not empty:top = stack.top对 top 进行操作(遍历 top 结点)stack.popif top.left != NULL:stack.push(top.left)if top.right != NULL:stack.push(top.right)
上面的方法简单且易于理解,利用了栈的先进后出特性,使用栈的迭代法还有其它变种思路,这里介绍一种:
1. 定义一个指针 p
指向根结点;
2. 如果 p
不为空指针或栈不为空,进行以下操作:
2.1 当指针 p
不为空,进行下面的操作:
2.1.1 记录或输出栈顶元素;
2.1.2 将 p
指针所指结点入栈;
2.1.2 p
指针更新为其左儿子;
2.2 p
指针指向栈顶元素,栈顶元素出栈;
2.3 p
指针更新为其右儿子;
这种方法基于 回溯
思想。
以上就是针对二叉树的前序遍历的基于栈的迭代算法,主要记录了两种常见思路,第一种思路的倾向性更强,但只适合先根结点再子树的遍历顺序,第二种思路基于回溯思想,适合二叉树的前序中序后序三种遍历,后面也会再次体现。
伪代码如下:
f(A):# 定义一个指针指向根结点ptr = A# 定义一个栈,记为stackstack = (stack)while ptr != NULL or stack not empty:while ptr != NULL:对 ptr 指向的结点进行操作(遍历)stack.push(ptr)ptr = ptr.leftptr = stack.topstack.pop()ptr = ptr.right
2.2.3 Morris 前序遍历
无论是递归法还是迭代法,都需要分配额外的辅助空间,比如递归栈的消耗以及显示地分配栈空间以迭代等。但是经过观察可知,一棵链式存储的二叉树中,除了真实的指向左右儿子的指针以外,其余的都是空指针,而空指针的数量经推算是 n + 1 n+1 n+1 个,其中 n n n 表示二叉树的结点个数。
而对于二叉树来说,“后继”结点或者“前驱”结点最多只有 n − 1 n-1 n−1 个(有 n n n 个结点的二叉树,其中能被称为“前驱结点”或“后继结点”的结点数最多 n − 1 n-1 n−1 个),那么在遍历过程中我们可否利用这些空指针来临时性地指向某个结点的前驱或者后继结点呢?答案是可行的,并且已经有现成的算法,就是接下来要介绍的 Morris
前序遍历算法。其具体思路如下:
1. 定义指针 cur
指向根结点,cur
表示“当前结点”
2. 当 cur
不为空时,执行以下步骤:
2.1 如果 cur
没有左儿子:
2.1.1 访问 cur
结点
2.1.2 将 cur
移动到其右儿子
2.2 如果 cur
有左儿子:
2.2.1 找到 cur
在中序遍历下的前驱节点 pre
:
2.2.1.1 pre = cur.left
2.2.1.2 当 pre.right
不为空且 pre.right != cur
:
2.2.1.2.1 pre = pre.right
2.2.2 如果 pre.right
为空:
2.2.2.1 访问 cur
结点
2.2.2.2 将 pre.right
指向 cur
(创建线索)
2.2.2.3 将 cur
移动到其左儿子
2.2.3 如果 pre.right = cur
:
2.2.3.1 断开线索(pre.right = NULL
)
2.2.3.2 将 cur
移动到其右儿子(cur = cur.right
)
该算法中蕴含了“线索化”二叉树的思想,利用了空闲的指针来辅助遍历二叉树,从而避免了额外的辅助空间开销,空间复杂度为 O ( 1 ) O(1) O(1)。
其伪代码如下:
f(A):cur = Awhile cur != NULL:if cur.left == NULL:对 cur 结点进行操作(遍历)cur = cur.rightelse:pre = cur.leftwhile pre.right != NULL and pre.right != cur:pre = pre.rightif pre.right == NULL:对 cur 结点进行操作(遍历)pre.right = curcur = cur.leftelse:pre.right = NULLcur = cur.right
由于这种遍历方法会修改树结构,所以只适合遍历树或者在遍历的过程中做一些可控的操作,如果涉及到中途退出,那这种方法会使指针指向错乱,最终导致内存泄漏。
2.3 中序遍历
2.3.1 递归实现
这种方法相当直观,这里直接给出伪代码:
f(A):if A == NULLreturnf(A的左子树)对 A 进行操作f(A的右子树)
2.3.2 使用栈将递归转为迭代
类似于前序遍历,中序遍历的非递归算法具体思路如下:
1. 定义一个指针 p
指向根结点;
2. 初始化一个空栈 s
;
3. 如果 p
不为空指针或栈 s
不为空,进行以下操作:
3.1 当指针 p
不为空,进行下面的操作:
3.1.1 将 p
指针所指的结点入栈;
3.1.2 p
指针更新为其左儿子;
3.2 如果 p
为空,则进行以下操作:
3.2.1 从栈 s
中弹出一个元素,将 p
指向该元素;
3.2.2 记录或输出 p
指针所指结点的值;
3.2.3 p
指针更新为其右儿子;
伪代码省略。
2.3.3 Morris 中序遍历
二叉树的中序遍历和前序遍历有较强的关联,因为前序遍历是 根 -> 左 -> 右
的顺序,中序遍历是 左 -> 根 -> 右
的顺序,而回溯可以形成 根 -> 左 -> 根
的顺序,所以可以很方便地完成前序和中序遍历。Morris
遍历也类似,只是在不同的时机访问结点:
1. 定义指针 cur
指向根结点,cur
表示“当前结点”
2. 当 cur
不为空时,执行以下步骤:
2.1 如果 cur
没有左儿子:
2.1.1 访问 cur
结点
2.1.2 将 cur
移动到其右儿子
2.2 如果 cur
有左儿子:
2.2.1 找到 cur
在中序遍历下的前驱节点 pre
:
2.2.1.1 pre = cur.left
2.2.1.2 当 pre.right
不为空且 pre.right != cur
:
2.2.1.2.1 pre = pre.right
2.2.2 如果 pre.right
为空:
2.2.2.1 将 pre.right
指向 cur
(创建线索)
2.2.2.2 将 cur
移动到其左儿子
2.2.3 如果 pre.right = cur
:
2.2.3.1 访问 cur
结点
2.2.3.2 断开线索(pre.right = NULL
)
2.2.3.3 将 cur
移动到其右儿子(cur = cur.right
)
伪代码与前序遍历几乎一致,故省略。
2.4 后序遍历
观察前面的二叉树前序遍历和中序遍历算法,可以看出来,二叉树的中序遍历和前序遍历的非递归算法只是在不同的时机访问结点,其代码逻辑几乎一样。但是后序遍历比较特殊,除了递归写法简单易懂外,非递归的写法和前序中序不同,甚至有些麻烦。
下面就介绍一下几种常见的二叉树后序遍历算法。
2.4.1 递归实现
递归算法是最直观的,前中后三种遍历算法的递归算法都类似,这里也不作说明,直接给出伪代码:
f(A):if A == NULLreturnf(A的左子树)f(A的右子树)对 A 进行操作
2.4.2 使用栈将递归转为迭代
使用栈的后序遍历的非递归算法具体思路如下:
1. 定义一个指针 p
指向根结点;
2. 初始化一个空栈 s
;
3. 定义一个指针 last
用于记录上次访问的结点,初始化为空;
4. 如果 p
不为空指针或栈 s
不为空,进行以下操作:
4.1 当指针 p
不为空,进行下面的操作:
4.1.1 将 p
指针所指的结点入栈;
4.1.2 p
指针更新为其左儿子;
4.2 如果 p
为空,则进行以下操作:
4.2.1 从栈 s
中取出栈顶元素(不弹出),将 p
指向该元素;
4.2.2 如果 p
的右儿子为空或右儿子已经被访问过(即 p->right = last
),则:
4.2.2.1 从栈 s
中弹出栈顶元素;
4.2.2.2 记录或输出 p
指针所指结点的值;
4.2.2.3 将 last
更新为 p
;
4.2.2.4 将 p
置为空;
4.2.3 否则,将 p
指针更新为其右儿子;
其伪代码如下:
f(A):p = Alast = NULLstack = (stack)while p != NULL or stack is not empty:while p != NULL:stack.push(p)p = p.leftp = stack.top()if p.right == NULL or p.right == last:stack.pop()对 p 结点进行操作last = pp = NULLelse:p = p.right
2.4.3 Morris 后序遍历
二叉树的 Morris
后序遍历比较复杂,以下是算法描述:
1. 定义指针 cur
指向根结点,并创建虚拟结点 dummy
,使其左子结点指向 cur
2. 初始化临时结点指针 pre
和结果收集列表
3. 当 cur
不为空时,循环执行:
3.1 如果 cur
的左子结点为空:
3.1.1 cur
移动到右子结点(cur = cur.right
)
3.2 否则:
3.2.1 找到 cur
的左子树的最右结点 pre
:
3.2.1.1 pre = cur.left
3.2.1.2 当 pre.right
存在且不等于 cur
时:
3.2.1.2.1 pre = pre.right
3.2.2 若 pre.right
为空:
3.2.2.1 建立线索:pre.right = cur
3.2.2.2 cur
移动到左子结点(cur = cur.left
)
3.2.3 若 pre.right == cur
:
3.2.3.1 断开线索:pre.right = NULL
3.2.3.2 逆序收集路径:从 cur.left
到 pre
的所有结点
3.2.3.3 cur
移动到右子结点(cur = cur.right
)
4. 最终处理:逆序收集从 dummy.left
到原根结点的路径
2.4.4 使用栈的序列转换
这里记录一种”就题论题“的算法,即只看题面”输出二叉树的后序遍历序列“,其实我们没有必要真正地去后序遍历一遍二叉树,因为它的非递归算法相比而言比较复杂(而我们基本不情愿使用递归算法),此时可以根据某些遍历顺序得到二叉树的遍历序列,然后对这个序列进行操作以得到”后序遍历序列“。
首先,按照”逆前序“的顺序遍历二叉树,这是最简单直观的非递归思路,得到序列 A
,然后你会惊讶地发现:A
和后序序列恰好相反。所以再反转一下 A
即可。
所谓的”逆前序“顺序,就是 根 -> 右 -> 左
的顺序。比如题目示例的二叉树:
其”逆前序“遍历的结果为:A C I G H B F E D
后序遍历的结果为:D E F B H G I C A
至于怎么得到逆前序遍历的序列,则可以使用上面介绍的前序遍历算法,将左右结点交换一下顺序即可。算法描述与伪代码这里省略。
3 代码实现
下面给出上述算法思路的 C
语言代码实现。上面的算法会用到两个数据结构:栈
和 队列
,但不幸的是,C
语言并没有相关的库以供调用,所以只好自己实现了。具体实现见 【数据结构 C 语言实现】栈和队列
// 注意要将存储数据的类型修改为题目所需的类型(C 语言特有的“泛型”)
#define ERROR NULL
typedef BinTree ElemType;
3.1 层序遍历
// 就遍历本身
void LevelorderTraversal(BinTree BT) {CircularQueue *q = create_circular_queue(100);if (BT) {BinTree p = NULL;enqueue(q, BT);while (!is_empty(q)) {p = dequeue(q);printf(" %c", p->Data);if (p->Left) {enqueue(q, p->Left);}if (p->Right) {enqueue(q, p->Right);}}}
}
// 体现“层序”
void LevelorderTraversal(BinTree BT) {CircularQueue *q = create_circular_queue(100);if (BT) {BinTree p = NULL;int size = 0;enqueue(q, BT);while (!is_empty(q)) {size = get_queue_size(q);while (size--) {p = dequeue(q);printf(" %c", p->Data);if (p->Left) {enqueue(q, p->Left);}if (p->Right) {enqueue(q, p->Right);}}}}
}
3.2 前中后序遍历
3.2.1 递归
void InorderTraversal(BinTree BT) {if (BT) {InorderTraversal(BT->Left);printf(" %c", BT->Data);InorderTraversal(BT->Right);}
}
void PreorderTraversal(BinTree BT) {if (BT) {printf(" %c", BT->Data);PreorderTraversal(BT->Left);PreorderTraversal(BT->Right);}
}
void PostorderTraversal(BinTree BT) {if (BT) {PostorderTraversal(BT->Left);PostorderTraversal(BT->Right);printf(" %c", BT->Data);}
}
3.2.2 使用栈的迭代法
栈的使用方法前面已经说了,这里默认已经实现了栈,只是调用。
void InorderTraversal(BinTree BT) {Stack *stk = create_stack(100);BinTree p = BT;while (p || !is_stack_empty(stk)) {while (p) {push(stk, p);p = p->Left;}p = pop(stk);printf(" %c", p->Data);p = p->Right;}destroy_stack(stk);
}
void PreorderTraversal(BinTree BT) {Stack *stk = create_stack(100);BinTree p = BT;while (p || !is_stack_empty(stk)) {while (p) {printf(" %c", p->Data);push(stk, p);p = p->Left;}p = pop(stk);p = p->Right;}destroy_stack(stk);
}
void PostorderTraversal(BinTree BT) {Stack *stk = create_stack(100);BinTree p = BT, q = NULL;while (p || !is_stack_empty(stk)) {while (p) {push(stk, p);p = p->Left;}p = peek(stk);if (!p->Right || p->Right == q) {pop(stk);printf(" %c", p->Data);q = p;p = NULL;} else {p = p->Right;}}destroy_stack(stk);
}
前序遍历的另一种实现
void PreorderTraversal(BinTree BT) {if (BT) {Stack *stk = create_stack(100);push(stk, BT);BinTree p = NULL;while (!is_stack_empty(stk)) {p = pop(stk);printf(" %c", p->Data);if (p->Right) {push(stk, p->Right);}if (p->Left) {push(stk, p->Left);}}destroy_stack(stk);}
}
使用序列转换的后序遍历,这种方法需要一个数组来暂存结果
void PostorderTraversal(BinTree BT) {if (BT) {Stack *stk = create_stack(100);char *tmp = (char *)malloc(100 * sizeof(char));int i = 0;push(stk, BT);BinTree p = NULL;while (!is_stack_empty(stk)) {p = pop(stk);tmp[i++] = p->Data;if (p->Left) {push(stk, p->Left);}if (p->Right) {push(stk, p->Right);}}i--;while (i >= 0) {printf(" %c", tmp[i--]);}free(tmp);destroy_stack(stk);}
}
3.2.3 Morris遍历
这类方法不需要栈
void InorderTraversal(BinTree BT) {Position cur = BT, p = NULL;while (cur) {if (!cur->Left) {printf(" %c", cur->Data);cur = cur->Right;} else {p = cur->Left;while (p->Right && p->Right != cur) {p = p->Right;}if (!p->Right) {p->Right = cur;cur = cur->Left;} else {printf(" %c", cur->Data);p->Right = NULL;cur = cur->Right;}}}
}
void PreorderTraversal(BinTree BT) {Position cur = BT, p = NULL;while (cur) {if (!cur->Left) {printf(" %c", cur->Data);cur = cur->Right;} else {p = cur->Left;while (p->Right && p->Right != cur) {p = p->Right;}if (!p->Right) {printf(" %c", cur->Data);p->Right = cur;cur = cur->Left;} else {p->Right = NULL;cur = cur->Right;}}}
}
// 反转链表(用于后序遍历)
void reverse(Position from, Position to) {if (from == to) return;Position x = from, y = from->Right, z;while (x != to) {z = y->Right;y->Right = x;x = y;y = z;}
}// 打印链表(用于后序遍历)
void print_reverse(Position from, Position to) {reverse(from, to);Position p = to;while (1) {printf(" %c", p->Data);if (p == from) break;p = p->Right;}reverse(to, from);
}// Morris 后序遍历
void PostorderTraversal(BinTree BT) {Position dummy = (Position)malloc(sizeof(struct TNode)); // 虚拟节点dummy->Left = BT;Position cur = dummy, p = NULL;while (cur) {if (!cur->Left) {cur = cur->Right;} else {p = cur->Left;while (p->Right && p->Right != cur) {p = p->Right;}if (!p->Right) {p->Right = cur;cur = cur->Left;} else {print_reverse(cur->Left, p); // 打印左子树到前驱节点的路径p->Right = NULL;cur = cur->Right;}}}free(dummy); // 释放虚拟节点
}
4 总结
二叉树的 4
种遍历有多种实现方式,这里简单记录了几种,其中,对于层序遍历,套路几乎是固定的,而对于前中后三种遍历,有递归迭代两类,由于递归总是有个“低效率”的标签,所以人们更倾向于迭代法,迭代法又有使用栈和不使用栈两种方法。
实际使用时大多使用栈迭代,这种方法比较折中。