概要
本题的核心旨在加强对二叉树的理解以及对深度优先搜索DFS以及广度优先搜索BFS的理解,理解本篇结合已经发过的LeetCode进阶226-翻转二叉树(华为面试题)将会对二叉树数据结构有初步的理解。树结构在实际开发日常中也经常被用到,关于树结构的进阶可以关注后续推文。
头条面试题-蛇形打印二叉树
老规则,为方便验证,找一下leetcode的原题不难发现对应着leetcode上的第103题——Binary Tree Zigzag Level Order Traversal (蛇形遍历二叉树)。
原题
103. Binary Tree Zigzag Level Order Traversal
Given a binary tree, return the zigzag level order traversal of its nodes' values. (ie, from left to right, then right to left for the next level and alternate between).
For example:
Given binary tree [3,9,20,null,null,15,7],
return its zigzag level order traversal as:
[
[3],
[20,9],
[15,7]
]
103. 二叉树的锯齿形(蛇形)层次遍历
给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
例如:
给定二叉树 [3,9,20,null,null,15,7],
返回锯齿形层次遍历如下:
[
[3],
[20,9],
[15,7]
]
分析&&思路
根据题意,由于题目要求蛇形遍历,不能依次按照顺序将遍历到的点数据存入列表中。而是要满足:1、奇数层的节点数据按照从左到右的顺序存入列表;2、偶数层的节点数据按照从右到左的顺序存入列表。而凡是涉及到二叉树遍历相关,可分两种思想实现:使用深度优先搜索DFS;使用广度优先搜索BFS。
深度优先搜索-DFS
相同的DFS思想又可以分为递归和非递归的思路
DFS—递归
递归遍历二叉树的最简单理解方法:将二叉树当成“三个节点“,遍历完当前(根)节点之后,依次遍历左子节点(左子树)、右子节点(右子树)。而在遍历拿到节点数据时,将节点数据存入对应层的列表中,存储数据根据奇偶层会有不同的顺序。
伪代码
zigzagLevelOrder方法:
1、创建结果嵌套列表,列表中每个元素是一个列表用于存储二叉树每一层的节点数据;
2、声明int型二叉树的层数;
3、调用dfs搜索,传入根节点、存储列表、当前层数;
4、返回结果数据列表;
dfs方法:
1、节点空判断,空节点遍历结束返回;
2、判断数据列表是否有存储当前层的数据,若未存储则先创建当前层的存储列表;
3、根据层数获取当前层数据列表;
4、若当前层为奇数层,则将当前节点数据按照顺序存入当前层列表的最后;
5、若当前层为偶数层,则将当前节点数据存入当前层列表的第一位;
6、递归遍历左子树;
7、递归遍历右子树;
编码实践
public List> zigzagLevelOrder(TreeNode root) {
List> result = new ArrayList>();
int level = 0;
dfs(root, result, level);
return result;
}
public void dfs(TreeNode node, List> list, int level) {
if (node == null) {
return;
}
if (list.size() <= level) {
List newLevel = new ArrayList();
list.add(newLevel);
}
List curList = list.get(level);
if ((level & 1) == 0) {
curList.add(node.val);
} else {
curList.add(0, node.val);
}
dfs(node.left, list, level + 1);
dfs(node.right, list, level + 1);
}
DFS—非递归
非递归本质借助存储数据结构将待遍历子节点暂时存入,而后依次取出循环遍历,本质思路和递归一样。
伪代码
LevelNode类:
用于存储节点信息以及节点当前所在二叉树层数
zigzagLevelOrder方法:
1、创建结果嵌套列表,列表中每个元素是一个列表用于存储二叉树每一层的节点数据;
2、创建用于存储节点信息的栈,初始存储根节点信息;
3、while循环直到栈中节点全部遍历;
3.1、节点出栈,并根据LevelNode获取当前节点层数;
3.2、若当前层为奇数层,则将当前节点数据按照顺序存入当前层列表的最后;
3.3、若当前层为偶数层,则将当前节点数据存入当前层列表的第一位;
3.4、判断当前节点的左子节点是否为空,不为空则入栈;
3.5、判断当前节点的右子节点是否为空,不为空则入栈;
4、返回结果数据列表;
编码实践
static class LevelNode {
int level;
TreeNode node;
public LevelNode(int l, TreeNode tn) {
level = l;
node = tn;
}
}
public List> zigzagLevelOrder(TreeNode root) {
List> result = new ArrayList>();
Stack stack = new Stack();
if (root != null) {
stack.push(new LevelNode(0, root));
}
while (!stack.isEmpty()) {
LevelNode lNode = stack.pop();
TreeNode node = lNode.node;
int level = lNode.level;
if (result.size() <= level) {
List newLevel = new ArrayList();
result.add(newLevel);
}
List curList = result.get(level);
if ((level & 1) == 0) {
curList.add(0, node.val);
} else {
curList.add(node.val);
}
if (node.left != null) {
stack.push(new LevelNode(level + 1, node.left));
}
if (node.right != null) {
stack.push(new LevelNode(level + 1, node.right));
}
}
return result;
}
广度优先搜索-BFS
广度优先搜索只有非递归实现,同深度优先的非递归实现一样,需要借助额外的数据结构对节点进行存储,循环遍历的形式。
BFS—非递归
广度优先搜索一般借助队列数据机构进行存储和遍历。由于搜索“广”,分层次遍历,因此使用队列。
伪代码
zigzagLevelOrder方法:
1、创建结果嵌套列表,列表中每个元素是一个列表用于存储二叉树每一层的节点数据;
2、根节点判空;
3、创建用于存储节点信息的队列,初始存储根节点;
4、声明标记位,标记当前层数是否为奇数层,初始第一层为奇数故赋为true;
5、while循环进行分层遍历,直到队列为空;
5.1、创建当前层的列表;
5.2、根据队列获取当前层的节点个数;
5.3、循环遍历当前层的所有节点
5.3.1、节点出队列;
5.3.2、根据奇偶层标记,将节点数据存入当前层列表,奇数层顺序,偶数层倒序;
5.3.3、判断当前节点的左子节点是否为空,不为空则入队列;
5.3.4、判断当前节点的右子节点是否为空,不为空则入队列;
5.4、将当前层数据列表计入结果列表中;
5.5、更新奇偶层标记;
4、返回结果数据列表;
编码实践
public static List> zigzagLevelOrder(TreeNode node) {
List> result = new ArrayList>();
if (node == null) {
return result;
}
Queue queue = new LinkedList();
queue.add(node);
boolean flag = true;
while (!queue.isEmpty()) {
List list = new ArrayList();
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode cur = queue.poll();
if (flag) {
list.add(cur.val);
} else {
list.add(0, cur.val);
}
if (cur.left != null) {
queue.add(cur.left);
}
if (cur.right != null) {
queue.add(cur.right);
}
}
result.add(list);
flag = !flag;
}
return result;
}
彩蛋
关于递归与非递归,从本题结果上看递归在时间复杂度上更胜一筹,而使用了额外数据结构的非递归循环实现缺根递归实现相差无几。我们似乎很难找到具体规律,而在实际开发中,如果再遇到一个业务算法既可以使用递归又可以使用非递归实现的时候该如何选择呢?这便是本文的彩蛋。