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