前言
二叉树遍历常用的一般分为前序、中序、后序三种,下面使用递归和迭代两种方法来实现这三种遍历,这三种遍历的顺序分别为:
- 前序:中左右
- 中序:左中右
- 后序:左右中
记得时候就是左右不变,中跟着遍历的方式走
递归
每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!
-
确定递归函数的参数和返回值:
确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。 -
确定终止条件:
写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。 -
确定单层递归的逻辑:
确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
所以以前序遍历为例看上面三要素:
-
确定递归函数的参数和返回值:
因为要打印出前序遍历节点的数值,所以需要传入list存放返回结果
void traversal(TreeNode root, List<Integer> ret)
-
确定终止条件:
如果当前遍历的这个节点是空,就直接return
if (root == null) { return; }
-
确定单层递归的逻辑:
前序遍历是中左右,所以在单层递归的时候,需要先取中间节点的值
ret.add(root.val); // 中 traversal(root.left, ret); // 左 traversal(root.right, ret); // 右
所以根据上面的分析,三种遍历的递归方式就不难写了,代码如下:
/**
* 遍历
*/
public List<Integer> traversal(TreeNode root) {
List<Integer> ret = new ArrayList<>();
// preOrderTraversal(root, ret);
// postOrderTraversal(root, ret);
return ret;
}
/**
* 前序
*/
private void preOrderTraversal(TreeNode root, List<Integer> ret) {
if (root == null) {
return;
}
ret.add(root.val);
traversal(root.left, ret);
traversal(root.right, ret);
}
/**
* 中序
*/
private void preOrderTraversal(TreeNode root, List<Integer> ret) {
if (root == null) {
return;
}
traversal(root.left, ret);
ret.add(root.val);
traversal(root.right, ret);
}
/**
* 后序
*/
private void postOrderTraversal(TreeNode root, List<Integer> ret) {
if (root == null) {
return;
}
traversal(root.left, ret);
traversal(root.right, ret);
ret.add(root.val);
}
迭代
前序遍历
前序遍历是中左右,每次先处理的是中间节点,那么先将跟节点放入栈中,然后将右孩子加入栈,再加入左孩子。
为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。代码如下:
// 前序迭代 按照 中左右 的顺序依次访问节点,并将数据处理入数组中
private List<Integer> preOrderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<TreeNode>();
List<Integer> ret = new ArrayList<Integer>();
if (root == null) {
return ret;
}
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
ret.add(node.val); // 中
if (node.right != null) {
stack.push(node.right); // 右 // 要先加入右孩子,出栈的时候才能先出做孩子
}
if (node.left != null) {
stack.push(node.left); // 左
}
}
return ret;
}
后序遍历
先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,代码如下:
/**
* 利用先序遍历的方式来简化
* 后序:左右中 <= 反转 <= 中右左 <= 换左和右的顺序 <= 中左右 : 前序
*/
private List<Integer> postOrderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<TreeNode>();
List<Integer> ret = new ArrayList<Integer>();
if (root == null) {
return ret;
}
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
ret.add(node.val); // 中
if (node.left != null) {
stack.push(node.left); // 左
}
if (node.right != null) {
stack.push(node.right); // 右
}
}
Collections.reverse(ret); // 将结果反转之后就是左右中的顺序了
return ret;
}
中序遍历
在前序和后序遍历的迭代中,有两个比较关键的操作:
- 处理:将元素放进result数组中
- 访问:遍历节点
分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
/**
* 用指针的遍历来帮助访问节点,栈则用来处理节点上的元素
* 中序:左中右,所以坚持的原则就是左子树存在就先处理左子树
*/
private List<Integer> inOrderTraversal(TreeNode root) {
List<Integer> ret = new ArrayList<Integer>();
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode cur = root; // 指针,标识访问节点
while (cur != null || !stack.isEmpty()) {
if (cur != null) { // 指针来访问节点,访问到最底层
stack.push(cur); // 将访问的节点放进栈
cur = cur.left; // 左
} else {
cur = stack.pop(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
ret.add(cur.val); // 中
cur = cur.right; // 右
}
}
return ret;
}
附言
上文解释了迭代实现的时候中序比较特殊的地方,原文中给出了三种迭代方式如何实现归一化的实现方法,主要的思想就是解决上文说的访问节点和处理节点不一致的情况,思路是将访问的节点放入栈中,把要处理的节点也放入栈中但是要做个标记,当遇到这个标记的时候表示下一个节点时一个处理节点,可以放进结果集,不过个人觉得没上文直接写的直观,有兴趣的可以直接到原文中查看
转自这里,点原文查看