【算法】005_二叉树

1. 用递归和非递归的方式实现二叉树的先序、中序、后序遍历

先序遍历

递归

public void preOrderRecur(TreeNode root){
    if (root == null){
        return;
    }
    System.out.println(root.val + " ");
    preOrderRecur(root.left);
    preOrderRecur(root.right);
}

非递归

  • 从栈中弹出一个结点cur
  • 打印,即访问该结点
  • 先把cur的右孩子入栈,再将左孩子入栈
  • 重复上述步骤

中序遍历

递归

public void inOrderRecur(TreeNode root){
    if (root == null){
        return;
    }
    inOrderRecur(root.left);
    System.out.println(root.val + " ");
    inOrderRecur(root.right);
}

递归

  • 将子树的左孩子入栈
  • 直到没有左孩子的时候,弹出结点root
  • 判断root的右孩子是否为空
    • 如果有右孩子结点,重复上面的过程,将这个结点的左孩子入栈
    • 如果没有右孩子,继续弹栈
  • 每次弹出的时候就打印结点,遇到空结点就会弹栈,只要有左孩子就压栈,没有左孩子时就到右孩子上去
public void inOrderUnRecur(TreeNode root){
    if (root == null){
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    while (!stack.isEmpty() || root != null){
        if (root != null){
            stack.push(root);
            root = root.left;
        }else{
            root = stack.pop();
            System.out.println(root.val + " ");
            root = root.right;
        }
    }
}

后序遍历

递归

public void postOrderRecur(TreeNode root){
    if (root == null){
        return;
    }
    postOrderRecur(root.left);
    postOrderRecur(root.right);
    System.out.println(root.val + " ");
}

非递归

先序遍历是中左右,后序遍历是左右中,调整一下顺序,使得遍历时为中右左,然后翻转就得到左右中

  • 从栈中弹出一个结点cur
  • cur放入收集数组中
  • 先将cur的左孩子入栈,再将右孩子入栈
  • 重复上述步骤
  • 翻转收集数组

也可以用一个栈来收集结果,由于栈是先进后出,就不用再翻转了

public void postOrderUnRecur(TreeNode root){
    if (root == null){
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    List<TreeNode> list = new ArrayList<>();
    stack.push(root);
    while (!stack.isEmpty()){
        TreeNode cur = stack.pop();
        list.add(cur);
        if (cur.left != null){
            stack.push(cur.left);
        }
        if (cur.right != null){
            stack.push(cur.right);
        }
    }
    Collections.reverse(list);
    list.forEach(node -> {
        System.out.println(node.val);
    });
}

2. 二叉树的最大宽度

为二叉树的结点编号

可以得到每一层的结点数量,第一个和最后一个结点的编号差值,就是每一层的宽度

public int getMaxWidth(TreeNode root){
    if (root == null){
        return 0;
    }
    LinkedList<TreeNode> queue = new LinkedList<>();
    HashMap<TreeNode, Integer> map = new HashMap<>();
    int ans = Integer.MIN_VALUE;
    queue.add(root);
    map.put(root, 0);
    while (!queue.isEmpty()){
        int size = queue.size();//每一层的结点数量
        int width = map.get(queue.get(size - 1)) - map.get(queue.get(0)) + 1;
        ans = Math.max(ans, width);
        while (size > 0){
            TreeNode cur = queue.pop();
            if (cur.left != null){
                queue.add(cur.left);
                map.put(cur.left, map.get(cur) * 2 + 1);
            }
            if (cur.right != null){
                queue.add(cur.right);
                map.put(cur.right, map.get(cur) * 2 + 2);
            }
            size--;
        }
    }
    return ans;
}

3. 判断一棵二叉树是否是搜索二叉树

递归

左子树上所有结点的值均小于它的根结点;右子树上所有结点均大于它的根结点。以中序遍历序列表示的时候是从小到大,所有结点的大小就是位于第一个结点和最后一个结点中间的。

isValidBST(TreeNode node, long lower, long upper)表示以node为根的子树,它的结点是否在lower和upper的范围里。

public boolean isBST(TreeNode root){
    return isValidBST(root, Integer.MIN_VALUE, Integer.MAX_VALUE);
}

public boolean isValidBST(TreeNode node, long lower, long upper){
    if (node == null){
        return true;
    }
    //node的 大小越界
    if (node.val <= lower || node.val >= upper){
        return false;
    }
    //递归左子树时修改上界,表示左子树所有结点都小于它的根结点
    //递归右子树同理
    return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper);
}

中序遍历

搜索二叉树的中序序列是递增的,所以中序遍历的时候判断当前结点是否大于上一个结点

public int pre = Integer.MIN_VALUE;
public boolean isBST2(TreeNode root){
    if (root == null){
        return true;
    }
    //中序遍历
    boolean left = isBST2(root.left);
    //判断左子树是否是搜索二叉树
    if (!left){
        return false;
    }
    //如果中序遍历时当前子树的根结点小于上一个结点,就说明这不是搜索二叉树
    if (root.val <= pre){
        return false;
    }else {
        //左子树包括根结点构成搜索二叉树,更新pre
        pre = root.val;
    }//左子树是搜索二叉树
    return isBST2(root.right);//左子树是搜索二叉树,当右子树也是搜索二叉树时,以root为根的树就是搜索二叉树
}

中序遍历非递归

public boolean isBST3(TreeNode root){
    if (root == null){
        return true;
    }
    Stack<TreeNode> stack = new Stack<>();
    while (!stack.isEmpty() || root != null){
        if (root != null){
            stack.push(root);
            root = root.left;
        }else{
            root = stack.pop();
            // System.out.println(root.val + " ");
            if (pre >= root.val){
                return false;
            }else {
                pre = root.val;
            }
            root = root.right;
        }
    }
    return true;
}

通用套路

当我们要判断root是否是搜索二叉树的时候,我们需要:

  • 左子树是搜索二叉树
  • 右子树是搜索二叉树
  • root大于左子树的最大值
  • root小于右子树的最小值

所以我们需要的左子树信息是:

  • 左子树是否是搜索二叉树
  • 左子树的最大值

需要的右子树的信息:

  • 右子树是否是搜索二叉树
  • 右子树的最小值

结果左右子树的规模就不一样了,那么我们就规定递归需要三个信息,将递归的规模统一:

  • 子树是否是搜索二叉树
  • 子树的最大值
  • 子树的最小值

有了左右子树的三个信息后,就可以知道root是否是搜索二叉树,以及搜索二叉树的最大值和最小值。

public boolean isBST4(TreeNode root){
    return process(root).isBST;
}
public class ReturnType{
    boolean isBST;
    int max;
    int min;
    public ReturnType(boolean isBST, int max, int min) {
        this.isBST = isBST;
        this.max = max;
        this.min = min;
    }
}
public ReturnType process(TreeNode root){
    if (root == null){
        return null;
    }
    ReturnType leftData = process(root.left);
    ReturnType rightData = process(root.right);

    int min = root.val;
    int max = root.val;
    //求了左子树的最大值和最小值后,又去与右子树比较,可以得到root的最大值和最小值
    //左子树的最大值和最小值
    if (leftData != null){
        min = Math.min(min, leftData.min);
        max = Math.max(max, leftData.max);
    }
    //右子树的最大值和最小值
    if (rightData != null){
        min = Math.min(min, rightData.min);
        max = Math.max(max, rightData.max);
    }//min是root树下的最小值;max是root树下的最大值。然后最后一行代码返回的时候,就得到了当前树也就是root的最大值和最小值
    
    boolean isBST = true;
    //左子树不平衡;或左子树的根小于等于左子树中的最小值,那么root这棵树就不平衡
    if (leftData != null && (!leftData.isBST || leftData.max >= root.val)){
        isBST = false;
    }
    //右子树同理
    if (rightData != null && (!rightData.isBST || rightData.min <= root.val)){
        isBST = false;
    }

    //返回我们需要的三个信息
    return new ReturnType(isBST,max,min);
}

4. 判断完全二叉树

  • 采用层序遍历
  • 如果一棵子树的根结点,它的左子树为空但右子树不为空,则这颗子树一定不是完全二叉树
  • 满足上面条件的情况下,如果遇到了一棵子树,左孩子不为空但右孩子为空,则之后层序遍历的所有结点必须是叶子结点
public boolean isCBT(TreeNode root){
    if (root == null){
        return true;
    }
    LinkedList<TreeNode> queue = new LinkedList<>();
    boolean leaf = false;
    queue.add(root);
    while (!queue.isEmpty()){
        TreeNode cur = queue.poll();
        if ((leaf && (cur.left!=null || cur.right != null)) || (cur.left == null && cur.right != null)){
            return false;
        }
        if (cur.left != null && cur.right == null){
            leaf = true;
        }
        if (cur.left != null){
            queue.add(cur.left);
        }
        if (cur.right != null){
            queue.add(cur.right);
        }
    }
    return true;
}

5. 判断平衡二叉树

后序遍历

对二叉树做后序遍历,从底至顶返回子树深度,若遇到子树不是平衡二叉树,则进行剪枝,直接向上返回。

  • 返回值
    • 当左右子树深度差小于等于1,就返回当前子树的深度
    • 当左右子树深度差大于1,返回-1,表示子树不是平衡二叉树
  • 终止条件
    • 当root为空,返回高度为0
    • 当左子树或者右子树深度为-1,说明左子树或者右子树不是平衡二叉树,剪枝,返回-1
public boolean isBalanced(TreeNode root){
    return recur(root) != -1;
}

public int recur(TreeNode root){
    if (root == null){
        return 0;
    }
    int left = recur(root.left);
    //剪枝
    if (left == -1){
        return -1;
    }
    int right = recur(root.right);
    //剪枝
    if (right == -1){
        return -1;
    }
    return Math.abs(left - right) <= 1 ? Math.max(left, right) + 1 : -1;
}

通用套路

一棵树是平衡二叉树必须满足三个条件:左子树是平衡二叉树;右子树是平衡二叉树;左右子树高度差小于等于1。

要判断root是否是平衡二叉树,我们就需要根据左右子树的信息来进行判断。

需要的左子树的信息:

  • 左子树是否是平衡二叉树
  • 左子树的高度

需要的右子树的信息:

  • 右子树是否是平衡二叉树
  • 右子树的高度

有了左右子树的信息,就可以知道左右子树是否是平衡二叉树,以及左右子树的高度差,然后就可以判断root是否是平衡二叉树。

在进行递归的时候,我们就需要:子树的高度、子树是否是平衡二叉树。返回的时候就可以得到root的两个信息。

public boolean isBalanced2(TreeNode root){
    return process(root).isBalanced;
}
public class ReturnType{
    public boolean isBalanced;//是否是平衡二叉树
    public int height;//高度

    public ReturnType(boolean isBalanced, int height) {
        this.isBalanced = isBalanced;
        this.height = height;
    }
}

public ReturnType process(TreeNode root){
    if (root == null){
        return new ReturnType(true, 0);//高度为0的平衡二叉树
    }

    ReturnType leftData = process(root.left);
    ReturnType rightData = process(root.right);

    int height = Math.max(leftData.height, rightData.height) + 1;
    //左子树是平衡二叉树、右子树是平衡二叉树、左右子树高度差小于等于1
    boolean isBalanced = leftData.isBalanced && rightData.isBalanced && Math.abs(leftData.height - rightData.height) <= 1;

    return new ReturnType(isBalanced, height);
}

6. 判断满二叉树

public boolean isFullTree(TreeNode root){
    if (root == null){
        return true;
    }
    ReturnType ret = isFull(root);
    return ret.node_cnt == (1 << ret.node_cnt - 1);
}

public class ReturnType{
    int height;
    int node_cnt;
    public ReturnType(int height, int node_cnt) {
        this.height = height;
        this.node_cnt = node_cnt;
    }
}
public ReturnType isFull(TreeNode root){
    if (root == null){
        return new ReturnType(0, 0);
    }

    ReturnType leftData = isFull(root.left);
    ReturnType rightData = isFull(root.right);

    int height = Math.max(leftData.height, rightData.height) + 1;
    int node_cnt = leftData.node_cnt + rightData.node_cnt + 1;

    return new ReturnType(height, node_cnt);
}

7. 给定两个二叉树的结点p和q,找到它们的最低公共祖先结点

递归

终止条件

  • root为空时返回null

  • root等于p或者q时,返回root

    如果p是q的根或者q是p的根,那么直接返回p或者q

    如果pq在两侧,加入先递归到p,则返回p,后面遇到q的时候返回q,就会满足返回值中的第二种情况。

递推

  • 递归左子树,返回left
  • 递归右子树,返回right

返回值

  • 当left和right都为空,返回null

    说明root的左右子树中都不包含p和q

  • 当left和right都不为空,返回root

    说明p、q分列在root的异侧,即分别在root的左右子树,则root为p和q的最近公共祖先

  • 当left为空,right不为空,返回right

    说明pq都不在root的左子树中

  • 当left不为空,right为空,返回left

TreeNode lowestAncestor(TreeNode root, TreeNode p, TreeNode q){
    if (root == null || root == p || root == q){
        return root;
    }

    TreeNode left = lowestAncestor(root.left, p, q);
    TreeNode right = lowestAncestor(root.right, p, q);

    // if (left == null && right == null){
    //     return null;
    // }
    // if (left != null && right != null){
    //     return root;
    // }
    // if (left == null && right != null){
    //     return right;
    // }
    // if (left != null && right == null){
    //     return left;
    // }
    if (left == null){
        return right;
    }
    if (right == null){
        return left;
    }
    return root;
}

哈希表

哈希表fatherMap存储:key是当前结点,value是key的父结点

由于fatherMap表,我们就可以从p结点开始,依次获得它的父结点,然后一直往上直到根结点,将这个路线中的所有父结点存储在set中。

再从q开始,往上走,如果过程中遇到的结点出现在了set中,说明这个结点就是pq的公共祖先。

public TreeNode lowestAncestor2(TreeNode root, TreeNode p, TreeNode q){
    HashMap<TreeNode, TreeNode> fatherMap = new HashMap<>();
    //先将根结点放入map
    fatherMap.put(root, root);
    //递归,将所有结点和其父结点的键值对放入表中
    recur(root, fatherMap);
    HashSet<TreeNode> set = new HashSet<>();
    TreeNode cur = p;
    while (fatherMap.get(cur) != cur){//根结点的父结点等于本身
        set.add(cur);
        cur = fatherMap.get(cur);//遍历父结点
    }
    //根结点也要加入set中
    set.add(cur);
    cur = q;
    while (!set.contains(cur)){
        cur = fatherMap.get(cur);
    }
    return cur;
}

public void recur(TreeNode root, HashMap<TreeNode, TreeNode> fatherMap){
    if (root == null){
        return;
    }
    fatherMap.put(root.left, root);
    fatherMap.put(root.right, root);
    recur(root.left, fatherMap);
    recur(root.right, fatherMap);
}

8. 在二叉树中找到一个结点的后继结点

image-20240207222018256

  • x的前驱结点是x的左子树的最右结点,后继节点是右子树的最左结点

  • 当x有右子树的时候,右子树的最左结点就是x的后继结点

  • 当x无右子树的时候,假设x的后继结点是y(y的前驱节点是x),则y的左子树的最右结点是x。如何找到y?

    • 所以我们需要从x出发,向上寻找,依次遍历x的父结点。直到x是某个结点的左孩子,则这个结点就是我们要找的y,这个结点的左子树的最右结点就是x。

    • 如果parent为空的时候都没有找到,就说明x是整棵树的最右结点,即中序遍历下的最后一个结点,它没有后继结点。

    image-20240208220531893

public class Node {
    public int value;
    public Node left;
    public Node right;
    public Node parent;

    public Node(int data) {
        this.value = data;
    }
}

public Node getSuccessorNode(Node node){
    if (node == null){
        return null;
    }
    if (node.right != null){
        return getLeftMost(node.right);
    }
    Node parent = node.parent;
    //node != parent.left :从x出发,向上寻找一个父结点,这个父结点是y的左子树的时候,就找到了y是x的后继结点
    while (parent != null && node != parent.left){
        node = parent;
        parent = node.parent;
    }
    return parent;
}

public Node getLeftMost(Node node){
    if (node == null){
        return null;
    }
    while (node.left != null){
        node = node.left;
    }
    return node;
}

9. 二叉树的序列化和反序列化

先序遍历

/**
     * 先序序列化
     * @param root
     * @return
     */
public String serialByPre(TreeNode root){
    if (root == null){
        return "#";
    }
    String res = root.val + ",";
    res += serialByPre(root.left);
    res += serialByPre(root.right);
    return res;
}

/**
     * 先序反序列化
     * @param preStr
     * @return
     */
public TreeNode reconByPreString(String preStr){
    String[] values = preStr.split(",");
    return reconPreOrder(values);
}

int index = 0;
public TreeNode reconPreOrder(String[] values){
    String value = values[index++];

    if (value.equals("#")){
        return null;
    }

    TreeNode root = new TreeNode(Integer.valueOf(value));
    root.left = reconPreOrder(values);
    root.right = reconPreOrder(values);
    return root;
}

层序遍历

public String serialByLevel(TreeNode root){
    if (root == null){
        return "#,";
    }
    String res = root.val + ",";
    LinkedList<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()){
        TreeNode cur = queue.poll();
        TreeNode left = cur.left;
        TreeNode right = cur.right;
        if (left != null){
            queue.add(left);
            res += left.val + ",";
        }else {
            res += "#,";
        }
        if (right != null){
            queue.add(right);
            res += right.val + ",";
        }else {
            res += "#,";
        }
    }
    return res;
}

