数据结构 - 二叉树

本文深入探讨了二叉树在算法中的应用,涵盖翻转等价判断、节点连接、展开成链表、构建最大树、前序中序构造、寻找相同子树、BST元素查找、转换、合法性判断、节点删除、路径总和、合并、双向链表转换、子结构判断及剪枝等操作。通过递归和遍历策略解决各种二叉树问题。
摘要由CSDN通过智能技术生成

二叉树的遍历思想在很多算法中体现。快速排序的本质是二叉树的先根遍历,归并排序和分治算法本质是后根遍历。
"二叉树题目的难点在于如何通过题目的要求思考出 每一个节点需要做什么,在什么时候做 "。

1.判断是否翻转等价二叉树

写递归函数时首先确定递归出口

class Solution {
    public boolean flipEquiv(TreeNode root1, TreeNode root2) {
        if (root1 == null && root2 == null) {
            return true;
        } else if (root1 == null || root2 == null) {
            return false;
        } else {
            return (root1.val == root2.val) && ((flipEquiv(root1.left, root2.left) && flipEquiv(root1.right, root2.right)) || (flipEquiv(root1.right, root2.left) && flipEquiv(root1.left, root2.right)));
        }
    }
}

2.填充二叉树节点的右侧指针

class Solution {
    public Node connect(Node root) {
        if (root == null) {
            return root;
        }
        findNext(root.left, root.right);
        return root;
    }

    private void findNext(Node l, Node r) {
        if (l == null) {
            return;
        }
        l.next = r;
        findNext(l.left, l.right);
        findNext(l.right, r.left);
        findNext(r.left, r.right);
    }
}

3.将二叉树按先根顺序展开成链表(即只有右子节点的树)

题目要求:
在这里插入图片描述
每个节点应该把自己的左右子树拉直,并把拉直的左子树放到原来右子树的位置,把拉直的右子树接在拉直的左子树下面。

# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution(object):
    def flatten(self, root):
        """
        :type root: TreeNode
        :rtype: None Do not return anything, modify root in-place instead.
        """
        # 递归出口
        if root is None:
            return None

		# 先拉平左右子树
        left_flatten = self.flatten(root.left)
        right_flatten = self.flatten(root.right)

		# 后根遍历,处理根节点的位置
		# 按照先根遍历顺序,将左子树的结果,拼接在根节点的右边,并将右子树的结果追加在后面
        root.right = left_flatten
        root.left = None

        if (left_flatten is None):
            root.right = right_flatten
        else:
            while (left_flatten.right is not None):
                left_flatten = left_flatten.right
            left_flatten.right = right_flatten

        return root

4.根据数组构建最大二叉树

  • 最大二叉树:左子树是由数组中,最大元素左边的数字构成的最大二叉树。
  • 先根遍历,思想类似于快速排序。
class Solution(object):
    def constructMaximumBinaryTree(self, nums):
        """
        :type nums: List[int]
        :rtype: TreeNode
        """
        if len(nums) == 0:
            return None
        # 寻找最大值下标
        max_idx = nums.index(max(nums))
        return TreeNode(nums[max_idx], self.constructMaximumBinaryTree(nums[: max_idx]), 
        				self.constructMaximumBinaryTree(nums[max_idx + 1: ]))

5.通过前序和中序遍历结果构造二叉树

  • 前序的第一个元素为整棵树的根节点,中序遍历中,根节点左侧元素构成左子树,右侧元素构成右子树,递归构造即可
  • 知道这个规律后很简单,和4几乎完全一致
class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        if (preorder.length == 0) {
            return null;
        }

        TreeNode root = new TreeNode(preorder[0]);

        // 确定根在中序序列的下标
        int inorderRootIdx = 0;
        for (int i = 0; i < inorder.length; i++) {
            if (inorder[i] == root.val) {
                inorderRootIdx = i;
                break;
            }
        }

        int[] leftPreorder = Arrays.copyOfRange(preorder, 1, inorderRootIdx + 1);
        int[] leftInorder = Arrays.copyOfRange(inorder, 0, inorderRootIdx);
        int[] rightPreorder = Arrays.copyOfRange(preorder, inorderRootIdx + 1, preorder.length);
        int[] rightInorder = Arrays.copyOfRange(inorder, inorderRootIdx + 1, inorder.length);

        root.left = this.buildTree(leftPreorder, leftInorder);
        root.right = this.buildTree(rightPreorder, rightInorder);

        return root;
    }
}

6.寻找相同子树——序列化*

  • 大致思路是,采用后根遍历,自底向上进行(因为要先知道底层的结构,才能判断有无相同子树)
  • 既然比较“相同”,需要设置备忘录,记录已经见过的树的结构
  • 引入序列化,字符串比较替换递归的节点比较
