我们都知道二叉树可以按照头结点的输出顺序分为:前序遍历、中序遍历和后序遍历。今天我们就来一起探讨一下二叉树这三种遍历方法分辨用算法如何实现?
Leetcode144(前序遍历)、94(中序遍历)、145(后序遍历)。
递归方法遍历
递归遍历二叉树是最为常见的方法,但是说到递归很多人就会头疼,理解递归原理但就是写不出来。我们就先拿中序遍历举例吧!对于中序遍历意思就是父结点在第二个输出,画个图举例吧:
其特点是左右子节点一定在父节点的两侧。明白了遍历规则下面就是如何使用递归,所谓的递归就是自我调用函数自己调用自己,书写递归时一定要搞清楚三点:
- 明确子过程
子过程是我自己方便讲述想到的名词,递归就是要将一个过程拆分成多个相同的子过程,对于本体二叉树遍历子过程就是中序遍历整个二叉树,而我们知道每个二叉树都是由多个节点组成的,我们将一个节点和他左右节点这三个节点看做一小部分,我们只需要处理好这一小部分的中序遍历,多个一小部分最终组合成整个树的中序遍历,所以对于这个问题来讲子过程就是处理每个节点和其左右节点的中序遍历(这个是不是比较简单?)
- 如何递进
递进是递归运行起来的关键,如何递进要具体情况具体分析,本题中我们要不断的去处理每个子过程,那上图举例,父节点1的左右子节点分别是2和3,而2和3同时也可以作为父节点,只有当以2这个父节点的子过程拿到结果以后,1这个子过程才能得到结果,所以这里的递进条件就是以左右子节点为参数再次调用函数。
- 确定结束条件
其实递归就是“不撞南墙不回头”,而结束条件就是“南墙”。递归一定要有终止条件,并且终止条件一定要写在递归调用之前!!对与遍历二叉树而言,终止条件就是当节点等于null时结束递归。
具体代码实现:
public List<Integer> inorderTraversal(TreeNode root) {
List list = new ArrayList<Integer>();
pre(root,list);
return list;
}
//递归函数
private void pre(TreeNode root ,List<Integer> list){
if(root==null){
return;
}
pre(root.left,list);
list.add(root.val);
pre(root.right,list);
}
实现递归的函数实际上就三行,对于pre(root.left,list);
他必须一直想左节点递归,知道左节点为null时才能得到返回值,此时这个节点一定是二叉树数中最左边的那个节点,此时在保存节点。
而前序遍历和后序遍历与之大体相同,区别在与什么时候保存节点,前序遍历在一开始保存顺序是:list.add(root.val);pre(root.left,list);pre(root.right,list);
后序节点要求父节点在最后保存:pre(root.left,list);pre(root.right,list);list.add(root.val);
迭代方法遍历
同样以中序遍历举例,主要思路是在遍历过程中维护一个栈时刻保存遍历的父节点,不断向左遍历到最深处,当左节点为空时不断出栈,并保存出栈元素。
代码如下:
public List<Integer> inorderTraversal(TreeNode root) {
//迭代方法实现
List list = new ArrayList<Integer>();
if(root == null){
return list;
}
TreeNode node = root;
Stack<TreeNode> stack = new Stack<TreeNode>();
while(!stack.isEmpty()||node!=null){
while(node!=null){
stack.push(node);
node=node.left;
}
node = stack.pop();
list.add(node.val);
node = node.right;
}
return list;
}
整个执行过程和递归的过程实际上是一样的,而迭代的代码更好理解。对于前序遍历的迭代方法也是同理,要在入栈时保存即可:
public List<Integer> preorderTraversal(TreeNode root) {
//迭代方法实现
List list = new ArrayList<Integer>();
if(root == null){
return list;
}
TreeNode node = root;
Stack<TreeNode> stack = new Stack<TreeNode>();
while(!stack.isEmpty()||node!=null){
while(node!=null){
list.add(node.val);
stack.push(node);
node=node.left;
}
node = stack.pop();
node = node.right;
}
return list;
}
而后序遍历的方法要大有不同,我们先来看一下代码:
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if(root == null) {
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()) {
TreeNode node = stack.pop();
if(node.left != null) {
stack.push(node.left);
}
if(node.right != null) {
stack.push(node.right);
}
result.add(0, node.val);
}
return result;
}
信息的同学会发现,这串代码和行序遍历算法很像,去别在于行序遍历算法是用队列存储节点,而这个是用栈来存储的。
拓展方法:Morris遍历实现
下面是用Morris遍历实现的后序遍历,该方法特点在于使用栈的遍历方法,可以在O(1)的空间复杂度下实现中、前、后序遍历。
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
TreeNode dummy = new TreeNode(0);
dummy.left = root;
TreeNode curr = dummy;
while (curr != null) {
if (curr.left == null) {
curr = curr.right;
} else {
TreeNode pre = curr.left;
while (pre.right != null && pre.right != curr) {
pre = pre.right;
}
if (pre.right == null) {
pre.right = curr;
curr = curr.left;
} else {
pre.right = null;
reverseAddNodes(curr.left, result);
curr = curr.right;
}
}
}
return result;
}
private void reverseAddNodes(TreeNode node, List<Integer> result) {
int startIdx = result.size();
while (node != null) {
result.add(node.val);
node = node.right;
}
int endIdx = result.size() - 1;
while (startIdx < endIdx) {
Collections.swap(result, startIdx, endIdx);
startIdx++;
endIdx--;
}
}