JAVA学习记录(十八)—— 二叉树进阶习题

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

class TreeNode{
    public char val;
    public TreeNode left;
    public TreeNode right;
    public TreeNode(char val){
        this.val = val;
    }
}

public class Main {
    
    public static int i = 0; // 将i定位静态,放在外面,这样每次在创建树的部分,i不会从0开始
    // 根据字符串创建二叉树
    public static TreeNode creatTree(String str){
        if(str == null || str.length() <= 0) return null;
        // 使用i遍历所给的字符串,当为字母时,创建结点
        // 构建根 -》 构建根的左 -》 构建跟的右
        // 构建结点之后i++;
        //int i = 0; i是不能放在这里的
        TreeNode root  = null; // 用来存放新建的结点
        if(str.charAt(i) != '#'){
            root = new TreeNode(str.charAt(i)); // 根据当前i下标的字符新建一个结点
            i++;
            root.left = creatTree(str);  // 递归左树
            root.right = creatTree(str); // 递归右树
            
        }else{
            // 如果遇到#号了,i++,跳过这个
            i++;
        }
        return root;
    }
    // 将创建的二叉树按中序遍历输出
    public static void inOrderTraversal(TreeNode root){
        if(root == null) return;
        inOrderTraversal(root.left);
        System.out.print(root.val + " ");
        inOrderTraversal(root.right);
    }
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        String str = scan.nextLine();
        // 根据读入的字符串创建一个二叉树
        TreeNode root = creatTree(str);
        inOrderTraversal(root);
    }
}

  2、二叉树的分层遍历
  • 给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
示例:
二叉树:[3,9,20,null,null,15,7],
    3
   / \
  9  20
    /  \
   15   7
返回其层序遍历结果:

[
  [3],
  [9,20],
  [15,7]
]
  • 需要使用到队列来进行处理
  • 1、如果当前root不为空,那么root入队
  • 2、判断队列是否为空?
    不为空:(1)弹出队头元素,且保存并打印;(2)判断当前的cur.left和cur.right
    为空: return 返回出去
public void levelOrderTraversal(TreeNode root){
    if(root == null) return;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root); //将当前跟节点入队
    while(!queue.isEmpty()){ // 只要队列不为空,就继续循环
        TreeNode cur = queue.poll(); // 创建一个引用来保存出队结点的信息
        System.out.print(cur.val + " "); // 打印当前出队的结点
        if(cur.left != null){
            queue.offer(cur.left); 
        }
        if(cur.right != null){
            queue.offer(cur.right); // 将此时所出结点的左右结点都入队
        }
    }
}
  • 运行逻辑为下
    在这里插入图片描述

  • 上述代码是打印出来层序遍历,力扣上的题需要返回一个二维数组,需要将返回的结果保存起来,运行逻辑与上图相同,只是多了一个list来存放每层的元素

// 将层序遍历的结果保存在二维数组里
    public List<List<Integer>> levelOrderTraversal(TreeNodee root){
        List<List<Integer>> ret = new ArrayList<>();
        if(root == null) return ret;
        Queue<TreeNodee> queue = new LinkedList<>(); // 使用一个队列来存放分层遍历的各元素
        queue.offer(root);  // 将当前根节点放入队列中
        while( !queue.isEmpty()){ // 只要队列不为空
            List<Integer> list = new ArrayList<>(); // 存放每一层的结点
            int size = queue.size(); // 因为会弹出队列,所以要将每次队列中的size都保存下来(当前队列不为空,有多少个元素)
            while(size > 0){
                TreeNodee cur = queue.poll();  // 当前弹出的元素;
                list.add(cur.val); //将当前弹出的元素放进list中
                if(cur.left != null){
                    queue.offer(cur.left);
                }
                if(cur.right != null){
                    queue.offer(cur.right);
                }
                size--; // 出去了一个元素,size--
            }
            // 此时队列里存放的是第二层的两个结点,不为空,进入外层while循环
            // new一个新的list,size此时等于2,依次往复

            // 每次走完一层,将这一层得到的list放入到ret二维数组中
            ret.add(list);
        }
        return ret;
    }

  3、给定一个二叉树,找到该树中两个指定节点的最近公共祖先
  • **给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
    百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
  • 使用前序遍历的思想
  • 1、先判断root根节点本身是不是p或q中的一个,是的话,root就是我们要找的
  • 2、不是的话,分别去找根的左和根的右
  • 2.1、左右两边都不为空(找到了p和q), root是公共祖先
  • 2.2、左边不为空,右边为空,左边第一次找到的节点就是公共祖先
  • 2.3、左边为空,右边不为空,右边第一次找到的结点就是公共祖先
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q){
    if(root == null) return null;
    if(root == p || root == q) return root;
    // 分别递归当前根节点的左边和右边
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p. q);
    if(left != null && right != null) return root;
    if(left != null) return left;
    if(right != null) return right;
    return null;
}
  4、二叉搜索树转换成排序双向链表
  • 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
  • 以中序遍历的思路,在遍历的过程中,顺便修改right和left的指向