class Solution {

    Map<String, Integer> str2Times = new HashMap<>();  // 存放序列化值到次数的映射
    Map<TreeNode, String> node2Str = new HashMap<>();  // 存放节点到序列化值的映射
    List<TreeNode> result = new LinkedList<>();

    public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
        postOrder(root);
        return result;
    }

    private void postOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        // 自底向上构造
        findDuplicateSubtrees(root.left);
        findDuplicateSubtrees(root.right);
        // 处理当前节点,看是否出现过相同结构的子树
        String currStr = getSerializedTree(root);
        if (str2Times.keySet().contains(currStr)) {
            if (str2Times.get(currStr) == 1) {  // 相同结构只添加一次(只有第二次出现时,将节点添加到结果)
                result.add(root);
            }
            str2Times.put(currStr, str2Times.get(currStr) + 1);
        } else {
        	// 没有见过当前结构
            str2Times.put(currStr, 1);
        }
    }

    private String getSerializedTree(TreeNode root) {
        // 获取树的先根(带空符号)遍历顺序
        if (root == null) {
            return "#";
        }
        if (node2Str.keySet().contains(root)) {
            return node2Str.get(root);
        }
        String str = String.valueOf(root.val) + "," + getSerializedTree(root.left) + "," + getSerializedTree(root.right);
        node2Str.put(root, str);
        return str;
    }
}

7.BST中第k大的元素

  • 凡提及BST,一定用到的思路是:
    • BST左子树的节点都小于当前节点,右子树节点都大于当前节点
    • BST的中序遍历是升序序列
class Solution {
    int count = 0;

    public int kthLargest(TreeNode node, int k) {
        if (node == null) {
            return -111;
        }
        int l = kthLargest(node.right, k);
        if (l != -111) {
            return l;  // 目标值在右子树
        } else if (count == k - 1) {
            return node.val;  // 目标值为当前节点
        } else {
            count++;
            return kthLargest(node.left, k);  // 目标值在左子树
        }
    }
}

8.BST转换为累加树

  • 题目要求:给出BST的根节点,该树的节点值各不相同,请你将其转换为累加树,使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和
  • 从题目得知,累加操作要从最大的元素开始,直到最小的元素
  • 很快想到要用 右子树->根节点->左子树 的中根遍历顺序解决问题
class Solution {

    int sum = 0;

    public TreeNode convertBST(TreeNode root) {
        // 中根遍历:右-中-左
        if (root == null) {
            return null;
        }
        inorder(root);
        return root;
    }

    private void inorder(TreeNode node) {
        if (node == null) {
            return;
        }
        inorder(node.right);
        sum += node.val;
        node.val = sum;
        inorder(node.left);
    }
}

9.判断BST是否合法

最简单的思路是中序遍历检查是否递增,只需记录前一个节点的值
需要自底向上进行,后根遍历。
对于每个节点,保证自己的左右子树为BST,同时节点大于左子树最大值,小于右子树最小值。

/* 方法1 */
class Solution {
    public int prev = -1;
    public boolean isValidBST(TreeNode root) {
        // 中序遍历即可
        if (root == null) {
            return true;
        }

        boolean l = this.isValidBST(root.left);
        if (!l) {
            return false;
        }

        boolean m = true;
        if (this.prev != -1) {
            m = this.prev < root.val;
        }
        this.prev = root.val;
        if (!m) {
            return false;
        }

        boolean r = this.isValidBST(root.right);
        return r;
    }
}

/* 方法2 */
class Solution {
    public boolean isValidBST(TreeNode root) {
        if (root == null || (root.left == null && root.right == null)) {
            return true;
        }
        return verify(root, Long.MAX_VALUE, Long.MIN_VALUE);
    }

    private boolean verify(TreeNode node, long high, long low) {
        if (node == null) {
            return true;
        }
        long l = (node.left == null) ? Long.MIN_VALUE : node.left.val;
        long r = (node.right == null) ? Long.MAX_VALUE : node.right.val;
        // 先根遍历,携带界限向下探索
        return node.val < high && node.val > low && node.val > l && node.val < r && verify(node.left, node.val, low) && verify(node.right, high, node.val);
    }
}

10.删除BST指定节点**

对于不具有子节点的待删除点,直接删除即可。
具有单个子节点,用子节点替换。
具有两个子节点,需要用左子树的最大节点/右子树的最小节点替换。

利用节点作为返回值,减少冗余代码。

# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution(object):
    def deleteNode(self, root, key):
        """
        :type root: TreeNode
        :type key: int
        :rtype: TreeNode
        """
        # 不存在待删除节点
        if root is None:
            return None

		# 先根遍历
        # 分情况讨论:待删节点无子节点、有一个子节点、有两个子节点
        elif root.val == key:
            if root.left is None and root.right is None:
                return None
            elif root.left is not None and root.right is None:
                return root.left
            elif root.left is None and root.right is not None:
                return root.right
            else:
                # 有两个子节点时,让左侧最大或右侧最小的节点替代该节点
                # 这里选择用左侧最大的节点
                left_node = root.left
                left_node_father = root
                while (left_node.right is not None):
                    left_node_father = left_node
                    left_node = left_node.right
                root.val = left_node.val
                root.left = self.deleteNode(root.left, root.val)  # ***精髓***

        # 当前节点不是待删节点,交给子树处理
        elif root.val > key:
            root.left = self.deleteNode(root.left, key)

        else:
            root.right = self.deleteNode(root.right, key)

        return root

11.路径总和III

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

  • 使用DFS+前缀和的思路。DFS的一个明显好处是,每条正在搜索的路径一定符合题目中对路径的要求
  • 将 path[0]设置为0,可以方便统计从根节点到某个节点的前缀和
class Solution {

    int count = 0;
    List<Long> path = new LinkedList<>();  // 记录当前路径上的前缀和

    public int pathSum(TreeNode root, int targetSum) {
        // 加入空节点,方便计算路径包括根节点的值
        path.add(0L);
        // 执行搜索
        dfs(root, targetSum);
        return count;
    }

    private void dfs(TreeNode node, int targetSum) {
        // 递归出口
        if (node == null) {
            return;
        }
        // 计算当前位置的前缀和
        long tempPrefixSum = path.get(path.size() - 1) + node.val;
        // 寻找当前路径是否有符合条件的路径
        for (int i = 0; i < path.size(); i++) {
            if (tempPrefixSum - path.get(i) == targetSum) {
                count++;
            }
        }
        path.add(tempPrefixSum);
        dfs(node.left, targetSum);
        dfs(node.right, targetSum);
        path.remove(path.size() - 1);
    }
}

12.合并二叉树

合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。

class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        if (root1 == null && root2 == null) {
            return null;
        } else if (root1 == null) {
            return root2;
        } else if (root2 == null) {
            return root1;
        } else {
            return new TreeNode(root1.val + root2.val, mergeTrees(root1.left, root2.left), mergeTrees(root1.right, root2.right));
        }
    }
}

13.BST转双向有序循环链表*

  • 利用中序遍历BST得到递增序列的规则,使用全局的遍历 head 记录链表头,pre 记录之前的节点
  • 一个比较巧妙的思想:递归结束时 pre 是最后的节点,和 head 首尾相连
class Solution {
    Node head = null;  // 返回链表头节点
    Node pre = null;  // 前节点

    public Node treeToDoublyList(Node root) {
        if (root == null) {
            return null;
        }
        // 中序遍历并修改指针
        inorder(root);
        // 修改头尾节点指向
        head.left = pre;
        pre.right = head;
        return head;
    }

    private void inorder(Node node) {
        if (node == null) {
            return;
        }
        inorder(node.left);
        if (head == null) {
            head = node;
            pre = node;
        } else {
            node.left = pre;
            pre.right = node;
            pre = node;
        }
        inorder(node.right);
    }
}

14.树的子结构

  • 判断判断B是不是A的子结构
  • 最后一行是核心:分别判断 pRoot2 是否是以 pRoot1、pRoot1.left、pRoot1.right 为根的树的子结构
    • isSubStructure(a, b):a中是否包含b,a不一定是首节点
    • find(a, b):从节点a和b开始,a是否包含b
class Solution {
    public boolean isSubStructure(TreeNode A, TreeNode B) {
        if (A == null || B == null) {
            return false;
        }
        return isSubStructureFromHere(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
    }

    private boolean find(TreeNode a, TreeNode b) {
        if (b == null) {
            return true;
        } else if (a == null) {
            return false;
        } else {
            return (a.val == b.val) && find(a.left, b.left) && find(a.right, b.right);
        }
    }
}

15.二叉树剪枝

给你二叉树的根结点 root ,此外树的每个结点的值要么是 0 ,要么是 1 。返回移除了所有不包含 1 的子树的原二叉树。

  • 删除过程要自底向上,所以后根遍历
class Solution {
    public TreeNode pruneTree(TreeNode node) {
        if (node == null) {
            return null;
        }
        // 后根遍历
        node.left = pruneTree(node.left);
        node.right = pruneTree(node.right);
        // 子节点被删除完了,且当前节点值为0,那么当前节点也要删除
        if (node.val == 0 && node.left == null && node.right == null) {
            return null;
        }
        return node;
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值