二叉树的简单应用和练习
相关文章
一、简介
本篇章主要介绍二叉树的一些操作和简单应用,用于巩固和提升对二叉树的理解和使用。
二、练习
1.相同的树
-
给你两棵二叉树的根节点 p 和 q ,检验这两棵树是否相同
1 1 / \ same / \ 2 3 2 3 1 1 / not same \ 3 3
-
实现
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null) {
return true;
}
if (p == null || q == null) {
return false;
}
if (p.val != q.val) {
return false;
}
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
2.翻转二叉树
-
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
1 1 2 3 => 3 2 4 5 6 7 7 6 5 4
-
实现
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
dfs(root);
return root;
}
// 先序遍历 对每一个节点做 子节点交换处理
private void dfs(TreeNode root) {
if (root == null) {
return;
}
// swap过程
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
dfs(root.left);
dfs(root.right);
}
3.对称二叉树
-
判断给定的二叉树是否是对称二叉树
1 / \ 2 2 / \ / \ 3 5 5 3 输出: true
-
方法一:先对根节点左孩子做翻转操作,在比较翻转后的左孩子结果和右孩子是否一样。
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
TreeNode left = bfs(root.left);
return isSameTree(left, root.right);
}
// 翻转
private TreeNode bfs(TreeNode root) {
if (root == null) {
return null;
}
Queue<TreeNode> queue = new LinkedList<>();
TreeNode curNode = root;
queue.add(curNode);
TreeNode tmp;
while (!queue.isEmpty()) {
curNode = queue.poll();
tmp = curNode.left;
curNode.left = curNode.right;
curNode.right = tmp;
if (curNode.left != null) {
queue.add(curNode.left);
}
if (curNode.right != null) {
queue.add(curNode.right);
}
}
return root;
}
// p、q是否相同
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null) {
return true;
}
if (p == null || q == null) {
return false;
}
if (p.val != q.val) {
return false;
}
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
- 方法二:递归
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
return judge(root.left, root.right);
}
private boolean judge(TreeNode p, TreeNode q) {
if (p == null && q == null) {
return true;
}
if (p == null || q == null) {
return false;
}
if (p.val != q.val) {
return false;
}
return judge(p.left, q.right) && judge(p.right, q.left);
}
4.平衡二叉树
4.1 判断平衡二叉树
-
给定一个二叉树,判断它是否是高度平衡的二叉树。
-
定义:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
1 / \ 2 3 / \ 4 5 输出:true
-
实现
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
}
return judge(root) > 0;
}
// 如果是平衡二叉树,返回深度,否则返回-1
private int judge(TreeNode root) {
if (root == null) {
return 0;
}
// 左右子树的深度
int left = judge(root.left);
int right = judge(root.right);
if (left == -1 || right == -1 || Math.abs(left - right) > 1) {
return -1;
}
return Math.max(left, right) + 1;
}
5.二叉搜索树
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
5.1 验证二叉搜索树
判断给定树是否是一个有效的二叉搜索树。
3
/ \
2 5
\
4
输出: false
- 方法一:中序遍历,始终有序
// dfs
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode curNode = root;
int preVal = Integer.MIN_VALUE;
boolean startFlag = false;
while (curNode != null || !stack.empty()) {
while (curNode != null) {
stack.push(curNode);
curNode = curNode.left;
}
curNode = stack.pop();
// System.out.println(curNode.val);
if (!startFlag) { // 第一个元素
startFlag = true;
} else { // 第一个元素不同preVal作比较
if (curNode.val <= preVal) { // 非有序
return false;
}
}
preVal = curNode.val;
// System.out.println("preVal: " + preVal);
curNode = curNode.right;
}
return true;
}
- 方法二:递归判断每个子树是否在合理的取值区间
public boolean isValidBST02(TreeNode root) {
if (root == null) {
return true;
}
return isValid(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean isValid(TreeNode root, long min, long max) {
if (root == null) {
return true;
}
if (root.val <= min || root.val >= max) {
return false;
}
// 左子树的值在 [min,root.val)
// 右子树的值在 (root.val, max]
return isValid(root.left, min, root.val) && isValid(root.right, root.val, max);
}
5.2 不同的二叉搜索树
-描述:给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同 二叉搜索树 。
示例:n=3
1 1 2 3 3
\ \ / \ / /
2 3 1 3 2 1
\ / / \
3 2 1 2
-分析
/*
一、以i为根节点
1、i的左面 [1,i] 对应不同的二叉搜索树集合 leftList
2、i的右面 [i+1,n] 对应不同的二叉搜索树集合 rightList
3、以i为根节点的 不同二叉搜索树为 leftList 和 rightList 的笛卡尔积组合 => output
二、所有的不同的二叉搜索树集合
for i in [1,n] union(output)
例如 n=2
以1为根节点
left [1,i-1] => [1,0] => [null]
right [i+1,n] => [2,2] => [Node(2)]
=> 1
/ \
null 2
以2为根节点
left [1,i-1] => [1,1] => [Node(1)]
right [i+1,n] => [3,2] => [null]
=> 2
/ \
1 null
*/
- 实现
public List<TreeNode> generateTrees(int n) {
if (n == 0) {
return new ArrayList<>();
}
return generate(1, n);
}
private List<TreeNode> generate(int left, int right) {
List<TreeNode> output = new ArrayList<>();
if (left > right) {
output.add(null);
return output;
}
if (left == right) {
output.add(new TreeNode(left));
return output;
}
// 以每个i作为根节点
for (int i = left; i <= right; i++) {
List<TreeNode> leftList = generate(left, i - 1);
List<TreeNode> rightList = generate(i + 1, right);
for (TreeNode l : leftList) {
for (TreeNode r : rightList) {
TreeNode root = new TreeNode(i);
root.left = l;
root.right = r;
output.add(root);
}
}
}
return output;
}
5.3 恢复二叉搜索树
-
描述:给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。
/* 1 3 / / 3 => 1 \ \ 2 2 */
-
思路:二叉搜索树是中序遍历有序的,所以
1、中序遍历二叉树,将结果存入List中。
2、对List做排序。
3、中序遍历重新给二叉树填值。
说明:空间复杂度O(n),且做了两次完整的遍历。 -
实现
public void recoverTree(TreeNode root) {
if (root == null) {
return;
}
List<Integer> list = new ArrayList<>();
dfs(root, list);
Collections.sort(list);
recover(root, list);
}
// dfs
private void dfs(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
dfs(root.left, list);
list.add(root.val);
dfs(root.right, list);
}
// dfs
private void recover(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode curNode = root;
int idx = 0;
while (curNode != null || !stack.empty()) {
while (curNode != null) {
stack.push(curNode);
curNode = curNode.left;
}
curNode = stack.pop();
curNode.val = list.get(idx++);
curNode = curNode.right;
}
}
- 改进:其实大可不必做两次完全遍历。
1、中序遍历二叉树,将结果存入List中。
2、找到 单调递增数组中被交换的两个数 (场景会比较受限,不能存在重复元素)。
3、中序遍历调整二叉树中对应的两个被交换的节点值。两个节点值都被恢复后结束恢复程序。
4、存在更优化的解法,感兴趣的朋友可以去做进一步了解。 - 实现
public void recoverTree(TreeNode root) {
if (root == null) {
return;
}
List<Integer> list = new ArrayList<>();
dfs(root, list);
// 被交换的两个数
int[] swappedNums = findSwapTwoNum(list);
recover(root, swappedNums[0], swappedNums[1], 2);
}
// dfs
private void dfs(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
dfs(root.left, list);
list.add(root.val);
dfs(root.right, list);
}
/*
[1,3,4,5,6,7] => [1,6,4,5,3,7]
*/
public int[] findSwapTwoNum(List<Integer> list) {
int preNumIdx = -1; // 第一个被交换数的位置
int postNumIdx = -1; // 第二个被交换数的位置
for (int i = 0; i < list.size() - 1; i++) {
if (list.get(i) > list.get(i + 1)) {
postNumIdx = i + 1;
if (preNumIdx == -1) { // 此前第一个被交换的数还未找到
preNumIdx = i;
} else {
break;
}
}
}
return new int[]{list.get(preNumIdx), list.get(postNumIdx)};
}
private void recover(TreeNode root, Integer preNum, Integer postNum, int handleTimes) {
if (root == null) {
return;
}
if (handleTimes == 0) {
return;
}
recover(root.left, preNum, postNum, handleTimes);
// 重新设置两个节点值
if (root.val == preNum) {
root.val = postNum;
handleTimes--;
} else if (root.val == postNum) {
root.val = preNum;
handleTimes--;
}
recover(root.right, preNum, postNum, handleTimes);
}
5.4 将有序数组转换为二叉搜索树
-
描述:给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
/* 3 / \ [0,1,2,3,4,5] => 1 5 / \ / 0 2 4 说明:结果不唯一
*/
-
思路:
1、选取数组中间元素(偶数选偏右)[0,1...n] => left[0,mid-1] [mid] right[mid+1,right] => Node(nums[mid]) / \ toBST(nums[0...mid-1]) toBST(nums[mid+1...right])
2、中间元素生成根节点,左右子数组又为子问题。递归到子数组为一个元素或者越界。
-
实现
public TreeNode sortedArrayToBST(int[] nums) {
if (nums.length == 0) {
return null;
}
return toBST(nums, 0, nums.length - 1);
}
private TreeNode toBST(int[] nums, int left, int right) {
if (left > right) {
return null;
}
if (left == right) {
return new TreeNode(nums[left]);
}
int mid = (left + right + 1) / 2;
TreeNode root = new TreeNode(nums[mid]);
root.left = toBST(nums, left, mid - 1);
root.right = toBST(nums, mid + 1, right);
return root;
}
5.5 将有序链表转换为二叉搜索树
- 说明:思路同数组,待更新链表后更新此篇。
6.构造二叉树
6.1 从前序与中序遍历序列构造二叉树
-
描述:给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
-
preorder 和 inorder 均 无重复 元素
/* 1 / \ 3 5 / \ 7 9 输入: preorder = [1,3,5,7,9], inorder = [3,1,7,5,9] */
-
思路
1、取preorder首个元素num为根,在对应的inorder中寻找num的位置。以上述输入为例子
2、[3]&&[3] 是左子树;[5,7,9]&&[7,5,9]是右子树。
3、原址计算,需要标记preorder的左右边界和inorder的左右边界;递归。 -
实现
public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = preorder.length;
Map<Integer, Integer> map = arrayToMap(inorder);
return build(preorder, inorder, map, 0, n - 1, 0, n - 1);
}
/**
* 根据先序遍历和中序遍历构建二叉树
*
* @param preorder 先序遍历
* @param inorder 中序遍历
* @param map 中序遍历map
* @param pl preorder left
* @param pr preorder right
* @param il inorder left
* @param ir inorder right
* @return
*/
private TreeNode build(int[] preorder, int[] inorder, Map<Integer, Integer> map, int pl, int pr, int il, int ir) {
if (pl > pr) {
return null;
}
if (pl == pr) {
return new TreeNode(preorder[pl]);
}
int num = preorder[pl];
TreeNode root = new TreeNode(num);
int idx = map.get(num);
// 1.中序遍历num左侧元素个数
// 2.int range = idx - il;
// 3.即从 pl + 1 到新的 npr 之间有 idx - il 个元素
// 4.所以 npr - (pl + 1) + 1 = range = idx - il
// 5.得出 npr = idx + pl -il
System.out.println(String.format("[num=%s]", num));
System.out.println(String.format("[left][%s-%s][%s-%s]", pl + 1, idx + pl - il, il, idx - 1));
System.out.println(String.format("[right][%s-%s][%s-%s]", idx + pl - il + 1, pr, idx + 1, ir));
root.left = build(preorder, inorder, map, pl + 1, idx + pl - il, il, idx - 1);
root.right = build(preorder, inorder, map, idx + pl - il + 1, pr, idx + 1, ir);
return root;
}
private Map<Integer, Integer> arrayToMap(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], i);
}
return map;
}
7.二叉树路径
7.1 二叉树的所有路径
-
描述:给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
/* 1 / \ 2 3 \ 5 输入:root = [1,2,3,null,5] 输出:["1->2->5","1->3"] */
-
方法一:广度优先搜索(一)
思路:
1、层级遍历,记录每一个节点的父节点,parentMap
2、当左右孩子都是null的时候,到达路径终点。
3、从叶子节点反向追踪到根路径,得到路径。
实现:
// 记录每节点的父节点
private Map<TreeNode, TreeNode> parentMap = new HashMap<>();
public List<String> binaryTreePaths(TreeNode root) {
List<String> pathList = new ArrayList<>();
if (root == null) {
return pathList;
}
genTreePaths(root, pathList);
return pathList;
}
private void genTreePaths(TreeNode root, List<String> pathList) {
Queue<TreeNode> queue = new LinkedList<>();
TreeNode curNode = root;
queue.add(curNode);
while (!queue.isEmpty()) {
curNode = queue.poll();
if (curNode.left == null && curNode.right == null) {
pathList.add(buildPath(curNode, parentMap));
} else {
if (curNode.left != null) {
queue.add(curNode.left);
parentMap.put(curNode.left, curNode);
}
if (curNode.right != null) {
queue.add(curNode.right);
parentMap.put(curNode.right, curNode);
}
}
}
}
// build path
private String buildPath(TreeNode node, Map<TreeNode, TreeNode> parentMap) {
List<Integer> path = new ArrayList<>();
while (node != null) {
path.add(node.val);
node = parentMap.get(node);
}
Collections.reverse(path);
return path.stream().map(String::valueOf).collect(Collectors.joining("->"));
}
-
方法二:广度优先搜索(二)
思路:
1、层级遍历,因为每个节点都有与之一一对应的路径,设一个和节点树结构一样的路径"树"(虚拟的树 queue)/* 1 1 / \ / \ 2 3 => 1->2 1->3 \ \ 5 1->2->5 (nodeQueue) (pathQueue) */
2、每次取节点时,同步更新路径到pathQueue,左右孩子都为null到达路径终点。
3、取叶子节点对应的路径。
实现:
public List<String> binaryTreePaths(TreeNode root) {
List<String> pathList = new ArrayList<>();
if (root == null) {
return pathList;
}
bfs(root, pathList);
return pathList;
}
// bfs
private void bfs(TreeNode root, List<String> pathList) {
Queue<TreeNode> nodeQueue = new LinkedList<>();
Queue<String> pathQueue = new LinkedList<>();
TreeNode curNode = root;
String curPath = String.valueOf(curNode.val);
nodeQueue.add(curNode);
pathQueue.add(curPath);
while (!nodeQueue.isEmpty()) {
curNode = nodeQueue.poll();
curPath = pathQueue.poll();
if (curNode.left == null && curNode.right == null) {
pathList.add(curPath);
} else {
if (curNode.left != null) {
nodeQueue.add(curNode.left);
pathQueue.add(new StringBuffer(curPath).append("->").append(curNode.left.val).toString());
}
if (curNode.right != null) {
nodeQueue.add(curNode.right);
pathQueue.add(new StringBuffer(curPath).append("->").append(curNode.right.val).toString());
}
}
}
}
- 方法三:深度优先搜索
思路同方法二
实现:
public List<String> binaryTreePaths(TreeNode root) {
List<String> pathList = new ArrayList<>();
if (root == null) {
return pathList;
}
// bfs(root, pathList);
dfs(root, new StringBuffer(), pathList);
return pathList;
}
// dfs
private void dfs(TreeNode root, StringBuffer pathBuffer, List<String> pathList) {
pathBuffer.append(root.val);
if (root.left == null && root.right == null) {
pathList.add(pathBuffer.toString());
} else { // 左右孩子存在不为空的情况
pathBuffer.append("->");
if (root.left != null) {
dfs(root.left, new StringBuffer(pathBuffer), pathList);
}
if (root.right != null) {
dfs(root.right, new StringBuffer(pathBuffer), pathList);
}
}
}
7.2 路径总和
- 描述:给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
- 分析:在已经掌握求解二叉树所有路径的情况下,想要解出此题已是手到擒来,但本题 仅仅要求判断路径是否存在,如此便不需要先遍历出所有的路径,而是采取深度搜索的算法。
- 实现:
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}
return search(root, targetSum);
}
private boolean search(TreeNode root, int targetSum) {
if (root.left == null && root.right == null) {
return root.val == targetSum;
}
targetSum -= root.val;
boolean left = false, right = false;
if (root.left != null) {
left = search(root.left, targetSum);
}
if (root.right != null) {
right = search(root.right, targetSum);
}
return left || right;
}
思考:也可以使用回溯法或者bfs,间隔一段时间,自己每次写出来的也不一样。
7.3 路径总和II
- 给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
- 本次只展示dfs的写法。
- 方法一
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) {
return res;
}
List<Integer> path = new ArrayList<>();
dfs(root, targetSum, path, res);
return res;
}
private void dfs(TreeNode root, int targetSum, List<Integer> path, List<List<Integer>> res) {
path.add(root.val);
targetSum -= root.val;
if (root.left == null && root.right == null) {
if (targetSum == 0) {
res.add(path);
}
} else {
if (root.left != null) {
dfs(root.left, targetSum, new ArrayList<>(path), res);
}
if (root.right != null) {
dfs(root.right, targetSum, new ArrayList<>(path), res);
}
}
}
- 方法一改进
1、方法一在递归子问题的时候,每次都new新的List,空间开销较大。
2、改用回溯的方法,使用同一个List实例。
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) {
return res;
}
List<Integer> path = new ArrayList<>();
dfs2(root, targetSum, path, res);
return res;
}
private void dfs2(TreeNode root, int targetSum, List<Integer> path, List<List<Integer>> res) {
// add
path.add(root.val);
targetSum -= root.val;
if (root.left == null && root.right == null) {
if (targetSum == 0) {
res.add(new ArrayList<>(path));
}
}
if (root.left != null) {
dfs2(root.left, targetSum, path, res);
}
if (root.right != null) {
dfs2(root.right, targetSum, path, res);
}
// remove
path.remove(path.size() - 1);
}
7.4 路径总和III
- 描述:给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
-
思路:双层递归
1、外层递归循环遍历到每个节点。以每个节点作为根节点,执行步骤2.
2、从根节点出发,搜索路径和为targetSum的路径。 -
实现
// 路径计数
private int count = 0;
public int pathSum(TreeNode root, int targetSum) {
if (root == null) {
return 0;
}
dfs(root, targetSum);
return count;
}
// 1、尝试以每一个节点为根
private void dfs(TreeNode root, int targetSum) {
if (root == null) {
return;
}
// 内层递归
// bfs(root, targetSum);
dfsSearch(root, targetSum);
dfs(root.left, targetSum);
dfs(root.right, targetSum);
}
// 2、从根节点搜索targetSum
private void dfsSearch(TreeNode root, int targetSum) {
if (root == null) {
return;
}
targetSum -= root.val;
if (targetSum == 0) {
count++;
}
dfsSearch(root.left, targetSum);
dfsSearch(root.right, targetSum);
}
// 方法二
private void bfs(TreeNode root, int targetSum) {
if (root == null) {
return;
}
// 节点队列
Queue<TreeNode> nodeQueue = new LinkedList<>();
TreeNode curNode = root;
// System.out.println(String.format("[root:%s]", root.val));
nodeQueue.add(curNode);
// 路径和队列
Queue<Integer> sumQueue = new LinkedList<>();
Integer sum = 0;
sumQueue.add(sum);
while (!nodeQueue.isEmpty()) {
curNode = nodeQueue.poll();
sum = sumQueue.poll() + curNode.val;
// System.out.println(String.format("[curNode:%s][sum:%s]", curNode.val, sum));
if (sum == targetSum) { // 寻找目标5. 3->2 还可能有 3->2->-4->4
count++;
}
if (curNode.left != null) {
nodeQueue.add(curNode.left);
sumQueue.add(sum);
}
if (curNode.right != null) {
nodeQueue.add(curNode.right);
sumQueue.add(sum);
}
}
}
- 提示:存在更优的前缀和解法,请感兴趣的读者自行思考实现。
7.5 二叉树中的最大路径和
-
描述:路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
输出:42 -
思路
1、遍历每个节点作为根节点。
2、计算根节点和左右孩子能够组合成的最大值。注意:该组合一定包含根,不一定包含左右子树。
3、取根节点和 左右孩子中”贡献值“大的一方作为一条子路径返回。比如 上图的 15 20(root) 7 组合,返回 15+20 -
实现
// 最大值
private int max = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
if (root == null) {
return 0;
}
dfsForMaxSum(root);
return max;
}
// 包含根节点的root的最大 左根右 路径和
private int dfsForMaxSum(TreeNode root) {
if (root == null) {
return 0;
}
int sum = root.val;
int left = Math.max(dfsForMaxSum(root.left), 0);
int right = Math.max(dfsForMaxSum(root.right), 0);
// 更新最大值 // 包含根节点的root的最大 左根右 路径和
max = Math.max(max, root.val + left + right);
// 处理返回值 选取左右子节点贡献大的一方
sum += Math.max(left, right);
return sum;
}
三、心得体会
- 在熟练掌握二叉树的遍历的基础上,解法自然会多种多样。
- 本篇写的潦草,没有详细的解题步骤,主要是为了提示自己,重新拾起基础;总结自己的解题思路,题目永远是无限的,方法形成的过程更重要。