二叉树的遍历及解题思路

本篇文章参考于左程云算法课程,部分详细讲解于左程云算法课程视频

二叉树的遍历

​ 在解决二叉树问题时,很多时候需要对二叉树进行遍历,根据遍历的结果进而解决问题。因此,二叉树的遍历对解决二叉树相关问题显得尤为重要。二叉树的遍历方法大致分为三种:深度优先遍历、广度优先遍历、Morris遍历。

一、深度优先遍历

深度优先遍历分为:先序遍历、中序遍历、后序遍历,二对于这三种遍历方式又分别有递归形式与非递归形式的实现。

1. 先序遍历

先序遍历的流程为:

  • 访问本节点
  • 访问左孩子
  • 访问右孩子
(1) 递归形式
public void preOrderRecur(Node head){
    if(head==null) return;
    System.out.print(head.value);
    preOrderRecur(head.left);
    preOrderRecur(head.right);
}
(2) 非递归形式
public void preOrderNotRecur(Node head){
    if(head!=null){
        Stack<Node> stack=new Stack<Node>();
        stack.add(head);
        while(!stack.isEmpty()){
            head=stack.pop();
            System.out.print(head.value);
            if(head.right!=null) stack.push(head.riight);
            if(head.left!=null) stack.push(head.left);
        }
    }
}

非递归形式先序遍历的主要思想为:

  • 将本节点(头节点)入栈
    • 栈顶元素出栈,记为curNode
    • 将curNode右孩子入栈
    • 将curNode左孩子入栈
    • 循环入栈、出栈过程,直至栈空

2. 中序遍历

先序遍历的流程为:

  • 访问左孩子
  • 访问本节点
  • 访问右孩子
(1) 递归形式
public void inOrderRecur(Node head){
    if(head==null) return;
    preOrderRecur(head.left);
    System.out.print(head.value);
    preOrderRecur(head.right);
}
(2) 非递归形式
public void inOrderNotRecur(Node root){
    Stack<Node> stack=new Stack<>();
    while (root!=null||!stack.isEmpty()){
        if (root!=null){
            stack.push(root);
            root=root.left;
        }else {
            root=stack.pop();
            System.out.println(root.val);
            root=root.right;
        }

    }
}

非递归形式中序遍历的主要思想为:

  • 若当前节点curNode非空,则入栈

  • 不断将curNode左孩子,及其左子孙节点入栈,直至左子孙节点为空

  • 当左子孙节点为空,栈顶元素出栈即为curNode

  • 寻找curNode右子树

  • 循环流程,直至栈空


3. 后序遍历

先序遍历的流程为:

  • 访问左孩子
  • 访问右孩子
  • 访问本节点
(1) 递归形式
public void posOrderRecur(Node head)
    if(head==null) return;
    preOrderRecur(head.left);
    preOrderRecur(head.right);
    System.out.print(head.value);
}
(2) 非递归形式
public void posOrderNotRecur(Node root){
    if (root!=null){
        Stack<Node> stack=new Stack<>();
        stack.push(root);
        Node curNode=null;
        while (!stack.isEmpty()){
            curNode=stack.peek();
            if (curNode.left!=null&&root!=curNode.left&&root!=curNode.right) stack.push(curNode.left);
            else if (curNode.right!=null&&root!=curNode.right) stack.push(curNode.right);
            else {
                System.out.println(stack.pop().val);
                root=curNode;
            }
        }
    }
}

非递归形式后序遍历的主要思想为:

  • 根节点入栈
    • 若栈顶元素curNode左孩子不为空且最新出栈元素不是curNode的左右子元素
      • curNode左孩子入栈
    • 若栈顶元素curNode右孩子不为空且最新出栈元素不是粗人Node的右孩子
      • curNode右孩子入栈
    • 否则,栈顶元素出栈
    • 循环入栈、出栈直至栈空

二、广度优先遍历

广度优先遍历即是按二叉树的层级遍历每个层级的元素

public void levelOrder(Node root){
    if (root!=null){
        Queue<Node> queue=new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()){
            root=queue.poll();
            System.out.println(root.val);
            if (root.left!=null) queue.add(root.left);
            if (root.right!=null) queue.add(root.right);
        }
    }
}

