【算法日记】二叉树经典算法:转换双向链表,层序遍历,找公共祖先,构建二叉树

二叉搜索树与双向链表(BM30)

题目描述

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。如下图所示
在这里插入图片描述

数据范围:输入二叉树的节点数 0 ≤ n ≤ 1000,二叉树中每个节点的值 0 ≤ val ≤ 1000
要求:空间复杂度O(1)(即在原树上操作),时间复杂度 O(n)

注意:
1.要求不能创建任何新的结点,只能调整树中结点指针的指向。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继
2.返回链表中的第一个节点的指针
3.函数返回的TreeNode,有左右指针,其实可以看成一个双向链表的数据结构
4.你不用输出双向链表,程序会根据你的返回值自动打印输出

输入描述: 二叉树的根节点
返回值描述: 双向链表的其中一个头节点。

示例1
输入:{10,6,14,4,8,12,16}
返回值:From left to right are:4,6,8,10,12,14,16;From right to left are: 16,14,12,10,8,6,4;
说明:输入题面图中二叉树,输出的时候将双向链表的头节点返回即可。
示例2
输入:{5,4,#,3,#,2,#,1}
返回值:From left to right are:1,2,3,4,5;From right to left are:5,4,3,2,1;
说明:树的形状如下

                    5
                  /
                4
              /
            3
          /
        2
      /
    1

解题思路

题目要求返回一个固定节点,而递归调用是多次的,所以要分成两个方法。

  • ConvertChild():
    • 搜索二叉树的特点是:中序遍历得到的结果是有序的 所以要采用中序遍历。
    • prev来记录前一个节点。prev必须是类变量,不可以是局部变量,因为递归调用会重复把prev置空。
    • 转换成双向链表,left和right就代表逻辑链表节点上的左右。当前节点的left置为prev,prev(不为空的情况下)的right置为root。prev置为root
    • 此时题目给的pRootOfTree是链表的中心,把head移到链表左端,并返回

在这里插入图片描述

代码示例

public class Solution {
    TreeNode prev = null;
    public TreeNode Convert(TreeNode pRootOfTree) {
        if (pRootOfTree == null)
            return null;
        ConvertChild(pRootOfTree);
        TreeNode head = pRootOfTree;
        while (head.left != null)
            head = head.left;
        return head;
    }
    public void ConvertChild(TreeNode root) {
        if (root == null)
            return ;
        ConvertChild(root.left);
        if (prev != null)
            prev.right = root;
        root.left = prev;
        prev = root;
        ConvertChild(root.right);
    }
}

二叉树遍历(KY11)

题目描述

编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。
例如如下的先序遍历字符串: ABC##DE#G##F### 其中#表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。

输入描述: 输入包括1行字符串,长度不超过100。
输出描述: 可能有多组测试数据,对于每组数据, 输出将输入字符串建立二叉树后中序遍历的序列,每个字符后面都有一个空格。 每个输出结果占一行。

示例
输入:abc##de#g##f###
输出:c b e g d f a

解题思路

  • createTree():遍历字符串,因为函数要循环调用,字符串要依次遍历,所以下标i应该是类变量而非局部变量。
  • 如果字符不是#,则创建新节点,i自增,接着创建左树和右树
  • 如果字符是# ,则i自增,不创建节点。此时root还是null,返回上一个函数。
  • main函数:先调用createTree(),再中序遍历二叉树,输出字符
  • 为了防止牛客网给出多组测试字符串造成i过大,i要手动置为0

在这里插入图片描述
在这里插入图片描述

代码示例

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (in.hasNext()) { 
            String str = in.nextLine();
            TreeNode node =  createTree(str);
            inOrder(node);
            i = 0;
        }
    }
    public static int i = 0;
    static TreeNode createTree(String str){
        TreeNode root = null;
        if(str.charAt(i)!='#'){
            root = new TreeNode(str.charAt(i));
            i++;
            root.left = createTree(str);
            root.right = createTree(str);
        }
        else
            i++;
        return root;
    }
    static void inOrder(TreeNode root){
        if(root==null)
        return;
        inOrder(root.left);
        System.out.print(root.val+" ");
        inOrder(root.right);
    }
}

二叉树的层序遍历(LC102)

题目描述

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:
在这里插入图片描述
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]

解题思路

在这里插入图片描述
当队列不为空时,循环处理每一层:

  • 先获取当前队列大小,即当前层的节点数量
  • 创建一个临时列表存储当前层的节点值
  • 循环取出当前层的所有节点(循环次数为当前层节点数):
    • 取出队首节点,将其值加入临时列表
    • 如果该节点有左子节点,将左子节点入队
    • 如果该节点有右子节点,将右子节点入队
  • 当前层处理完毕后,将临时列表加入结果列表

代码示例