在这里插入图片描述

  • 代码运行逻辑大致如下:
  • 1、中序遍历,pCur肯定是先走到2这个位置,要使变成一个链表,2的前驱要指向null,后继则指向5。
  • 2、先看前驱部分,我们要定义一个结点来存放我们的前驱部分,于是有了prev结点,起初它为null。
  • 3、这里就让此时pCur即2的前驱为nullpCur.left = null,并且让prev移到pCur处prev = pCur
  • 4、然后根据中序遍历,下一次该到的位置是pCur.right即此时2的右孩子结点,但他为空,于是就直接返回,此时在这一次的递归中2这个节点已经走完了,于是pCur就到了上一层的5上。
  • 5、此时在再次执行pCur.left = null,即将5节点的前驱指向2结点。
  • 6、根据上面步骤,连接了2和5节点的前驱,但还没有让2的后继指向5。
  • 7、接着上面的流程,此时pCur在5结点,prev在2结点,按照第3步应该要让prev移动到5节点处,但是,这时候应该让2的后继指向5,即prev.right = pCur,这个条件要添加到pCur.left = nullprev = pCur之间,但是直接加到中间在第一次递归执行时,便会因为当时prev还为null,此时的prev.right会出现空指针异常,所以应当增加一个if条件,在prev不为空的时候执行。
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public TreeNode prev = null;
    public void ConvertChild(TreeNode pCur) {
        if(pCur == null) return;
        ConvertChild(pCur.left);
        pCur.left = prev;
        if(prev != null) {
            prev.right = pCur;
        }
        prev = pCur;
        ConvertChild(pCur.right);
    }
    
    public TreeNode Convert(TreeNode pRootOfTree) {
        if(pRootOfTree == null) return null;
        ConvertChild(pRootOfTree);
        TreeNode head = pRootOfTree;
        while(head.left != null) {
            head = head.left;
        }
        return head;
    }
}
  5、根据一棵树的前序遍历与中序遍历构造二叉树
  • 前序遍历可以确定哪一个是根
  • 中序遍历可以确定左树和右树
  • 整体思路流程为:
      对于前序遍历[ABCDEFGHI]和中序遍历[HDBEIAFCG],此时中序遍历中inBeginH位置,inEndG,根据各自遍历的特点,A这个位置肯定是最开始的root点,在中序遍历中,用A将此中序数组分成左边右边两个部分,左边就是左树,右边就是右树,此时,以左树为例[HDBEI],当前的中序遍历数组中inBeginH位置,inEndI,在进入下一次递归建立树的时候,前序遍历的数组下标向后移动一位到B,明显B就是左树的根结点,这时在同样进行分割递归下去。详细步骤如下:
  • 1、判断所输入的两种遍历数组是否为空,不为空继续
  • 2、通过递归创建二叉树,所需的参数是<前序遍历的数组>、<前序遍历数组的下标位置>、<中序遍历数组>、<中序遍历数组中的开始位置(inBegin)和结束位置(inEnd)>
  • 3、由前序遍历数组,有了在前序遍历中根节点的下标,我们new一个新的根TreeNode root = new TreeNdoe(preorder[preIndex])
  • 4、有了根节点,找当前根节点在中序遍历数组中的位置index
    在中序遍历中找int[] inorder,找的是相等的值int key,范围是inBegin~inEnd
  • 5、找到了当前根结点在中序遍历数组中的位置后,再将前序遍历上的下标preIndex向后移动
  • 6、此时中序遍历数组中index的左边就是左树,右边就是右树
  • 7、前序遍历数组是一直不变的,变得是前序遍历的下标,下标在每次遍历后会向后走1
  • 8、中序遍历数组在一直变,每一次遍历时,针对当前index根节点,左树就是数组中inBegin ~ index - 1,右树就是数组中index + ~ inEnd
  • 9、分别递归中序遍历中标记处的左树和右树
  • 10、返回当前树得根结点
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    // 前序中的0下标必然是该二叉树的根节点
    // 我们在中序遍历中找出与前序中找到的根节点值相等的点,这个点就是根节点,根据中序遍历的性质,该结点左边是左树,右边是右树
    // 然后根据这个结点划分
    // 左边作为一个单独的树,右边作为一个单独的树
    // 再找通过递归上述步骤来逐步划分
    public int preIndex = 0; // 前序遍历的下标
    // 要建立起一个二叉树,必然还是使用递归,通过递归子问题,构建一个一个树
    public TreeNode buildTreeChild(int[] preorder, int[] inorder, int inbegin, int inend){
        if(inbegin > inend) return null; //说明此时没有子树
        // 两位置相等时,该节点已经是最下面的子节点了,此时在进行子树递归时,以左树为例,inend = index - 1,此时小于了inbegin

        // 要将当前这个根节点生成
        TreeNode root = new TreeNode(preorder[preIndex]);  // preorder[preIndex] 在前序遍历中找到的根节点的值
        //要在中序遍历中找到根节结点的位置
        int index = findValInorder(inorder, preorder[preIndex], inbegin, inend);
        // 在中序遍历内找到了根节点位置后,再在前序遍历上的下标preIndex向后移动
        preIndex++;
        
        // 此时在 index 的左边就是左树, 右边就是右树
        // 前序遍历数组是一直不变的,变得是前序遍历的下标,下标在每次遍历后会向后走1个位置
        // 中序遍历数组在一直变,每一遍历时,左树的位置就是 inBegin ~ index - 1的位置
        root.left = buildTreeChild(preorder, inorder, inbegin, index - 1);
        root.right = buildTreeChild(preorder, inorder,index + 1, inend);

        return root;
    }
    // 要在中序遍历中找到根节结点的位置
    public int findValInorder(int[] inorder, int key, int inbegin, int inend){
        for(int i = inbegin; i <= inend; i++){ //因为我们在最开始传入后序遍历下标时,传给递归的就是inorder.length-1,所以这里取等于
            if(inorder[i] == key){
                return i;      // 找到了返回i下标
            }
        }
        return -1;
    }
    public TreeNode buildTree(int[] preorder, int[] inorder) { // 输入的是前序遍历和中序遍历的数组
        if(preorder == null || inorder == null) return null;
        if(preorder.length == 0 || inorder.length == 0) return null;  //先判断给的两个数组是否合法
        // 输入数组合法,则通过递归创建二叉树
        return buildTreeChild(preorder, inorder, 0, inorder.length-1); // inorder.length-1表示的是中序便利的范围
    }
}

  6、根据一棵树的中序遍历与后序遍历构造二叉树
  • 思路与前序遍历是相同的,后序遍历同样可以确定根节点的位置,只是需要从数组的后面往前走
  • 注意:逆向走后序遍历的时候,递归是 先构建右子树,再构建左子树