层序遍历的主要思想为:

  • 根节点入队
    • 队首元素出队记为curNode
    • curNode左孩子不为空,将curNode左孩子入队
    • curNode右孩子不为空,将curNode右孩子入队
    • 循环入队、出队,直至队空

三、Morris遍历

Morris遍历相较与广度优先遍历和深度优先遍历,显著的降低了空间复杂度和时间复杂度。因此,当优化某个需将二叉树遍历遍历的算法时,可选择Morris遍历。由于Morris遍历时改变了某些节点的指向,因此该遍历方法不宜用于不能改变二叉树内容的问题。

public void morris(Node head){
    if(head==null) return;
    Node cur=head;
    Node mostRight=null;
    while(cur!=null){
        mostRight=cur.left;//mostRight为cur的左孩子
        if(mostRight!=null){//cur存在左子树
            while(mostRight.right!=null&&mostRight.right!=cur){//获取左子树最右节点
                mostRight=mostRight.right;
            }
            if(mostRight.right==null){//第一次到cur
                mostRight.right=cur;
                cur=cur.left;
                continue;
            }else{//mostRight.right==cur即是第二次到cur
                mostRight.right=null;
            }
        }
        cur=cur.right;
    }
}

Morris遍历的本质实际为实现了线索二叉树。

Morris遍历的主要思想为:(假定cur为头节点)

  • 如果cur没有左孩子,cur向右移动(cur=cur.right)
  • 如果cur有左孩子,找到其左子树上最右的节点mostRight;
    • 如果mostRight的右指针指向null,则让该节点右指针指向cur,然后cur向左移动(cur=cur.left)
    • 如果mostRight的右指针指向cur,则让该节点右指针指向null,然后cur向右移动(cur=cur.right)
  • cur为空时遍历停止

1. Morris先序遍历

public void morrisPreOrder(Node head){
    if(head==null) return;
    Node cur=head;
    Node mostRight=null;
    while(cur!=null){
        mostRight=cur.left;//mostRight为cur的左孩子
        if(mostRight!=null){//cur存在左子树
            while(mostRight.right!=null&&mostRight.right!=cur){//获取左子树最右节点
                mostRight=mostRight.right;
            }
            if(mostRight.right==null){//第一次到cur
                System.out.print(cur.value);
                mostRight.right=cur;
                cur=cur.left;
                continue;
            }else{//mostRight.right==cur即是第二次到cur
                mostRight.right=null;
            }
        }else{//cur不存在左子树
            System.out.print(cur.value);
        }
        cur=cur.right;
    }
}

Morris先序遍历的思想为:在Morris遍历的基础上,无论能够到达当前节点一次或两次,均在首次到达时打印。


2. Morris中序遍历

public void morrisInOrder(Node head){
    if(head==null) return;
    Node cur=head;
    Node mostRight=null;
    while(cur!=null){
        mostRight=cur.left;//mostRight为cur的左孩子
        if(mostRight!=null){//cur存在左子树
            while(mostRight.right!=null&&mostRight.right!=cur){//获取左子树最右节点
                mostRight=mostRight.right;
            }
            if(mostRight.right==null){//第一次到cur
                mostRight.right=cur;
                cur=cur.left;
                continue;
            }else{//mostRight.right==cur即是第二次到cur
                mostRight.right=null;
            }
        }
        System.out.print(cur.value);
        cur=cur.right;
    }
}

Morris先序遍历的思想为:在Morris遍历的基础上

  • 当某个节点只能到达一次是,直接打印
  • 当某个节点能到达两次时,在第二次到达时打印

3. Morris后序遍历