public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<>();
        if(root==null)
        return ret;

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while(!queue.isEmpty()){
            int size = queue.size();
            List<Integer> list = new ArrayList<>();
            while(size!=0){
                TreeNode cur = queue.poll();
                list.add(cur.val);
                if(cur.left!=null)
                    queue.offer(cur.left);
                if(cur.right!=null)
                    queue.offer(cur.right);
                size--;
            }
            ret.add(list);
        }
        return ret;
    }

二叉树的公共祖先(LC236)

题目描述

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

示例 1:
在这里插入图片描述
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
在这里插入图片描述
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2],p = 1, q = 2
输出:1

解题思路

  • 思路一:
    可以分成三种情况:
    1. 根节点root就是p或q 直接返回root
    2. p和q分别在左树和右树
    3. p和q都在左树或都在右树

分别定义 leftTree ,rightTree 在子树中遍历,如果两个都不是空,证明p,q在左右两侧,那么root就是最小公共祖先;如果其中一个为空,证明p,q在同一侧,返回已找到的公共祖先。

  • 思路二:
    利用栈存储从根节点到p和q的路径,找公共节点
    • getPath():先让root入栈,如果node是root,返回true;接着在左右子树寻找路径,如果左右子树中都没有,说明当前节点不是到达node的经过的节点,出栈。

代码示例

  • 思路一:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null)
            return null;
        if(p==root||q==root)
            return root;
        TreeNode leftTree = lowestCommonAncestor(root.left,p,q);
        TreeNode rightTree = lowestCommonAncestor(root.right,p,q);
        if(leftTree!=null&&rightTree!=null)
            return root;
        else if(leftTree!=null)
            return leftTree;
        else
            return rightTree;
    }
  • 思路二:
boolean getPath(TreeNode node,TreeNode root, Stack<TreeNode> stack){
        if(root==null)
            return false;
        stack.push(root);
        if(root==node)
            return true;
        boolean ret = getPath(node,root.left,stack);
        if(ret)
            return true;
        ret = getPath(node,root.right,stack);
        if(ret)
            return true;
        stack.pop();
        return false;
    }
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null)
            return null;
        Stack<TreeNode> s1 = new Stack<>();
        Stack<TreeNode> s2 = new Stack<>();
        getPath(p,root,s1);
        getPath(q,root,s2);
        int size1 = s1.size();
        int size2 = s2.size();
        if(size1>size2){
            int size = size1-size2;
            while(size!=0){
                s1.pop();
                size--;
            }
        }else{
            int size = size2-size1;
            while(size!=0){
                s2.pop();
                size--;
            }
        }
        while(!s1.isEmpty()){
            TreeNode tmp1 = s1.pop();
            TreeNode tmp2 = s2.pop();
            if(tmp1==tmp2){
                return tmp1;
            }
        }
        return null;
    } 

从前序和中序遍历序列构建二叉树(LC105)

题目描述

给定两个整数数组 preorderinorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例 1:
在这里插入图片描述
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1]
输出: [-1]

解题思路

在这里插入图片描述
为了便于递归下标参数的传递,另写一个函数。

前序遍历确定根节点,所以先创建根节点,在中序数组中找到根节点值的下标,下一次传参时创建左树的右边界就是preIndex-1;创建右树的左边界就是preIndex+1。

代码示例

    int preIndex;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return buildTreeChile(preorder,inorder,0,inorder.length-1);
    }
    public TreeNode buildTreeChile(int[] preorder,int[] inorder,int inBegin,int inEnd){
        if(inBegin>inEnd)
        return null;
        TreeNode root = new TreeNode(preorder[preIndex]);
        int rootIndex = getIndex(preorder[preIndex],inorder,inBegin,inEnd);
        preIndex++;
        root.left = buildTreeChile(preorder,inorder,inBegin,rootIndex-1);
        root.right = buildTreeChile(preorder,inorder,rootIndex+1,inEnd);
        return root;
    }
    int getIndex(int val,int[] inorder,int inBegin,int inEnd){
        for(int i =inBegin;i<=inEnd;i++ ){
            if(inorder[i]==val)
            return i;
        }
        return -1;
    }

从中序和后序遍历序列构建二叉树(LC106)

题目描述

给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

示例 1:
在这里插入图片描述
输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出:[3,9,20,null,null,15,7]
示例 2:
输入:inorder = [-1], postorder = [-1]
输出:[-1]

解题思路

与上一题的解法类似。需要注意的是:后序遍历的顺序是左 右 根,所以要在后序数组中从后往前遍历,先确定根节点,再创建右树,再创建左子树。