class Solution {
    public int posIndex = 0; // 前序遍历数组最后一给元素的下标,是全局要用的,所以定义在外面,赋值在主构建函数中赋值
    public TreeNode buildTreeChild(int[] inorder, int[] postorder, int inbegin, int inend){
        if(inbegin > inend){
            return null;
        } 
        // 新建当前根节点
        TreeNode root = new TreeNode(postorder[posIndex]);
        // 找到当前根节点在中序遍历数组中的位置
        int index = findValInorder(inorder, postorder[posIndex], inbegin, inend);
        // 将后序遍历数组的下标向前移动
        posIndex--;

        // 递归左树和右树
        root.right = buildTreeChild(inorder, postorder, index + 1, inend);
        root.left = buildTreeChild(inorder, postorder, inbegin, index - 1);
        
        
        return root;
    }
    // 在中序遍历数组中找到根结点的下标
    public int findValInorder(int[] inorder, int key, int inbegin, int inend){
        for(int i = inbegin; i <= inend; i++){
            if(inorder[i] == key){
                return i;
            }
        }
        return -1;
    }
    public TreeNode buildTree(int[] inorder, int[] postorder) { // 前序数组,后序数组
        // 首先判断给定数组是否合法
        if(inorder == null || postorder == null) return null;
        if(inorder.length == 0 || postorder.length == 0) return null;
        // 因为是从后往前走后序遍历数组,我们要拿到该数组的最后一个元素的下标
        posIndex = postorder.length - 1;
        // 递归创建树
        return buildTreeChild(inorder, postorder, 0, inorder.length-1);
    }
}

  7、根据二叉树创建字符串
  • 采用前序遍历的方式,将一个二叉树转换成一个由括号和整数组成的字符串。
