二叉树总结篇
二叉树就不多介绍了,但完全二叉树,二叉搜索树,平衡二叉树,B树,B+树等概念还是应该明确。
二叉树的遍历方式可大致分为递归法和迭代法。又可以根据遍历节点的顺序分成前、中、后序遍历。
二叉树的种类
完全二叉树
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1)个节点。
如果最底层节点也全都填满了,那么这是一颗 满二叉树。
二叉搜索树
二叉搜索树是一个有序树。
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树。
用中序遍历可以将二叉搜索树转换成有序数组。
平衡二叉搜索树
二叉搜索树的平均查找时间复杂度为O(logn),但是二叉搜索树是有可能退化为链表的。如何保证它不退化呢?
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二叉树的遍历方式
二叉树主要有两种遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历:一层一层的去遍历。
- 层次遍历(迭代法)
这里仅以前序遍历作一个例子,中序遍历和后序遍历只是换了顺序。
递归法
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
traversal(root, res);
return res;
}
public void traversal(TreeNode root, List<Integer> res) {
if(root == null) return;
res.add(root.val); // 中
traversal(root.left, res); // 左
traversal(root.right, res); // 右
}
}
迭代法,用栈来模拟递归,添加元素是右左中,这样出栈的顺序就是 中 左 右。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
Stack<TreeNode> s = new Stack<>();
if(root != null) s.push(root);
while(!s.isEmpty()){
TreeNode node = s.peek();
if(node != null){
s.pop();
if(node.right != null) s.push(node.right); //右
if(node.left != null) s.push(node.left); //左
s.push(node); //中
s.push(null);
}else{
s.pop();
res.add(s.pop().val);
}
}
return res;
}
}
层序遍历,用队列来遍历每一层的元素。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new LinkedList<>();
if(root == null) return res;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
List<Integer> list = new LinkedList<>();
int len = q.size();
while(len-- > 0){
TreeNode node = q.poll();
list.add(node.val);
if(node.left != null) q.offer(node.left);
if(node.right != null) q.offer(node.right);
}
res.add(list);
}
return res;
}
}
二叉树的性质
求深度是个看起来很简单的问题,这里要注意最小深度指的是叶子节点到根节点的路径。
这类问题都可以先求左边深度,再求右边深度,最后比较得到结果然后return给上层的节点。
实在理不清递归的思路,可以用层序遍历法,很好用!
104. 二叉树的最大深度
class solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
}
111. 二叉树的最小深度
class Solution {
/**
* 递归法,相比求MaxDepth要复杂点
* 因为最小深度是从根节点到最近**叶子节点**的最短路径上的节点数量
*/
public int minDepth(TreeNode root) {
if (root == null) return 0;
int leftDepth = minDepth(root.left);
int rightDepth = minDepth(root.right);
if (root.left == null) return rightDepth + 1;
if (root.right == null) return leftDepth + 1;
// 左右结点都不为null
return Math.min(leftDepth, rightDepth) + 1;
}
}
101. 对称二叉树
这道题的思路比较简单,但我一开始没想到。可以用两个指针q,p来遍历左子树和右子树。如果是完全对称的二叉树,其左子树应该和右子树相同。
class Solution {
public boolean isSymmetric(TreeNode root) {
return check(root, root);
}
public boolean check(TreeNode q, TreeNode p){
if(q == null && p == null) return true;
if(q == null || p == null) return false;
return q.val == p.val && check(q.left, p.right) && check(q.right, p.left);
}
}
226. 翻转二叉树
class Solution {
/**
* 前后序遍历都可以,层序也好用
*/
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
invertTree(root.left);
invertTree(root.right);
swapChildren(root);
return root;
}
private void swapChildren(TreeNode root) {
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
}
}
116. 填充每个节点的下一个右侧节点指针
class Solution {
public Node connect(Node root) {
if(root == null) return root;
Node cur = root;
Queue<Node> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
int len = q.size();
Stack<Node> s= new Stack<>();
while(len-- > 0){
Node node = q.poll();
if(node.left != null) q.offer(node.left);
if(node.right != null) q.offer(node.right);
if(!s.isEmpty()){
Node n = s.peek();
n.next = node;
}
s.push(node);
}
}
return root;
}
}
222. 完全二叉树的节点个数
递归法(后序遍历)
class Solution {
public int countNodes(TreeNode root) {
if(root == null) return 0;
int left = countNodes(root.left);
int right = countNodes(root.right);
return left + right + 1;
}
}
110. 平衡二叉树
class Solution {
public boolean isBalanced(TreeNode root) {
int res = getHeight(root);
return res == -1 ? false : true;
}
public int getHeight(TreeNode root){
if(root == null) return 0;
int leftDepth = getHeight(root.left); // 左
if(leftDepth == -1) return -1;
int rightDepth = getHeight(root.right); // 右
if(rightDepth == -1) return -1;
int res = 1;
res = Math.abs(leftDepth - rightDepth) > 1 ? -1 : Math.max(leftDepth, rightDepth) + 1; // 中
return res;
}
}
257. 二叉树的所有路径
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> res = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
if(root == null) return res;
getPath(root, path, res);
return res;
}
public void getPath(TreeNode root, LinkedList<Integer> path, List<String> res){
path.offer(root.val);
if(root.left == null && root.right == null){
StringBuilder sb = new StringBuilder();
for(Integer i : path){
sb.append(i.toString());
sb.append("->");
}
sb.delete(sb.length()-2, sb.length());
res.add(sb.toString());
return;
}
if(root.left != null){
getPath(root.left, path, res);
path.pollLast();
}
if(root.right != null){
getPath(root.right, path, res);
path.pollLast();
}
}
}
404. 左叶子之和
左叶子就是叶子结点,如果不是叶子节点则不算。使用递归法,后序遍历,在返回值上累加即可。
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
if(root == null) return 0;
int leftSum = sumOfLeftLeaves(root.left);
if(root.left != null && root.left.left == null && root.left.right == null){
leftSum += root.left.val;
}
int rightSum = sumOfLeftLeaves(root.right);
return leftSum + rightSum;
}
}
112. 路径总和
class Solution {
public Boolean res = false;
public boolean hasPathSum(TreeNode root, int targetSum) {
findPath(root, 0, targetSum);
return res;
}
public void findPath(TreeNode root, int sum, int target){
if(root == null) return;
sum += root.val; //中
if(root.left == null && root.right == null && sum == target){
res = true;
}
findPath(root.left, sum, target); //左
findPath(root.right, sum, target); //右
}
}
113. 路径总和 II
一道涉及回溯的问题,这里使用了前序遍历,因为要计算每个路径的和,每到一个节点都会加入了path列表中,然后遍历左节点和右节点;遍历完成后要删除path中的值。
class Solution {
public List<List<Integer>> res = new LinkedList<>();
public List<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
findPath(root, 0, targetSum);
return res;
}
public void findPath(TreeNode root, int sum, int target){
if(root == null) return;
path.add(root.val); //中
sum += root.val;
if(root.left == null && root.right == null && sum == target){
res.add(new ArrayList(path));
}
findPath(root.left, sum, target); // 左
findPath(root.right, sum, target); // 右
path.remove(path.size()-1); //回溯path, sum不需要回溯因为它是临时变量
}
}
二叉树的构造
从中序遍历和后序遍历能还原唯一的二叉树,从中序遍历和前序遍历能还原唯一的二叉树。
主要思路就是后序遍历的最后一个值就是根节点,我们只需要从后序遍历入手,再找到这个值在中序遍历的索引(需要一个哈希表),就能把中序遍历分成3份,分变为[左子树,根节点,右子树],再递归创建右子树和左子树,就能创建出整个树。
值得注意的是,一定要先创建右子树,因为只能从右子树中得到根节点,再得到所有右子树的根节点后,左子树的索引才能确定。
从前序遍历构造也是同样的道理。
106. 从中序与后序遍历序列构造二叉树
class Solution {
public HashMap<Integer, Integer> map = new HashMap<>();
public int postIndex;
public int[] inorder;
public int[] postorder;
public TreeNode buildTree(int[] inorder, int[] postorder) {
this.inorder = inorder;
this.postorder = postorder;
postIndex = postorder.length - 1;
int i = 0;
for(int n: inorder){
map.put(n, i++);
}
return build(0, inorder.length - 1);
}
public TreeNode build(int inLeft, int inRight){
if(inLeft > inRight) return null;
int val = postorder[postIndex--];
TreeNode root = new TreeNode(val);
int index = map.get(val);
root.right = build(index + 1, inRight);
root.left = build(inLeft, index - 1);
return root;
}
}
105. 从前序与中序遍历序列构造二叉树
class Solution {
public HashMap<Integer, Integer> map = new HashMap<>();
public int[] preorder;
public int[] inorder;
public int preIndex;
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
this.inorder = inorder;
preIndex = 0;
int i = 0;
for(int n : inorder){
map.put(n, i++);
}
return build(0, preorder.length - 1);
}
public TreeNode build(int inLeft, int inRight){
if(inLeft > inRight) return null;
int val = preorder[preIndex++];
TreeNode root = new TreeNode(val);
int index = map.get(val);
root.left = build(inLeft, index - 1);
root.right = build(index + 1, inRight);
return root;
}
}
654. 最大二叉树
递归法求解,前序遍历,构造二叉树一般都是前序遍历,因为要先确定二叉树的根节点,再确定左右节点,时间复杂度O(n^2)
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
if(nums.length == 0) return null;
return build(nums, 0, nums.length - 1);
}
public TreeNode build(int[] nums, int left, int right){
if(left > right) return null;
int index = left;
for(int i = left + 1; i <= right; i++){
if(nums[i] > nums[index]) index = i;
}
TreeNode root = new TreeNode(nums[index]);
root.left = build(nums, left, index - 1);
root.right = build(nums, index + 1, right);
return root;
}
}
617. 合并二叉树
没什么说的,简单的递归,前序遍历
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1 == null && root2 == null) return null;
if(root1 == null && root2 != null) return root2;
if(root1 != null && root2 == null) return root1;
TreeNode root = new TreeNode(root1.val + root2.val);
root.left = mergeTrees(root1.left, root2.left);
root.right = mergeTrees(root1.right, root2.right);
return root;
}
}
二叉搜索树
二叉搜索树的主要特点就是有序,可以看成是一个有序的数组,这样求最值,最小差值都会很容易。
700. 二叉搜索树中的搜索
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
while(root != null){
if(root.val == val) return root;
if(root.val < val){
root = root.right;
}else{
root = root.left;
}
}
return null;
}
}
98. 验证二叉搜索树
中序遍历递归法
class Solution {
public TreeNode pre = null;
public boolean isValidBST(TreeNode root) {
if(root == null) return true;
boolean left = isValidBST(root.left);
if(pre != null && pre.val > root.val) return false;
pre = root;
boolean right = isValidBST(root.right);
return left && right;
}
}
中序遍历迭代法,使用null标记中节点。
class Solution {
public boolean isValidBST(TreeNode root) {
if(root == null) return true;
TreeNode pre = null;
Stack<TreeNode> s = new Stack<>();
s.push(root);
while(!s.isEmpty()){
TreeNode node = s.peek();
if(node != null){
s.pop(); //弹出,避免重复操作
if(node.right != null) s.push(node.right); // right
s.push(node); //中
s.push(null); //null标记中节点
if(node.left != null) s.push(node.left); // left
}else{
s.pop(); //弹出空节点
node = s.pop();
if(pre != null && pre.val >= node.val) return false;
pre = node;
}
}
return true;
}
}
530. 二叉搜索树的最小绝对差
中序遍历迭代法
class Solution {
public int getMinimumDifference(TreeNode root) {
Stack<TreeNode> s = new Stack<>();
TreeNode pre = null;
int diff = Integer.MAX_VALUE;
s.push(root);
while(!s.isEmpty()){
TreeNode node = s.peek();
if(node != null){
s.pop();
if(node.right != null) s.push(node.right);
s.push(node);
s.push(null);
if(node.left != null) s.push(node.left);
}else{
s.pop();
node = s.pop();
if(pre != null) diff = Math.min(diff, Math.abs(node.val - pre.val));
pre = node;
}
}
return diff;
}
}
中序遍历递归法
class Solution {
public int diff = Integer.MAX_VALUE;
public TreeNode pre = null;
public int getMinimumDifference(TreeNode root) {
inorder(root);
return diff;
}
public void inorder(TreeNode root){
if(root == null) return;
inorder(root.left);
if(pre != null) diff = Math.min(diff, Math.abs(pre.val - root.val));
pre = root;
inorder(root.right);
}
}
501. 二叉搜索树中的众数
一般来讲,求众数这种需要计算频率的问题,都会使用哈希表来记录频率,如下面这种算法。但这道题是二叉搜索树,一个有序的数组里,如果会出现众数,那他们一定是连续的,其实不需要一个哈希表,只需要记录前一个节点。
中序遍历,哈希表算法
class Solution {
public LinkedList<Integer> res = new LinkedList<>();
public Map<Integer, Integer> map = new HashMap<>();
public int[] findMode(TreeNode root) {
inorder(root);
return res.stream().mapToInt(Integer::valueOf).toArray();
}
public void inorder(TreeNode root){
if(root == null) return;
inorder(root.left);
map.put(root.val, map.getOrDefault(root.val, 0) + 1);
if(res.size() != 0){
if(map.get(root.val) > map.get(res.get(0))){
LinkedList<Integer> result = new LinkedList<>();
result.add(root.val);
res = result;
}
else if(root.val != res.get(0) && map.get(root.val) == map.get(res.get(0))){
res.add(root.val);
}
}else{
res.add(root.val);
}
inorder(root.right);
}
}
中序遍历,只记录前一个节点
class Solution {
public LinkedList<Integer> res = new LinkedList<>();
public TreeNode pre = null;
public int count = 0;
public int maxCount = 0;;
public int[] findMode(TreeNode root) {
inorder(root);
return res.stream().mapToInt(Integer::valueOf).toArray();
}
public void inorder(TreeNode root){
if(root == null) return;
inorder(root.left);
if(pre == null || root.val != pre.val){
count = 1;
}else{
count++;
}
if(count > maxCount){
res.clear();
res.add(root.val);
maxCount = count;
}else if(count == maxCount){
res.add(root.val);
}
pre = root;
inorder(root.right);
}
}
二叉树的共同祖先
236. 二叉树的最近公共祖先
最开始的思路肯定是要后序遍历,因为这道题要进行从下至上的遍历,只有后序遍历能从下至上查找。
那怎么才算找到了最近公共祖先呢?也就是说有哪些情况呢?
根据以上定义,若 root 是 p,q 的 最近公共祖先 ,则只可能为以下情况之一:
- p 和 q 在 root 的子树中,且分列 root 的 异侧(即分别在左、右子树中)
- p = root,且 q 在 root 的左或右子树中
- q = root,且 p 在 root 的左或右子树中
这道题很难想出递归的终止条件,主要还是因为,一个节点自己也是自己的祖先。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == q || root == p) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left == null && right == null) return null;
if(left == null && right != null) return right;
if(left != null && right == null) return left;
return root;
}
}
235. 二叉搜索树的最近公共祖先
这道题的逻辑大致和[236]相同,区别是这道题是二叉搜索树,如下是二叉树找公共祖先的算法
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || 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 null;
if(left == null) return right;
if(right == null) return left;
return root;
}
}
二叉搜索树是有序的,那么公共祖先已一定在[p, q]区间或者[q, p]区间内,而在这个区间内的数是不是一定是最近的公共祖先呢?
是!所以我们不需要遍历整个树,找到这个节点就立刻返回,算法如下
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q);
if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q);
return root;
}
}
701. 二叉搜索树中的插入操作
挺简单的题,不需要重构二叉搜索树,只要见缝插针就好
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if(root == null) return new TreeNode(val);
TreeNode cur = root;
traversal(cur, val);
return root;
}
public void traversal(TreeNode root, int val){
if(root == null) return;
if(root.val < val) traversal(root.right, val);
if(root.val > val) traversal(root.left, val);
TreeNode node = new TreeNode(val);
if(root.val < val && root.right == null) root.right = node;
if(root.val > val && root.left == null) root.left = node;
return;
}
}
450. 删除二叉搜索树中的节点
在二叉搜索树中删除节点要比添加节点困难一些,主要是对树结构的修改,依然是遍历二叉搜索树(不用遍历全树),找到了要被删除的节点后,分为4种情况
- 找到了 为叶子结点
- 找到了 左子树为空
- 找到了 右子树为空
- 找到了 左右子树都不为空
复杂的是第4种,我们要将这个节点的左子树移植到这个节点的右子树的最左边,使整个树保持二叉搜索树的性质
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null) return root;
if(root.val == key){
// 1. 找到了 为叶子结点
if(root.left == null && root.right == null){
return null;
}
// 2. 找到了 左子树为空
else if(root.left == null){
return root.right;
}
// 3. 找到了 右子树为空
else if(root.right == null){
return root.left;
}
// 4. 找到了 左右子树都不为空
else{
TreeNode right = root.right;
TreeNode left = root.left;
while(right.left != null){
right = right.left;
}
right.left = left;
return root.right;
}
}
if(root.val > key) root.left = deleteNode(root.left, key);
if(root.val < key) root.right = deleteNode(root.right, key);
return root;
}
}