本文已参与「新人创作礼」活动,一起开启掘金创作之路。
节点类
public static class Node {
public int value;
public Node left;
public Node right;
public Node(int v) {
value = v;
}
}
先序(递归)
先序遍历的顺序就是头 -> 左 -> 右
代码
public static void Preamble(Node head) {
if (head == null) {
return;
}
System.out.println(head.value);
pre(head.left);
pre(head.right);
}
中序(递归)
中序遍历的顺序就是左 -> 头 -> 右
代码
public static void MediumOrder(Node head) {
if (head == null) {
return;
}
in(head.left);
System.out.println(head.value);
in(head.right);
}
后序(递归)
中序遍历的顺序就是左 -> 右 -> 头
代码
public static void PostOrder(Node head) {
if (head == null) {
return;
}
pos(head.left);
pos(head.right);
System.out.println(head.value);
}
总结递归版本
上面我们直接给大家看代码的原因就是希望大家自己可以找到一个规律,我们发现递归版本其实都是一样的知识打印的地方不一样而已,而且打印的地方也是很有规律的,并不是无迹可寻的。
我们抽象出来就是这样的
抽象代码
public static void ending(Node head) {
if (head == null) {
return;
}
// 先序
f(head.left);
// 中序
f(head.right);
// 后序
}
这个就是我们抽象出来的代码我们只要在注释的地方写下打印语句就是其中的一个递归顺序。
我们接下来才是要命的地方。
我们都知道,生产环境下是比较讨厌递归的。因为创建一个函数栈和销毁一个函数栈是很浪费效率的,远远不如我们创建一个栈,走迭代来的好。
那我们接下来就写一写非递归版本
先序(非递归)
我们先看看先序的递归是怎么样运作的,我们好能知道它为什么递归。
我们在递归的时候,我们进入函数的时候,先直接打印
第二步,我们就直接到了左节点
第三步,打印了左节点,之后又到了左节点
……
我们发现,左节点打印完之后才会打印右节点
那我们能不能自己创建一个栈,然后拿到一个节点之后,先放右节点,然后放左节点。
之后我们循环弹出栈里面的节点,是不是都是先处理好左节点之后才能处理右节点
那我们知道这个大概之后就开始写代码了。
代码
我们的第一步就是创建一个栈
Stack<Node> stack = new Stack<>();
之后我们要把我们的头结点放过去
stack.push(head);
这个时候我们就要到循环里面去了。我们要从栈里面弹出节点,然后打印,之后就先放右节点再放左节点,这样就可以让左节点先处理,右节点后处理。循环的判断条件就是当栈不为空的时候。
while(!stack.isEmpty()){
Node cur = stack.pop();
System.out.print(cur.value + " ");
if(cur.right != null){
stack.push(cur.right);
}
if(cur.left != null){
stack.push(cur.left);
}
}
之后先序的非递归版本就结束了
中序(非递归)
我们知道,递归的实现是有规律的,那我们非递归是不是也有规律呢?
很遗憾,这种规律现在我还没有找到。
那我们还是先看中序的递归版本是怎么样运作的,我们让我们的栈来替代函数栈。
我们看中序的递归,先找左节点,等左节点为空的时候会打印并且会到右节点。那我们看到这个,我们就有点思路了。
我们创建一个栈,然后我们让节点一直往左,等为空的时候,pop一下,打印一下,然后让节点往右是不是就可以了。
代码
我们先创建一个栈
Stack<Node> stack = new Stack<>();
之后就是循环了。那为什么我们不像先序一样先把头结点放进去呢?
其实我们是可以先放进头结点的,不过我们这个时候也要把头结点变成他的坐节点。因为我们在后续压栈的时候,如果这边不改,我们会压两次头。
Stack<Node> stack = new Stack<>();
stack.push(head);
head = head.left;
这样的操作也是可以的。不过就有点多此一举了,因为我们后面会把头结点压进去的。
然后就是循环。
循环的判断条件就是当栈不为空,或者头结点不为空的时候
为什么会这样呢?
我们先看代码
while(!stack.isEmpty() || head != null){
if(head != null){
stack.push(head);
head = head.left;
}else{
head = stack.pop();
System.out.print(head.value + " ");
head = head.right;
}
}
我们看到代码之后我们会发现当我们打印头结点的右子树的时候,我们发现我们之前没有压过右子树的值。这个时候我们就要用到head != null的时候。
if(head != null){
stack.push(head);
head = head.left;
}
这个代码的判断就是将左边的都放到栈里面。
else{
head = stack.pop();
System.out.print(head.value + " ");
head = head.right;
}
这个就是知道左边没有东西了,那我们就可以打印了,并且可以放入右边的子树。
后序(非递归)
后序就是先打印子节点,最后打印父节点,那我们只要能判断自己的子节点有没有打印过就可以解决问题了。
那我们怎么解决呢?
我们先让节点一直往左边走,等到了最左的叶子节点的时候,我们就打印,打印好之后我们让原本指向头结点的指针指向叶子节点。
之后我们判断从栈中弹出来的节点判断指向叶子节点的指针是不是自己的儿子,如果是,就说明打印过了。
代码
我们还是先创建栈,并把头元素放进去
Stack<Node> stack = new Stack<>();
stack.push(head);
第二步我们就要循环了。
while(!stack.isEmpty()){
Node cur = stack.peek();
if(cur.left != null && head != cur.left && head != cur.right){
stack.push(cur.left);
}else if(cur.right != null && head != cur.right){
stack.push(cur.right);
}else{
System.out.print(stack.pop().value + " ");
head = cur;
}
}
我们先看看第一个注意点,为什么这边用peek不用pop,因为我们这边用到的元素之后还是会用到的,pop出来的话,之后还要在push回去就有点麻烦。
第二个注意点,第一个if的三个判断点。cur.left的判断是为了看看有没有左节点,如果有的话,就有可能要先压进去,因为我们最先找的就是左节点,后面两个的判断就是看看自己的儿子有没有打印过,打印过的话就不用压了,到后面直接打印自己。
第三个注意点就是第二个if,这个if,就是看看自己有没有右儿子,右儿子有没有被打印过。
至此,三种树遍历的递归和非递归都讲完了。