输入: 二叉树: [1,2,3,4]
       1
     /   \
    2     3
   /    
  4     

输出: "1(2(4))(3)"
  • 以题中给的这个树为列,前序遍历,从1结点进入打印根节点,去左边,左边不为空,打印一个(,打印2,再向左边,不为空,打印一个(,打印4,再向左,为空,向右,为空,4结点结束,打印一个),去2结点的右边,为空,不打印,2节点结束,打印一个),去1结点的右边,不为空,打印一个(,打印3,3左右都是空,3结点结束,打印一个)
  • 总结:
    遇到左边不为空,加(;当前树遍历完了,加)
class Solution {
    public void tree2strChile(TreeNode t, StringBuilder sb){
        if(t == null) return;
        sb.append(t.val);
        if(t.left == null){
            if(t.right == null){
                return;
            }else{
                sb.append("()");
            }
        }else{
            sb.append("(");
            tree2strChile(t.left, sb);
            sb.append(")");
        }

        if(t.right == null){
            return;
        }else{
            sb.append("(");
            tree2strChile(t.right, sb);
            sb.append(")");
        }
    }
    public String tree2str(TreeNode t) {
        StringBuilder sb = new StringBuilder();
        tree2strChile(t,sb);
        
        return sb.toString();
    }
}
  8、前、中、后序非递归遍历(栈)
  • 二叉树递归改非递归常用的两种数据结构:栈、队列

  • (一)前序非递归遍历
    1、使用栈,定义一个引用cur 以 前序遍历来将每个元素放入栈里,只要cur不为空,就入栈
    2、 当cur为空时,弹出栈顶元素,用引用top来记录,此时让cur走向弹出元素的右边,即cur = top.right
    3、当栈为空的时候,说明遍历完了整个二叉树

public void preOrderTraversalNor(TreeNode root){
    if(root == null) return;
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    while(cur != null || !stack.isEmpty()){
	   while(cur != null){
	       stack.push(cur);
	       System.out.print(cur.val + " ");
	       cur = cur.left;
	   }
	   // 此时说明已经到了当前树的最左边了
	   TreeNode top = stack.pop();
	   cur = top.right;
	}
}
  • (二)中序非递归遍历
  • 与前序不同的时是:出栈一次打印一次
public void inOrderTraversalNor(TreeNode root){
    if(root == null) return;
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    while(cur != null || !stack.isEmpty()){
	    while(cur != null){
	    	stack.push(cur);
	    	cur = cur.left;
	    }
	    TreeNode top = stack.pop();
	    System.out.print(top.val + " ");
	    cur = top.right;
    }
}

  • (三)后序非递归遍历
  • 在cur等于空的时候不能直接弹出栈,因为根据后序遍历的规则还要看当前根节点的右边,最后才看根节点
public void postOrderTraversalNor(TreeNode root){
    if(root == null) return null;
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    TreeNode prev = null; //只要结点被打印了,就指向这个结点
    while(cur != null || !stack.isEmpty()){
		while(cur != null){
			stack.push(cur);
			cur = cur.left;
		}
		TreeNode top = stack.peek();
		if(top.right == null || top.right == prev){ // top.right == null || top.right == 打印过了
			stack.pop();
			System.out.print(top.val + " ");
			prev = top;
		}else{
			cur = top.right;
		}
    }
}

  9、递增顺序查找树:给你一个树,请你按中序遍历重新排列树,是树中最左边的结点现在是树的根,且每个节点没有左子节点,只有一个右子节点。力扣(LeetCode)
示例 :
输入:[5,3,6,2,4,null,8,1,null,null,null,7,9]

       5
      / \
    3    6
   / \    \
  2   4    8
 /        / \ 
1        7   9

输出:[1,null,2,null,3,null,4,null,5,null,6,null,7,null,8,null,9]

 1
  \
   2
    \
     3
      \
       4
        \
         5
          \
           6
            \
             7
              \
               8
                \
                 9  

提示:
给定树中的结点数介于 1 和 100 之间。
每个结点都有一个从 0 到 1000 范围内的唯一整数值。
  • 中序遍历+构造新的树
    在树上进行中序遍历,就可以从小到大得到树上的结点,把这些节点的对应值放在数组中,此时数组中存放的值已是有序的,直接根据数据构建树
