LeetCode分类专题(二)——二叉树1
iwehdio的博客园:https://www.cnblogs.com/iwehdio/
学习自:
二叉树题目:
- 二叉树:226、116、114、654、105、106、652
- 二叉树序列化:297
- 二叉搜索树:230、537、98、700、701、450
1、二叉树
- 写树相关的算法,简单说就是,先搞清楚当前 root 节点该做什么,然后根据函数定义递归调用子节点,递归调用会让孩子节点做相同的事情。
翻转二叉树
-
通过观察,我们发现只要把二叉树上的每一个节点的左右子节点进行交换,最后的结果就是完全翻转之后的二叉树。
class Solution { public TreeNode invertTree(TreeNode root) { //递归基 if(root==null) return null; TreeNode temp = root.left; root.left = root.right; root.right = temp; invertTree(root.left); invertTree(root.right); return root; } }
-
值得一提的是,如果把交换左右子节点的代码放在后序遍历的位置也是可以的,但是放在中序遍历的位置是不行的。因为在中序遍历位置,两次访问的都是左节点的数据。
-
二叉树题目的一个难点就是,如何把题目的要求细化成每个节点需要做的事情。
填充二叉树节点的右侧指针
-
如果只依赖一个节点的话,肯定是没办法连接「跨父节点」的两个相邻节点的。
-
我们的做法就是增加函数参数,一个节点做不到,我们就给他安排两个节点,「将每一层二叉树节点连接起来」可以细化成「将每两个相邻节点都连接起来」。
class Solution { public Node connect(Node root) { if(root==null) return null; connectTwoNode(root.left, root.right); return root; } public void connectTwoNode(Node left, Node right) { if(left==null) return; /**** 前序遍历位置 ****/ // 将传入的两个节点连接 left.next = right; connectTwoNode(left.left, left.right); connectTwoNode(left.right, right.left); connectTwoNode(right.left, right.right); } }
-
但是这样做,其实是有重复连接的。每个节点只需要做两次连接即可:
- 将输入节点看作是根节点与null。绿框中的是左节点2的连接,红框是右节点3的连接。
- 在这里
connectTwoNode()
的语义就是,连接left.left指向left.right,和连接left.right指向right.left(如果right不为null)
class Solution { public Node connect(Node root) { if(root==null) return null; connectTwoNode(root, null); return root; } public void connectTwoNode(Node left, Node right) { if(left==null) return; /**** 前序遍历位置 ****/ // 将传入的两个节点连接 left.next = right; connectTwoNode(left.left, left.right); connectTwoNode(left.right, left.next==null?null:left.next.left); } }
将二叉树展开为链表
-
这个函数的定义:
- 给 flatten 函数输入一个节点 root,那么以 root 为根的二叉树就会被拉平为一条链表。
-
流程:
1、将
root
的左子树和右子树拉平。2、将
root
的右子树接到左子树下方,然后将整个左子树作为右子树。class Solution { public void flatten(TreeNode root) { if(root==null) return; flatten(root.left); flatten(root.right); TreeNode left = root.left; TreeNode right = root.right; root.right = left; //一定要将左子树置空 root.left = null; //从根节点开始是为了考虑各种边界情况,因为root已判定不为空 TreeNode p = root; while(p.right!=null){ p = p.right; } p.right = right; } }
最大二叉树
-
对于构造二叉树的问题,根节点要做的就是把想办法把自己构造出来。
-
遍历数组把找到最大值
maxVal
,把根节点root
做出来,然后对maxVal
左边的数组和右边的数组进行递归调用,作为root
的左右子树。class Solution { public TreeNode constructMaximumBinaryTree(int[] nums) { return findMax(nums, 0, nums.length); } public TreeNode findMax(int[] nums, int lo, int hi) { if(lo>=hi) return null; int maxIndex=lo; for(int i=lo+1; i<hi; i++){ if(nums[maxIndex]<nums[i]) maxIndex = i; } TreeNode root = new TreeNode(nums[maxIndex]); root.left = findMax(nums, lo, maxIndex); root.right = findMax(nums, maxIndex+1, hi); return root; } }
从前序与中序遍历序列构造二叉树
-
从前序遍历和中序遍历中找出根节点:就是前序遍历的第一个节点。
-
只要按中序遍历序列中根节点的位置,就可以区分左右子树的节点个数,从而直接划分索引:
public TreeNode buildTree(int[] preorder, int[] inorder) { return build(preorder, 0, preorder.length, inorder, 0, inorder.length); } public TreeNode build(int[] preorder, int plo, int phi, int[] inorder, int ilo, int ihi) { if(plo==phi) return null; //找出根节点的中序遍历中的位置 int index = findRoot(preorder[plo], inorder, ilo, ihi); TreeNode root = new TreeNode(preorder[plo]); //左子树的长度 int div = index - ilo; root.left = build(preorder, plo+1, plo+div+1, inorder, ilo, index); root.right = build(preorder, plo+div+1, phi, inorder, index+1, ihi); return root; } public int findRoot(int root, int[] order, int lo, int hi) { for(int i=lo; i<hi; i++) { if(root==order[i]) return i; } return lo; } }
-
优化:使用HashMap,以节点的值为索引,键为值。这样可以直接获得索引位置,只需要初始化的时候遍历一次。
从中序与后序遍历序列构造二叉树
-
与中序和前序遍历完全类似:
class Solution { public TreeNode buildTree(int[] inorder, int[] postorder) { Map<Integer, Integer> map = new HashMap<>(); for(int i=0; i<inorder.length; i++) { map.put(inorder[i], i); } return build(inorder, 0, inorder.length, postorder, 0, postorder.length, map); } public TreeNode build(int[] inorder, int ilo, int ihi, int[] postorder, int plo, int phi, Map<Integer, Integer> map) { if(ilo==ihi) return null; int index = map.get(postorder[phi-1]); TreeNode root = new TreeNode(postorder[phi-1]); int div = index - ilo; root.left = build(inorder, ilo, index, postorder, plo, plo+div, map); root.right = build(inorder, index+1, ihi, postorder, plo+div, phi-1, map); return root; } }
寻找重复的子树
-
如果你想知道以自己为根的子树是不是重复的,是否应该被加入结果列表中,你需要知道什么信息?
- 以我为根的这棵二叉树(子树)长啥样?
- 以其他节点为根的子树都长啥样?
-
如何才能知道以自己为根的二叉树长啥样?
- 其实看到这个问题,就可以判断本题要使用「后序遍历」框架来解决:
void traverse(TreeNode root) { traverse(root.left); traverse(root.right); /* 解法代码的位置 */ }
- 我要知道以自己为根的子树长啥样,是得先知道我的左右子树长啥样,再加上自己,就构成了整棵子树的样子。
-
怎么描述一棵二叉树的模样呢?
- 二叉树的前序后序遍历结果(包括null的,比如用
#
指代null)可以描述二叉树的结构。 - 扩展二叉树与其前序或后序序列是一一对应的
- 二叉树的前序后序遍历结果(包括null的,比如用
-
怎么知道别人长啥样?
- 借助一个外部数据结构,让每个节点把自己子树的序列化结果存进去。可以使用
HashMap
记录子树出现的次数。
- 借助一个外部数据结构,让每个节点把自己子树的序列化结果存进去。可以使用
-
这里主要需要知道
map.getOrDefault()
方法:当Map集合中有这个key时,就使用这个key值,如果没有就使用默认值defaultValue.default V getOrDefault(Object key, V defaultValue) { V v; return (((v = get(key)) != null) || containsKey(key)) ? v : defaultValue; }
class Solution {
LinkedList<TreeNode> ans = new LinkedList<>();
HashMap<String, Integer> map = new HashMap<>();
public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
traverse(root);
return ans;
}
String traverse(TreeNode root) {
if(root==null) return "#";
String left = traverse(root.left);
String right = traverse(root.right);
String subTree = new StringBuilder(left).append(",").append(right).append(",").append(root.val).toString();
int freq = map.getOrDefault(subTree, 0);
if(freq==1) {
ans.add(root);
}
map.put(subTree, freq+1);
return subTree;
}
}
2、序列化
二叉树的序列化与反序列化
-
一棵二叉树能够被重建,如果满足下面三个条件之一:
a1. 已知先序遍历;或
a2. 已知后序遍历;或
a3. 已知层序遍历; -
且满足下面三个条件之一:
b1. 前面已知的那种遍历包含了空指针;或
b2. 已知中序遍历,且树中不含重复元素;或
b3. 树是二叉搜索树,且不含重复元素。 -
前序遍历的序列化:
- 递归函数的作用是,把自己加入到序列化字符串中。
- 本质是,如果遇到null就返回。
-
前序遍历反序列化:
- 递归函数的作用是,把自己从序列化数组中取出。
- 序列化数组中的第一个元素是根节点,本质是遇到null会返回到右子树。
public class Codec {
String spl = ",";
String nll = "#";
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
ser(root, sb);
return sb.toString();
}
public void ser(TreeNode root, StringBuilder sb) {
//递归基
if(root==null) {
sb.append(nll).append(spl);
return;
}
sb.append(root.val).append(spl);
ser(root.left, sb);
ser(root.right, sb);
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
LinkedList<String> nodes = new LinkedList<>();
for(String s : data.split(spl)) {
nodes.add(s);
}
return deser(nodes);
}
public TreeNode deser(LinkedList<String> nodes) {
//终止条件
if(nodes.isEmpty()) return null;
String first = nodes.removeFirst();
//递归基
if(first.equals(nll)) return null;
TreeNode root = new TreeNode(Integer.parseInt(first));
root.left = deser(nodes);
//理解执行到这一步时,nodes数组中左子树的所有元素都已经被取出了,如果是后序遍历就要先right后left了
root.right = deser(nodes);
return root;
}
}
- 体会一下,在二叉树递归中返回值为节点类型,但是需要递归的修改传入参数的信息。
3、二叉搜索树
-
BST 的特性:
1、对于 BST 的每一个节点
node
,左子树节点的值都比node
的值要小,右子树节点的值都比node
的值大。2、对于 BST 的每一个节点
node
,它的左侧子树和右侧子树都是 BST。3、BST 的中序遍历结果是有序的。
二叉搜索树中第K小的元素
-
中序遍历序列就是一个升序排列,所以只需要对其进行中序遍历,遍历到第k个元素时终止,并记录元素的值即可。
class Solution { public int kthSmallest(TreeNode root, int k) { tranvrse(root, k); return ans; } int ans; int rank; public void tranvrse(TreeNode root, int k) { if(root==null) return; tranvrse(root.left, k); //中序遍历位置,记录遍历到第几个了,从1开始 rank++; if(k==rank) { ans = root.val; return; } tranvrse(root.right, k); } }
-
优化:
- 如果实现一个在二叉搜索树中通过排名计算对应元素的方法select(int k),每次都中序遍历太麻烦了。
- 需要在节点中维护一个信息,就是该节点的值在这颗BST中排第几。
- 这个信息具体可以是,以自己为根的这棵二叉树有多少个节点。有了
size
字段,外加 BST 节点左小右大的性质,对于每个节点node
就可以通过node.left.size
推导出node
的排名。
把二叉搜索树转换为累加树
-
每个节点都去计算右子树的和,是不行的。对于一个节点来说,确实右子树都是比它大的元素,但问题是它的父节点也可能是比它大的元素。
-
利用 BST 的中序遍历特性,维护一个遍历到目前位置的累加变量:
class Solution { public TreeNode convertBST(TreeNode root) { tranverse(root); return root; } int sum=0; public void tranverse(TreeNode root) { if(root==null) return; tranverse(root.right); sum += root.val; root.val = sum; tranverse(root.left); } }
验证二叉搜索树
-
仅仅只验证这个节点的值是否在其左右节点的值之间是不够的,因为应该是在左子树最右节点和右子树最左节点之间,否则可能出现:
-
可以在参数中传入对该子树的大小限制,包括最大值和最小值。注意这里包括等于:
class Solution { public boolean isValidBST(TreeNode root) { return valid(root, null, null); } public boolean valid(TreeNode root, Integer min, Integer max) { if(root==null) return true; if(min!=null && min>=root.val) return false; if(max!=null && max<=root.val) return false; return valid(root.left, min, root.val) && valid(root.right, root.val, max); } }
二叉搜索树中的搜索
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
if(root==null) return null;
if(root.val == val) return root;
if(root.val>val) {
return searchBST(root.left, val);
} else {
return searchBST(root.right, val);
}
}
}
二叉搜索树中的插入操作
-
插入其实和搜索很像。虽然插入一个数据后的二叉搜索树可以有多种形态,但是把插入节点放在叶节点是最简单的,而且也不会破环二叉搜索树。
-
具体来说,就是搜索这个值,在为null的节点处插入。相当于由于没有这个元素查找失败,但是这个元素在这个位置是合法的。
class Solution { public TreeNode insertIntoBST(TreeNode root, int val) { return insert(root, val); } public TreeNode insert(TreeNode root, int val) { if(root==null) return new TreeNode(val); if(root.val > val) { root.left = insert(root.left, val); } else { root.right = insert(root.right, val); } return root; } }
删除二叉搜索树中的节点
-
进行删除的第一步是查找。如果找到了就删除。重点在于删除操作。
-
删除可以按照找到的节点类型分为几种情况:
-
恰好是末端节点,两个子节点都为空,那么可以直接删除了。
-
只有一个非空子节点,那么它要让这个孩子接替自己的位置。
-
有两个子节点,麻烦了,为了不破坏 BST 的性质,必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。
-
-
需要注意的是:
- 一般不会通过
root.val = minNode.val
修改节点内部的值来交换节点,而是通过一系列略微复杂的链表操作交换root
和minNode
两个节点。 - 对于情况三,交换节点的值后,还需要递归的删除左子树中最大的那个节点,或者右子树中最小的那个节点。
class Solution { public TreeNode deleteNode(TreeNode root, int key) { if(root==null) return null; if(root.val==key) { if(root.left==null && root.right==null) return null; if(root.left==null) return root.right; if(root.right==null) return root.left; TreeNode max = findMax(root.left); root.val = max.val; root.left = deleteNode(root.left, max.val); } if(root.val > key) { root.left = deleteNode(root.left, key); } else { root.right = deleteNode(root.right, key); } return root; } public TreeNode findMax(TreeNode root) { if(root==null) return null; if(root.right==null) { return root; } else { return findMax(root.right); } } }
- 一般不会通过
iwehdio的博客园:https://www.cnblogs.com/iwehdio/