代码示例

 int postIndex;
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        postIndex = postorder.length-1;
        return buildTreeChild(inorder,postorder,0,inorder.length-1);
    }
    public TreeNode buildTreeChild(int[] inorder,int[] postorder,int inBegin,int inEnd){
        if(inBegin>inEnd)
        return null;

        TreeNode root = new TreeNode(postorder[postIndex]);
        int rootIndex = getIndex(postorder[postIndex],inorder,inBegin,inEnd);
        postIndex--;
        root.right = buildTreeChild(inorder,postorder,rootIndex+1,inEnd);
        root.left = buildTreeChild(inorder,postorder,inBegin,rootIndex-1);
        return root;
    }
    int getIndex(int val,int[] inorder,int inBegin,int inEnd){
        for(int i =inBegin;i<=inEnd;i++ ){
            if(inorder[i]==val)
            return i;
        }
        return -1;
    }

根据二叉树创建字符串(LC606)

题目描述

给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。

空节点使用一对空括号对 “()” 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。

示例 1:
在这里插入图片描述
输入:root = [1,2,3,4]
输出:"1(2(4))(3)"
解释:初步转化后得到 "1(2(4)())(3()())" ,但省略所有不必要的空括号对后,字符串应该是"1(2(4))(3)"
示例 2:
在这里插入图片描述
输入:root = [1,2,3,null,4]
输出:"1(2()(4))(3)"
解释:和第一个示例类似,但是无法省略第一个空括号对,否则会破坏输入与输出一一映射的关系。

解题思路

为了方便函数递归传参,新写一个函数。

  1. 先添加root的值,再判断左树,如果左树不为空,添加左括号,递归调用添加左数的值,添加右括号,
  2. 如果左树为空,再判断此时右树,右树如果也为空,就可以直接返回,如果右树不为空,那么左树的括号不能省略。
  3. 再单独判断右树,如果右树不为空,与左树类似,先添加左括号,再递归调用,再添加右括号

代码示例

 public String tree2str(TreeNode root) {
        StringBuilder ret = new StringBuilder();
        tree2strChild(root,ret);
        return ret.toString();
    }
    void tree2strChild(TreeNode root,StringBuilder s){
        if(root==null)
        return ;
        s.append(root.val);
        if(root.left!=null){
            s.append("(");
            tree2strChild(root.left,s);
            s.append(")");
        }else{
            if(root.right!=null)
            s.append("()");
            else
            return;
        }
        if(root.right!=null){
            s.append("(");
            tree2strChild(root.right,s);
            s.append(")");
        }
    }

非递归法前序遍历(LC144)

  • 左子树(内层循环):
    • 当cur不为空时,将其入栈并访问
    • 然后移动cur到其左子节点,继续上述操作
    • 这个过程实现了 “根 - 左” 的访问顺序,直到左子树尽头
  • 右子树处理:
    • 当左子树遍历完成(cur为空),弹出栈顶节点top
    • 将cur指向top的右子节点
    • 此时外层循环会继续处理右子树,重复左子树的遍历逻辑
    void preOrder(TreeNode root){
        TreeNode cur = root;
        Stack<TreeNode> stack = new Stack<>();
        while(cur!=null||!stack.isEmpty()){
            while(cur!=null){
                stack.push(cur);
                System.out.println(cur.val+" ");
                cur=cur.left;
            }
            TreeNode top =stack.pop();
            cur = top.right;
        }
    }

非递归法中序遍历(LC94)

与前序遍历类似,区别在于中序遍历先访问左树,所以要在内层循环结束(也就是到左子树尽头)再弹出并访问

    void inOrder(TreeNode root){
        TreeNode cur = root;
        Stack<TreeNode> stack = new Stack<>();
        while(cur!=null||!stack.isEmpty()){
            while(cur!=null){
                stack.push(cur);
                cur=cur.left;
            }
            TreeNode top =stack.pop();
            System.out.println(top.val+" ");
            cur = top.right;
        }
    }

非递归法后序遍历(LC145)

后序遍历要先保证右树遍历后再访问根,所以在输出根之前,先判断右树是否为空,如果为空,输出根节点并把根节点弹出;如果不为空则把右树赋值给cur继续遍历。

  • 如图,当cur为9时,右树不为空,所以把8赋值cur,此时8的右树为空,所以输出8并弹出。
  • 下一轮循环中top依旧是9,这样就陷入了死循环。
  • 应该记录右树是否被遍历过,输出的判断条件是右树为空或者右树被遍历过
  • 定义引用prev,当8输出的时候把8赋值给prev,这样下一轮循环中先判断prev是9的右树,再输出9.
    在这里插入图片描述
void postOrder(TreeNode root){
        TreeNode cur = root;
        TreeNode prev = null;
        Stack<TreeNode> stack = new Stack<>();
        while(cur!=null||!stack.isEmpty()){
            while(cur!=null){
                stack.push(cur);
                cur=cur.left;
            }
            TreeNode top =stack.peek();
            if(top.right==null||prev==top.right){
                System.out.println(top.val);
                stack.pop();
                prev=top;
            }else{
                cur=top.right;
            }
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值