public void printEdge(Node X){//逆序打印以X为头的树的右边界
    Node tail=reverseEdge(X);
    Node cur=tail;
    while(cur!=null){
        System.out.print(cur.value);
        cur=cur.right;
    }
    reverseEdge(tail);
}
public Node reverseEdge(Node from){//将右边界组成的链表逆向
    Node pre=null;
    Node next=null;
    while(from!=null){
        next=from.right;
        from.right=pre;
        pre=from;
        from=next;
    }
    return pre;
}
public void morrisPosOrder(Node head){
    if(head==null) return;
    Node cur=head;
    Node mostRight=null;
    while(cur!=null){
        mostRight=cur.left;//mostRight为cur的左孩子
        if(mostRight!=null){//cur存在左子树
            while(mostRight.right!=null&&mostRight.right!=cur){//获取左子树最右节点
                mostRight=mostRight.right;
            }
            if(mostRight.right==null){//第一次到cur
                mostRight.right=cur;
                cur=cur.left;
                continue;
            }else{//mostRight.right==cur即是第二次到cur
                mostRight.right=null;
                printEdge(cur.left);//逆序打印该节点左树的右边界
            }
        }
        cur=cur.right;
    }
    printEdge(head);//打印整棵树的右边界
}

Morris先序遍历的思想为:在Morris遍历的基础上,仅当能到达两次的节点且为第二次到达该节点,则逆序打印该节点左树的右边界,最后打印整棵树的右边界。


二叉树问题解决思路

​ 在解决二叉树的相关问题时,首先可以考虑是否可以通过二叉树的遍历解决,而后再考虑是否可以通过分解问题的思路解决(将问题分解为多个子问题,利用递归解决)。

一、 遍历解决

在这里插入图片描述

针对与本题,不难想到可以利用二叉树的层序遍历解决

public List<Integer> rightSideView(TreeNode root) {
    List<Integer> list=new ArrayList<>();
    Queue<TreeNode> queue=new LinkedList<>();
    if (root==null) return list;
    queue.add(root);
    while (!queue.isEmpty()){
        int size=queue.size();
        for (int i=0;i<size;i++){
            TreeNode curNode=queue.poll();
            if (i==size-1){//该层最右侧元素
                list.add(curNode.val);
            }
            if (curNode.left!=null) queue.add(curNode.left);
            if (curNode.right!=null) queue.add(curNode.right);
        }
    }
    return list;
}![]()

在这里插入图片描述

对于本题,不难想到可以利用二叉树的先序遍历解决

public void preOrder(TreeNode root,ArrayList<TreeNode> list){
    if (root==null) return;
    list.add(root);
    preOrder(root.left,list);
    preOrder(root.right,list);
}
public void flatten(TreeNode root) {
    ArrayList<TreeNode> nodeList=new ArrayList<>();
    preOrder(root,nodeList);
    if (nodeList.size()>0) {
        TreeNode curNode = nodeList.get(0);
        for (int i = 1; i < nodeList.size(); i++) {
            TreeNode temp = nodeList.get(i);
            curNode.left = null;
            curNode.right = temp;
            curNode = temp;
        }
    }
}

二、非遍历问题解决模板

本模板并不适用于所有的二叉树问题。适用于再二叉树递归时,无需考虑左右子树的具体情况,只需对左右子树所提供的某一特定信息进行操作,并进行判断解决。

流程大致如下:

  1. 罗列问题可能的所有情况
  2. 罗列判断标准
  3. 确定所需信息
  4. 获取左右子树判断信息
  5. 对左右子树的信息进行判断
  6. 根据左右子树的信息结合本节点情况,返回本子树的信息
public void fun(){
    process(root);
}
public InfoType process(TreeNode root){
    if(root==null) {
        //当节点为null时,返回的信息
    }
    InfoType left=process(root.left);
    //从左子树获取信息
    InfoType right=process(root.right);
    //从右子树获取信息
    
    //根据left、right信息对问题的标准进行判断
    
    return new InfoType();
    //根据左右子树的信息返回该节点的信息
}

在这里插入图片描述

以判断平衡二叉树为例:

  1. 罗列问题可能的所有情况
    • 左右子树均为平衡二叉树,但左右子树高度差大于1
    • 左右子树均不为平衡二叉树
    • 左右子树仅一个为平衡二叉树
  2. 罗列判断标准
    • 该节点左子树为平衡二叉树
    • 该节点右子树为平衡二叉树
    • 左右子树高度差小于等于1
  3. 确定所需信息
    • 该子树是否为平衡二叉树
    • 该子树的高度
  4. 获取左右子树判断信息
  5. 依据判断标准对左右子树的信息进行判断
  6. 根据左右子树的信息结合本节点情况,返回本子树的信息
