1、前言
一直以来,我都想让二叉树非递归的遍历算法深入我心,但是屡屡失败。为了加深自己的印象,我觉得有必要记录自己的实现思路。LeetCode 的解答区有很多大佬分享自己的解法,它们各有各的特点。在本文中,我对四种遍历都选取了自己比较中意的实现方案。在这之中,前序遍历和层序遍历的非递归实现比较容易,中序遍历次之,后序遍历理解起来难一些。而理解这些算法的最好方式就是画一棵二叉树按照代码的执行顺序模拟一遍。下面是这些方案的实现记录,希望能带给大家一些启发。
2、前序遍历和层序遍历
将前序遍历和层序遍历放在一起的原因,是因为它两非递归的代码实现思路大致相同。区别在于,前者使用的是栈,后者使用的是队列;’前者先将右孩子放入栈,而后者先将左孩子放入队列。
这里我用 LeetCode 原题作为基准完成代码:
前序遍历
层序遍历
前序遍历非递归实现代码:
public List<Integer> preOrder(TreeNode root){
List<Integer> res = new LinkedList<>();
if(root == null){
return res;
}
Deque<TreeNode> stack = new LinkedList<>(); // Java 官方推荐用 Deque 接口使用栈,
// 具体细节可参考这篇文章:https://mp.weixin.qq.com/s/Ba8jrULf8NJbENK6WGrVWg
stack.addFirst(root);
while(!stack.isEmpty()){
TreeNode node = stack.removeFirst();
res.add(node.val);
if(node.right != null){ // 先将右孩子放入队列
stack.addFirst(node.right);
}
if(node.left != null){ // 再将左孩子放入队列
stack.addFirst(node.left);
}
}
return res;
}
层序遍历实现代码:
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new LinkedList<>();
if(root == null){
return res;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
int size = queue.size(); // 注意 size 必须在这里固定,切勿写在下面的 for 循环中
List<Integer> list = new LinkedList<>();
for(int i = 0; i < size; i++){
TreeNode node = queue.remove();
list.add(node.val);
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
}
res.add(list);
}
return res;
}
3、中序遍历
根据中序遍历的特点,我们先从根节点遍历到树的最左下边,若有右孩子,则再以该右孩子为根,遍历到这棵子树的最左下边,如此反复即可。在这其中,我们需要用栈存储未被访问(出栈的元素才能算是被访问的元素)的节点数据。
LeetCode 原题链接:中序遍历
中序遍历的非递归实现:
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
if(root == null){
return res;
}
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root; // 指针 cur 代表当前遍历到的节点
while(!stack.isEmpty() || cur != null){ // 这里的 cur != null 仅在第一步有用
while(cur != null){
stack.addFirst(cur);
cur = cur.left;
}
TreeNode node = stack.removeFirst(); // 说明循环到了最左下边的节点,访问该节点,将其出栈
res.add(node.val);
cur = node.right; // 这句话是理解的关键。如果出栈的节点有右孩子,则回到上边继续循环;如果没有,上面的 while 循环也会被跳过继续让元素出栈
}
return res;
}
4、后序遍历
与中序遍历相比,由于后序遍历需要先访问右子树在访问根节点,所以需要一个 lastVisit 变量记录已经被访问的节点,如果右子树被访问过,则不用再继续往下遍历了。
LeetCode 原题链接:后序遍历
后序遍历的非递归实现:
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
if(root == null){
return res;
}
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
TreeNode lastVisit = null;
while(!stack.isEmpty() || cur != null){
while(cur != null){
stack.addFirst(cur);
cur = cur.left;
}
TreeNode node = stack.removeFirst();
if(node.right == null || lastVisit == node.right){
res.add(node.val); // 访问该节点
lastVisit = node; // 记录被访问过的节点
cur = null; // 置为 null 的原因是当前访问的子树,它的根节点都已经被访问了,后序遍历中根节点是最后一个被访问的元素,所以接下来需要元素继续出栈。
}else{
stack.addFirst(node); // 右边还有没被访问的,所以要把该节点压入栈中
cur = node.right; // 继续遍历以 node.right 为根的子树
}
}
return res;
}
5、总结
在 LeetCode 原题中,关于这四种遍历方式很多大佬给出了不一样的解答方案,比如这种通用的遍历方法,大家可以去学习。在本文中,除了层序遍历和前序遍历,其余两种遍历的方案都符合递归直觉,也就是用迭代的方式完成了递归的遍历顺序。这样做虽然在一定程度上增加了理解难度,但正因为它的遍历顺序模仿了递归,所以也不失为是一种更容易理解的方案。