这篇主要介绍二叉树遍历的递归方法和迭代方法。最后进行n叉树的遍历练习。
二叉树前中后序遍历分别对应力扣144、94、145题,n叉树遍历对应力扣589、590题。
目录
递归方法
//前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<Integer>();//用res来存遍历结果
preOrder(res,root);
return res;
}
public void preOrder(List<Integer> res,TreeNode root){
if(root==null)return;//递归终止条件,遍历到空节点就返回上一层
res.add(root.val);//访问结点(这句位置决定递归顺序)
preOrder(res,root.left);
preOrder(res,root.right);
}
}
//中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
inOrder(res,root);
return res;
}
public void inOrder(List<Integer> res,TreeNode root){
if(root==null)return;
inOrder(res,root.left);
res.add(root.val);
inOrder(res,root.right);
}
}
//后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res=new LinkedList<>();
postOrder(res,root);
return res;
}
public void postOrder(List<Integer> res,TreeNode root){
if(root==null)return;
postOrder(res,root.left);
postOrder(res,root.right);
res.add(root.val);
}
}
迭代方法(未统一写法)
因为递归调用会把函数的局部变量、参数值和返回的地址入栈,return的时候从栈顶弹出上次的各项参数,所以递归可以返回上一层位置(实质是用栈记录下了上一层的信息)。所以三种遍历方式可以用栈来模拟递归的过程,也就是所谓的迭代(递归是一环套一环的执行,而迭代是按照一定的逻辑顺序向前推进)。
·前序迭代
前序遍历的顺序是根->左->右,需要先处理根结点(根节点先入栈,在后续每一次处理中,都将栈顶元素出栈,加入res中),然后将右孩子入栈,再将左孩子入栈,这样可以保证出栈的顺序是左孩子在右孩子前;
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res= new LinkedList<>();
Stack<TreeNode> stack=new Stack<>();//栈里面存的是TreeNode类型的结点
if(root!=null)stack.push(root);//如果空树,什么都不管,栈就是空的,最后直接返回空的res
//前序遍历顺序(根左右),用栈模拟要根先入栈再出栈,然后右结点入栈,左节点入栈,这样出栈顺序就符合
while(!stack.isEmpty()){
root=stack.pop();
res.add(root.val);
if(root.right!=null)stack.push(root.right);
if(root.left!=null)stack.push(root.left);
}
return res;
}
}
·中序迭代
做完了前序迭代,肯定是想稍微改改前序迭代代码就套上中序,但这样是不行的。根源就在于对根结点的访问和处理上,前序的顺序是中左右,遍历过程中最先访问(遍历)的是根结点,每次循环中最先处理(出栈加入res)的也都是中间节点,然后是孩子结点。也就是说对结点访问和处理是连在一起的
但是中序遍历的顺序是左中右,要先访问根节点,然后一直向左子树访问,直到底部,开始处理结点(出栈加入res中),也就是说对结点的访问和处理是分开进行的,导致了处理和访问顺序的不一致。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res =new LinkedList<>();
if(root==null)return res;
Stack<TreeNode> stack =new Stack<>();
TreeNode cur=root;//用cur来遍历树
//当栈或当前遍历结点非空的时候
while(!stack.isEmpty()||cur!=null){
if(cur!=null){//当前遍历结点非空,就要入栈,然后往左子树遍历
stack.push(cur);
cur=cur.left;//左
}else{//当前遍历结点为空
cur=stack.peek();//栈顶元素就是要处理的数据,用cur保存
stack.pop();
res.add(cur.val);//中
cur=cur.right;//再往右子树遍历
}
}
return res;
}
}
·后序迭代
前序迭代顺序是中左右,后序是左右中,其实只要在前序基础上,将顺序变为中右左,再将最后的res翻转,就可以得到左右中的顺序;
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res =new LinkedList<>();
if(root==null)return res;
Stack<TreeNode> stack=new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
root=stack.pop();
res.add(root.val);
if(root.left!=null)stack.push(root.left);
if(root.right!=null)stack.push(root.right);
}
Collections.reverse(res);
return res;
}
}
迭代方法(统一模板)
在前一组迭代写法中,中序遍历是因为需要先访问到最左下才可以开始处理结点,而前序是可以一边访问一边处理,这样导致了代码写法的不一致,为了统一这个风格,可以在遍历的过程中,都将结点入栈,只不过在要处理的节点(也就是要加入res的结点)入栈后再入栈一个null结点作为标记,这样在后续出栈中如果碰到null结点,就知道下一个出栈的结点是要加入res的,这样一来只要调整左中右结点的入栈顺序,就可以控制最后出栈的顺序;说得有些绕,直接看三种遍历的对比代码找感觉:
//前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Deque<TreeNode> stack=new LinkedList<>();
if(root!=null)stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.peek();//cur获取栈顶元素
if(cur!=null){//栈顶元素非空,出栈
stack.pop();//避免后续对栈顶的元素重复操作,统一先出栈(前面已经用cur记录了,不怕丢)
if(cur.right!=null)stack.push(cur.right);//若左右子树为空,则空节点不入栈,保证栈顶元素为空节点只发生在是中结点入栈后
if(cur.left!=null)stack.push(cur.left);
//因为是前序遍历(中左右),入栈要按右左中的顺序
stack.push(cur);
stack.push(null);//在中结点入栈后,null入栈作为标记;
}else{//若栈顶元素为空,说明下一个栈顶元素是“中”结点
stack.pop();//null出栈
cur=stack.peek();//null的下一个栈顶元素就是“中结点”,用cur暂存
stack.pop();//存下后就可出栈
res.add(cur.val);
}
}
return res;
}
}
//中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res= new ArrayList<>();
Deque<TreeNode> stack=new LinkedList<>();
if(root!=null)stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.peek();
if(cur!=null){
stack.pop();
if(cur.right!=null)stack.push(cur.right);
stack.push(cur);
stack.push(null);
if(cur.left!=null)stack.push(cur.left);
}
else{
stack.pop();
cur=stack.peek();
stack.pop();
res.add(cur.val);
}
}
return res;
}
}
//后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Deque<TreeNode> stack= new LinkedList<>();
if(root!=null)stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.peek();
if(cur!=null){
stack.pop();
stack.push(cur);
stack.push(null);
if(cur.right!=null)stack.push(cur.right);
if(cur.left!=null)stack.push(cur.left);
}else{
stack.pop();
cur=stack.peek();
stack.pop();
res.add(cur.val);
}
}
return res;
}
可以观察到这样设置的话,代码的区别就只在于
//1
stack.push(cur);
stack.push(null);
以及
//2
if(cur.right!=null)stack.push(cur.right);
//3
if(cur.left!=null)stack.push(cur.left);
这三句的顺序的不同。
二叉树遍历
LeetCode 144. 二叉树的前序遍历
2023.06.11 二刷
递归方法代码如下:
//递归版本
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
preOrder(res,root);
return res;
}
public void preOrder(List<Integer> res,TreeNode root){
if(root==null)return;
res.add(root.val);
preOrder(res,root.left);
preOrder(res,root.right);
}
}
迭代方法(非统一模板),代码如下:
//迭代版本
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res= new ArrayList<>();
Deque<TreeNode> stack=new LinkedList<>();
if(root!=null)stack.push(root);
//前序遍历顺序(根左右),用栈模拟要根先入栈再出栈,然后右结点入栈,左节点入栈,这样出栈顺序就符合
while(!stack.isEmpty()){
root=stack.pop();
res.add(root.val);
if(root.right!=null)stack.push(root.right);
if(root.left!=null)stack.push(root.left);
}
return res;
}
}
迭代法,统一模板:
思想:
迭代法需要使用栈,但用栈的话就无法解决遍历结点和处理结点的一致问题。
那么将访问到的结点放入栈中,处理过的结点也放进去,但是要对它进行标记,即处理的结点放入栈中之后,紧接着放入一个空指针入栈作为标记。后面栈顶元素碰到null,说明null的下一个栈顶元素就是访问过但还没处理的元素,后面就可以对null进行出栈,保存下一个栈顶元素,然后栈顶元素出栈,将保存的元素值加入res
代码如下:
//迭代统一版
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
if(root==null)return res;
Deque<TreeNode> stack=new LinkedList<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.pop();//栈顶出栈,并让cur存储出栈元素
if(cur!=null){//出栈栈顶元素的非
// 判断出栈元素左右子树是否为空,不为空就入栈
if(cur.right!=null)stack.push(cur.right);
if(cur.left!=null)stack.push(cur.left);
//因为是前序遍历(中左右),入栈要按右左中的顺序
stack.push(cur);
stack.push(null);//在中结点入栈后,null入栈作为标记;
}else{//若栈顶元素为空,说明下一个栈顶元素是“中”结点
cur=stack.pop();;//null的下一个栈顶元素就是“中结点”,用cur暂存
res.add(cur.val);
}
}
return res;
}
}
LeetCode 145. 二叉树的后序遍历
2023.06.11 二刷
递归版本代码如下:
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res=new LinkedList<>();
postOrder(res,root);
return res;
}
public void postOrder(List<Integer> res,TreeNode root){
if(root==null)return;
postOrder(res,root.left);
postOrder(res,root.right);
res.add(root.val);
}
}
迭代版本,修改自前序遍历
前序遍历:中左右,调整代码入栈顺序->中右左,反转res->左右中(后续遍历)
代码如下:
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res =new Arraylist<>();
if(root==null)return res;
Deque<TreeNode> stack=new LinkedList<>();
stack.push(root);
while(!stack.isEmpty()){
root=stack.pop();
res.add(root.val);
if(root.left!=null)stack.push(root.left);
if(root.right!=null)stack.push(root.right);
}
Collections.reverse(res);
return res;
}
}
迭代法,统一模板,代码如下:
//统一版本
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
if(root==null)return res;
Deque<TreeNode> stack= new LinkedList<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.pop();
if(cur!=null){
stack.push(cur);
stack.push(null);
if(cur.right!=null)stack.push(cur.right);
if(cur.left!=null)stack.push(cur.left);
}else{
cur=stack.pop();
res.add(cur.val);
}
}
return res;
}
}
LeetCode 94. 二叉树的中序遍历
2023.06.11 二刷
递归版本,代码如下:
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
inOrder(res,root);
return res;
}
public void inOrder(List<Integer> res,TreeNode root){
if(root==null)return;
inOrder(res,root.left);
res.add(root.val);
inOrder(res,root.right);
}
}
迭代法,非统一模板
中序遍历与前序和后序遍历不太一样
前序遍历的时候,遍历结点和处理结点是一起前后进行的,而中序遍历无法这样
代码如下:
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res =new LinkedList<>();
if(root==null)return res;
Stack<TreeNode> stack =new Stack<>();
TreeNode cur=root;//用cur来遍历树
//当栈或当前遍历结点非空的时候
while(!stack.isEmpty()||cur!=null){
if(cur!=null){//当前遍历结点非空,就要入栈,然后往左子树遍历
stack.push(cur);
cur=cur.left;//左
}else{//当前遍历结点为空
cur=stack.peek();//栈顶元素就是要处理的数据,用cur保存
stack.pop();
res.add(cur.val);//中
cur=cur.right;//再往右子树遍历
}
}
return res;
}
}
迭代法,统一模板,代码如下:
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res= new ArrayList<>();
if(root==null)return res;
Deque<TreeNode> stack=new LinkedList<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.pop();
if(cur!=null){
if(cur.right!=null)stack.push(cur.right);
stack.push(cur);
stack.push(null);
if(cur.left!=null)stack.push(cur.left);
}else{
cur=stack.pop();
res.add(cur.val);
}
}
return res;
}
}
n叉树的遍历
LeetCode 589. N 叉树的前序遍历
//递归法
class Solution {
List<Integer> res=new ArrayList<>();
public List<Integer> preorder(Node root) {
if(root==null)return res;
res.add(root.val);
for(Node child:root.children)preorder(child);
return res;
}
}
/*
前序遍历顺序是根-左-右,每层循环里面都是根结点先出栈,加入res,可以保证结果集中根会在孩子结点前;
还需要解决左右的顺序,栈先进后出的特性,要先让右孩子入栈,这样后面出栈可让左孩子先出栈加入res,保证结果集中的左-右顺序;结合起来就是根-左-右的顺序;
*/
//借助栈实现的迭代法
class Solution {
public List<Integer> preorder(Node root) {
List<Integer> res=new ArrayList<>();
if(root==null)return res;
Stack<Node>stack=new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
root=stack.pop();
res.add(root.val);
//因为栈先进后出,所以需要先让右边的孩子结点入栈
for(int i=root.children.size()-1;i>=0;--i)stack.push(root.children.get(i));
}
return res;
}
}
LeetCode 590. N 叉树的后序遍历
//递归法
class Solution {
List<Integer> res=new ArrayList<>();
public List<Integer> postorder(Node root) {
if(root==null)return res;
for(Node child:root.children)postorder(child);
res.add(root.val);
return res;
}
}
/*
与前序遍历迭代改后序遍历迭代类似,前序是根-左-右,后序是左-右-根;
只要在入栈时,让左孩子先于右孩子入栈,这样出栈就是右孩子先出,保证右-左顺序加入res;
而根总是先于孩子结点入栈,在孩子结点入栈的那层循环里,根结点值已经加入res;
这样可以保证结果集里是根-右-左,只要最后翻转res就可以变成左-右-根;
*/
//后序迭代法
class Solution {
public List<Integer> postorder(Node root) {
List<Integer> res=new ArrayList<>();
if(root==null)return res;
Stack<Node>stack=new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
root=stack.pop();
res.add(root.val);
for(int i=0;i<root.children.size();++i)
stack.push(root.children.get(i));
}
Collections.reverse(res);
return res;
}
}