class Solution{
    public TreeNode increasingBST(TreeNode root){
        List<Integer> vals = new  ArrayList();
        inorder(root, vals);             //首先根据中序遍历,得到原树中的元素(此题得到的将会是有序数组)
        TreeNode ans = new TreeNode(0);  // 作用相当与链表中的头节点
        TreeNode cur = ans;              // 用来遍历新树的下标(引用)
        for(int v : vals){               // 遍历储存的数组
            cur.right = new TreeNode(v); // 对当前root的right赋值
            cur = cur.right;             // cur移动到当前root的right子结点处
        }
        return ans.right;                // 输出是从1开始的。0不输出
    }
    public void inorder(TreeNode node, List<Integer> vals){ // 标准的中序遍历结构
        if(node == null) return;
        inorder(node.left, vals);  // 左结点
        vals.add(node.val);        // 添加根节点
        inorder(node.right, vals); // 右结点
    }
}
  10、合并二叉树
  • 给定两个二叉树,将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。力扣(LeetCode)
示例 1:
输入: 
	Tree 1                     Tree 2                  
          1                         2                             
         / \                       / \                            
        3   2                     1   3                        
       /                           \   \                      
      5                             4   7       
                 
输出: 
合并后的树:
	     3
	    / \
	   4   5
	  / \   \ 
	 5   4   7
注意: 合并必须从两个树的根节点开始。
  • 深度优先搜索
    从根节点开始同时遍历两个二叉树,并将对应的节点进行合并
    对应的节点有三种情况,每种有不同的合并方式:
    (1)如果两个二叉树的对应结点都为空,则合并后的二叉树的对应节点也为空;
    (2)如果两个二叉树的对应节点只有一个为空,则合并后的二叉树的对应节点为其中的非空结点;
    (3)如果两个二叉树的对应节点都不为空,则合并后的二叉树的对音G节点的值为两个二叉树的对应节点的值之和。
class Solution{
    public TreeNode mergeTrees(TreeNode t1, TreeNode t2){
        if(t1 == null) return t2;
        if(t2 == null) return t1;
        TreeNode merged = new TreeNode(t1.val + t2.val); 
        merged.left = mergeTrees(t1.left, t2.left);
        merged.right = mergeTrees(t1.right, t2.right);
        return merged;
    }
}

  11、二叉树的最大宽度
  • 使用队列来层序遍历二叉树,将每层元素入队
  • 子树结点入队由公式 父节点n ;左孩子结点(2n) ; 右孩子结点(2n+1)
public int widthOfBinaryTree(TreeNode root) {
        if(root == null) return 0;
        Queue<TreeNode> q = new LinkedList<>(); //记录层序遍历的顺序性
        LinkedList<Integer> list = new LinkedList<>(); // 存储层次遍历后的树
        q.offer(root);   // 当前根入队
        list.add(1);    // 将当前跟放入链表,后续标记子树的下标
        int res = 1; // 存放最大宽度的结果
        while (!q.isEmpty()) {
            int count = q.size(); //这一层的节点个数
            for(int i =count; i> 0; i--) {
                TreeNode cur = q.poll();
                Integer curIndex = list.removeFirst(); 
                if(cur.left != null) {
                    q.offer(cur.left);
                    list.add(curIndex * 2); // 当前节点的左子节点是2i
                }
                if(cur.right != null) {
                    q.offer(cur.right);
                    list.add(curIndex * 2 +1); // 当前节点的右子节点是2i+1
                }
            }
            //  for循环走一遍,说明遍历完了一层
            // list中sizew为1的情况下,宽度也为1,没有必要计算。k
            //   这一层的宽度是链表的最后一个元素减第一个元素
            if(list.size() >= 2) {
                res = Math.max(res, list.getLast() - list.getFirst() + 1);
            }
        }
 
 
        return res;
    }

  12、二叉树的完全性检验
  • 使用队列以层序存放各层元素
  • 出队时连续出队两个Null时,则不是完全二叉树
class Solution {
    public boolean isCompleteTree(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<>();
        TreeNode prevNode = root;
        queue.add(root);
        while(!queue.isEmpty()){
            TreeNode cur = queue.poll();
            if(prevNode == null && cur != null){ // 连续出队的两个为null
                return false;
            }
            if(cur != null){
                queue.add(cur.left);
                queue.add(cur.right);
            }
            prevNode = cur;
        }
        return true;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值