class ReturnType{
    public boolean isBalanced;
    public int height;
    public ReturnType(boolean isB,int hei){
        isBalanced=isB;
        height=hei;
    }
}
public boolean isBalanced(TreeNode root){
    return process(root).isBalanced;
}
public ReturnType process(TreeNode root){
    if(root==null) return new ReturnType(true,0);
    ReturnType leftData=process(root.left);
    ReturnType rightData=process(root.right);
    int height=Math.max(leftData.height,right.height)+1;
    boolean isBalanced=leftData.isBalanced && rightData.isBalanced && Math.abs(leftData.height-right.height)<2;
    return new ReturnType(isBalanced,height);
}

在这里插入图片描述

依据该模板对于本问题可以得出以下分析:

  1. 罗列问题可能的所有情况
    • 左右子树均为二叉搜索树,且左右子树于本节点满足二叉搜索树要求
    • 左右子树均为二叉搜索树,但左右子树于本节点不满足二叉搜索树要求
    • 左右子树均不为二叉搜索树
  2. 罗列判断标准
    • 左右子树均为二叉搜索树
    • 左子树最大值小于本节点
    • 右子树最小值大于本节点
  3. 确定所需信息
    • 是否为二叉搜索树
    • 左子树最大值
    • 右子树最小值
  4. 获取左右子树判断信息
  5. 依据判断标准对左右子树的信息进行判断
  6. 根据左右子树的信息结合本节点情况,返回本子树的信息
public class Info{
    public boolean isSearch;
    public Long max;//左子树最大值
    public Long min;//右子树最小值
    public Info(boolean isSearch,Long max,Long min){
        this.isSearch=isSearch;
        this.max=max;
        this.min=min;
    }
}
public Info process(TreeNode root){
    if (root==null) return new Info(true,Long.MIN_VALUE,Long.MAX_VALUE);
    Info left=process(root.left);
    Info right=process(root.right);
    boolean isSearch=false;
    Long max= Math.max(root.val, right.max);
    Long min= Math.min(root.val, left.min);
    if (left.isSearch&& right.isSearch&& root.val> left.max&&root.val< right.min){
        isSearch=true;
        return new Info(isSearch,max,min);
    }else return new Info(isSearch,max,min);
}
public boolean isValidBST(TreeNode root) {
    return process(root).isSearch;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
重建二叉树是一道经典的二叉树问题,其解题思路如下: 1. 首先,我们需要根据前序遍历序列和中序遍历序列来重建二叉树。前序遍历序列的第一个节点一定是二叉树的根节点,中序遍历序列中根节点左边的所有节点都属于二叉树的左子树,右边的所有节点都属于二叉树的右子树。 2. 我们可以通过前序遍历序列找到二叉树的根节点,然后在中序遍历序列中找到根节点的位置。根节点左边的所有节点都属于左子树,右边的所有节点都属于右子树。 3. 接下来,我们递归构建左子树和右子树。我们可以根据前序遍历序列和中序遍历序列中左右子树的节点数量确定左右子树的范围。 4. 重复以上步骤,直到遍历完整个序列,最终得到重建的二叉树。 下面是重建二叉树的代码实现,代码注释中有详细的解释。 ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def buildTree(preorder, inorder): """ :type preorder: List[int] :type inorder: List[int] :rtype: TreeNode """ # 如果前序遍历序列和中序遍历序列为空,返回None if not preorder or not inorder: return None # 前序遍历序列的第一个节点一定是二叉树的根节点 root_val = preorder[0] root = TreeNode(root_val) # 在中序遍历序列中找到根节点的位置 root_index = inorder.index(root_val) # 递归构建左子树和右子树 left_preorder = preorder[1:root_index+1] left_inorder = inorder[:root_index] left = buildTree(left_preorder, left_inorder) right_preorder = preorder[root_index+1:] right_inorder = inorder[root_index+1:] right = buildTree(right_preorder, right_inorder) # 将左右子树连接到根节点上 root.left = left root.right = right return root ``` 以上就是重建二叉树的详细解题思路和代码实现。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值