public TreeNode reconByLevelString(String levelStr){
    String[] values = levelStr.split(",");
    int index = 0;
    LinkedList<TreeNode> queue = new LinkedList<>();
    if (values[index].equals("#")){
        return null;
    }
    TreeNode root = new TreeNode(Integer.valueOf(values[index++]));
    queue.add(root);
    while (!queue.isEmpty()){
        TreeNode cur = queue.poll();

        String leftValue = values[index++];
        if (leftValue.equals("#")){
            cur.left = null;
        }else {
            cur.left = new TreeNode(Integer.valueOf(leftValue));
            queue.add(cur.left);
        }
        String rightValue = values[index++];
        if (rightValue.equals("#")){
            cur.right = null;
        }else {
            cur.right = new TreeNode(Integer.valueOf(rightValue));
            queue.add(cur.right); 
        }
    }
    return root;
}

10. 折纸问题

image-20240208134930623

  • 折纸的次数对应二叉树的层数
  • 凹折痕位于左子树,凸折痕位于右子树
  • 二叉树中序遍历的结果就是折痕的顺序
//i结点的层数
//down==true表示凹结点;down==false表示凸结点
public void printProcess(int i, int N, boolean down){
    //层数越界
    if (i > N){
        return;
    }
    //模拟二叉树
    printProcess(i + 1, N, true);//模拟遍历左子树,表示递归到第i+1层,是第i层的左子树
    System.out.println(down ? "凹" : "凸");
    printProcess(i + 1, N, false);
}

printProcess(1, N, true);
posted @ 2024-02-08 22:07  平平无奇的five  阅读(36)  评论(0)    收藏  举报