👏作者简介:大家好,我是笙一,java大二练习生,喜欢算法和java相关知识。
📕正进行的系列:算法
第一次写博客,有所欠缺感谢指正。
目录
前言
最近写算法题的过程中,忽然对基础的深度优先遍历记忆模糊。想到可能对于新手来说,对基础的二叉树深度优先遍历非递归版本实现理解不是很深刻,所以写下这篇博客。
深度优先遍历
深度优先遍历是一个非常经典的算法,新手经常在二叉树相关题目中遇到它。而最基本的就其三种遍历方式:先序遍历,中序遍历,后序遍历。对于一个二叉树,包含其根节点,左子树,右子树。而这三种遍历方式的区别就是遍历顺序的不同。一般规定:先序遍历顺序为根、左、右,中序遍历顺序为左、根、右,后序遍历顺序为左、右、根。
递归方式实现
递归方式实现非常简单,我们这里不做叙述,直接看代码:
先序遍历(递归版)
class Node {
public int value;
public Node left;
public Node right;
public Node(int val) {
this.value = val;
}
}
public void preTraversal(Node head) {
if(head == null) {
return;
}
System.out.print(head.value + " ");
preTraversal(head.left);
preTraversal(head.right);
}
中序遍历(递归版)
public void midTraversal(Node head) {
if(head == null) {
return;
}
midTraversal(head.left);
System.out.print(head.value + " ");
midTraversal(head.right);
}
后续遍历(递归版)
public void posTraversal(Node head) {
if(head == null) {
return;
}
posTraversal(head.left);
posTraversal(head.right);
System.out.print(head.value + " ");
}
非递归方式(迭代)实现
众所周知,所有的递归代码都是利用栈来存信息,所以都可以改成非递归代码,无非采用自己压栈的方式。对于二叉树的遍历当然也不例外。
先序遍历(非递归版)
采取以下步骤实现:
1. 创建一个新的栈 stack,将头节点head压栈。
2. 从stack中弹出栈顶节点 cur,弹出就打印其值,然后如果 cur 有右孩子节点先压入右孩子,如果 cur 有左孩子节点再压入左孩子。
3. 重复过程2,直到栈 stack 内为空。
分析一下这个过程:我们已知栈可以将信息逆序,而先序遍历的顺序是根,左, 右,意思就是,对于每一个子树,我们要先遍历根节点,然后遍历其左子树和右子树。所以我们在弹出节点的同时,在栈内是先压右,后压左,这样在每次弹出节点时的顺序才会正确。
代码实现:
public void preTraversalWithUnRecur(Node head) {
if (head == null) {
return;
}
Stack<Node> stack = new Stack<>();
stack.push(head);
while (!stack.isEmpty()) {
head = stack.pop();
System.out.print(head.value + " ");
if (head.right != null) {
stack.push(head.right);
}
if (head.left != null) {
stack.push(head.left);
}
}
}
中序遍历(非递归版)
中序遍历的实现是我认为三种遍历方式中较为有趣的一种,我们先讲它的实现步骤,再来聊聊为什么要这么做。
采取以下步骤实现:
1. 创建一个新的栈 stack,将头节点 head 压栈。
2. 在 stack 中压入从头节点开始,所有的左孩子节点(将整棵树的左边界压入栈中)。并且每次压入时使此时 head = head.left 。
3. 当 head 为空时,stack 弹出一个节点node,弹出就打印,并且让此时 head = node.right, 继续重复步骤2 。
4. 直到 stack 为空, 所有过程完成。
理解一下:
中序遍历需要的是按照左、中、右顺序遍历,所以需要先拿到全局最左的节点。而每次弹出最左的节点时,下一个弹出的节点是最左节点的父节点。这样就说明为什么需要将一整条左边界都压入栈中。而当弹出左节点的父节点后,如果父节点有右子树,又需要按照中序遍历去遍历整个右子树,这就是为什么步骤3我们需要获得到右节点时,重复了步骤2 。这样我们从栈中弹出的所有节点顺序,就是我们要的结果。实在理解不了也没关系,我们可以根据code来理解一下。
代码实现:
public void midTraversalWithUnRecur(Node head) {
if (head == null) {
return;
}
Stack<Node> stack = new Stack<>();
while (!stack.isEmpty() && head != null) {
// head 如果不等于null, 那就说明左边界没压完
// 如果等于 null,可以弹出栈顶节点,就是此时的最左节点
// 而else代码块内就是步骤3 的实现,如果找到右节点(不为空),此时head不等于空,下一个循环就继续实现步骤2
// 如果没右节点,那么继续else,弹出下一个节点,看看它有没有右子树
if(head != null) {
stack.push(head);
head = head.left;
} else {
head = stack.pop();
System.out.print(head.value + " ");
head = head.right;
}
}
}
后序遍历(非递归版)
与先序遍历方式类似。我们可以思考,后序遍历的顺序是左、右、根,反过来看不就是根、右、左吗?与先序遍历的根、左、右非常类似。而要实现先序遍历,我们采取的是先压右,再压左,那么我们在实现后序遍历时只要改变压入策略就行。这样我们得到的就是想要的结果的逆序。接下来就简单了,只要在遍历的过程中将结果压入新的一个栈,最后从栈里一个个拿出来就行了。
采取以下步骤实现:
1. 创建一个新的栈 stack1,用来进行遍历,创建另一个新的栈 stack2,用来存放遍历过程中得到的节点。将头节点head压入 stack1。
2. 从stack1中弹出栈顶节点 cur,弹出就将其压入 stack2,然后如果 cur 有左孩子节点先压入左孩子,如果 cur 有右孩子节点再压入右孩子。
3. 重复步骤2,直到栈 stack1 内为空。
4. 从 stack2 中不断弹出节点,打印其值。
代码实现:
public void posTraversalWithUnRecur(Node head) {
if (head == null) {
return;
}
Stack<Node> stack1 = new Stack<>();
Stack<Node> stack2 = new Stack<>();
stack1.push(head);
while (!stack1.isEmpty()) {
head = stack1.pop();
stack2.push(head);
if (head.left != null) {
stack1.push(head.left);
}
if (head.right != null) {
stack1.push(head.right);
}
}
while (!stack2.isEmpty()) {
System.out.print(stack2.pop().value + " ");
}
}
总结
非递归实现二叉树的三种遍历方式还是挺重要的,在面试上也是常考题。一定要做到熟练掌握,“张口就来”。我是笙一,一个努力拼搏的人,一起进步,加油!