二叉树的经典算法题-算法通关村
1 二叉树里的双指针
- 所谓的双指针就是定义了两个变量,在二又树中有时候也需要至少定义两个变量才能解决问题,这两个指针可能针对一棵树,也可能针对两棵树,姑且也称之为“双指针”吧。这些问题一般是与对称、反转和合并等类型相关。
1.1 判断两棵树是否相同
-
LeetCode100:给你两棵二叉树的根节点 p 和q,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
-
示例1:
输入:p = [1, 2, 3], q = [1, 2, 3]
输出:true -
-
示例2:
输入:p = [1, 2], q = [1, null, 2]
输出:false -
-
这个貌似就是两个二叉树同时进行前序遍历,先判断根节点是否相同,如果相同再分别判断左右子节点是否相同,判断的过程中只要有一个不相同就返回 false,如果全部相同才会返回true。其实就是这么回事。
-
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; } //走到这一步说明节点p和q是完全相同的, // 接下来需要判断其左左和右右是否满足条件 return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); }
1.2 对称二叉树
-
LeetCode101: 给定一个二叉树,检查它是否是镜像对称的。例如下面这个就是对称二叉树:
-
-
但是下面这个[1, 2, 2, null, 3, null, 3] 则不是对称的:
-
-
如果树是镜像的,下面这个图更直观一些:
-
-
因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。这里的关键还是如何比较和如何处理结束条件。单层递归的逻辑就是处理左右节点都不为空,且数值相同的情况。
- 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
- 比较 内侧是否对称,传入左节点的右孩子,右节点的左孩子。
- 如果左右都対称就返回true、有一側不対称就返回false。
接下来就是合并和进一步简化:
-
class SymmetricBinaryTree{ public boolean isSymmetric(TreeNode root){ //逐层比较二叉树的对称性。 // 如果在任何层级发现不对称的情况,函数就会返回 false。 // 如果所有层级的比较都通过,最终 isSymmetric 函数会返回 true, // 表明整棵树是对称的。 if(root == null){ return true; } return check(root.left, root.right); } public boolean check(TreeNode p, TreeNode q){ //两个节点都为空,说明是叶子节点的末端,返回 true if(p == null && q == null){ return true; } //其中一个节点为空而另一个不为空, // 说明这两个节点不是成对出现的,返回 false if(p == null || q == null){ return false; } //两个节点的值不相同, // 说明这两个节点不匹配,返回 false if(p.val != q.val){ return false; } //如果两个节点的值相同, // 接下来需要递归地检查 p 的左子节点与 q 的右子节点是否相匹配, // 以及 p 的右子节点与 q 的左子节点是否相匹配。 return check(p.left, q.right) && check(p.right, q.left); } }
1.3 合并二叉树
-
LeetCode617:给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
-
-
两个二叉树的对应节点可能存在以下三种情况,对于每种情况使用不同的合并方式。
• 如果两个二叉树的对应节点都为空,则合并后的二又树的对应节点也为空;
•如果两个二叉树的对应节点只有一个为空,则合并后的二叉树的对应节点为其中的非空节点;
•如果两个二又树的对应节点都不为空,则合并后的二又树的对应节点的值为两个二又树的对应节点的值之和,此时需要显性合并两个节点。
对一个节点进行合并之后,还要对该节点的左右子树分别进行合并: -
public TreeNode mergeTrees(TreeNode root1, TreeNode root2){ if(root1 == null){ return root2; } if(root2 == null){ return root1; } TreeNode mergeNode = new TreeNode(root1.val + root2.val); mergeNode.left = mergeTrees(root1.left, root2.left); mergeNode.right = mergeTrees(root1.right, root2.right); //合并后的子树的根节点 return mergeNode; }
2 路径专题
2.1 二叉树的所有路径
-
LeetCode257:给你一个二叉树的根节点 root,按 任意顺序,返回所有从根节点到叶子节点的路径。叶子节点是指没有子节点的节点。
-
示例:
输入:root = [1, 2, 3, null, 5]
输出:[“1->2->5”, “1->3”] -
-
我们可以注意到有几个叶子节点,就有几条路径,那如何找叶子节点呢?我们知道深度优先搜索就是从根节点开始一直找到叶子结点,我们这里可以先判断当前节点是不是叶子结点,再决定是不是向下走,如果是叶子结点,我们就增加一条路径,就像下面图中这样:
-
-
这里还有个问题,当得到一个叶子结点容易,那这时候怎么知道它所在的完整路径是什么呢?例如上图中得到D之后,怎么知道其前面的A和B呢?简单,增加一个String类型的变量中,访问每个节点访问的时候先存到String中,到叶子节点的时候再添加到集合里:
-
public List<String> binaryTreePaths(TreeNode root){ List<String> res = new ArrayList<>(); dfs(root, "", res); return res; } void dfs(TreeNode root, String path, List<String> res){ //如果当前节点 root 是叶子节点,即它没有左子节点也没有右子节点, // 将当前路径 path 加上当前节点的值 root.val 并添加到结果列表 res 中。 if(root.left == null && root.right == null){ res.add(path + root.val); return; } dfs(root.left, path+root.val+"->", res); dfs(root.right, path+root.val+"->", res); }
2.2 路径总和
-
上面讨论的找所有路径的方法,那我们是否可以再找一下哪条路径的和为目标值呢?
-
LeetCode112:给你二叉树的根节点 root 和一个表示目标和的整数 targetSum,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum。叶子节点 是指没有子节点的节点。
-
示例:
输入:root = [5, 4, 8, 11, null, 13, 4, 7, 2, null, null, null, 1],targetSum = 22
输出:true -
-
示例2:
输入:root = [1, 2, 3] , targetSum = 5
输出:false -
本题询问是否存在从当前节点 root 到叶子节点的路径,满足其路径和为 sum,假定从根节点到当前节点的值之和为val,我们可以将这个大问题转化为一个小问题:是否存在从当前节点的子节点到叶子的路径,满足其路径和为 sum - val。
不难发现这满足递归的性质,若当前节点就是叶子节点,那么我们直接判断 sum 是否等于 val即可(因为路径和已经确定,就是当前节点的值,我们只需要判断该路径和是否满足条件)。若当前节点不是叶子节点,我们只需要递归地询问它的子节点是否能满足条件即可。 -
public boolean hasPathSum(TreeNode root, int sum){ if(root == null){ return false; } if(root.right == null && root.right == null){ return sum == root.val; } //从目标和 sum 中减去当前节点的值 root.val, // 得到新的 sum。将左子树的调用结果存储在变量 left 中。 boolean left = hasPathSum(root.left, sum-root.val); boolean right = hasPathSum(root.right, sum-root.val); return left || right; }
3 反转的妙用
-
LeetCode226 翻转二叉树,将二叉树整体翻转。
-
-
剑指offer27,根据上图,可以发现想要翻转树,就是把每一个节点的左右孩子交换一下。关键在于遍历顺序,前中后序应该选哪一种遍历顺序。遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果。
这是一道很经典的二叉树问题。显然,我们从根节点开始,递归地对树进行遍历,并从叶子节点先开始翻转。如果当前遍历到的节点 root 的左右两棵子树都已经翻转,那么我们只需要交换两棵子树的位置,即可完成以 root 为根节点的整棵子树的翻转。 -
前序交换
-
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); //翻转操作完成,然后逐层返回,最终返回已经翻转的根节点 root。 return root; }
-
后序交换
-
public TreeNode invertTree2(TreeNode root){ if(root == null){ return null; } TreeNode left = invertTree(root.left); TreeNode right = invertTree(root.right); root.left = right; root.right = left; return root; }
-
这道题目使用前序遍历和后序遍历都可以,主要区别是是前序是先处理当前节点再处理子节点,是自顶向下,后序是先处理子结点最后处理自己,一个是自下而上的。观察下图就明白了:
-
-
层次遍历
-
本题还可以使用层次遍历实现,核心思想是元素出队时,先将其左右两个孩子不是直接入队,而是先反转再放进去,代码如下:
-
public TreeNode invertTree3(TreeNode root){ if(root == null){ return null; } //将二叉树中的节点逐层放入队列中,再迭代处理队列中的元素 Deque<TreeNode> treeQueue = new ArrayDeque<>(); treeQueue.offer(root); while(!treeQueue.isEmpty()){ //每次从队列中拿出一个节点,并交换这个节点的左右子树 TreeNode temp = treeQueue.poll(); TreeNode left = temp.left; temp.left = temp.right; temp.right = left; //如果当前节点的左子树不为空,则放入队列等待后续处理 if(temp.left != null){ treeQueue.add(temp.left); } //如果当前节点的右子树不为空,则放入队列等待后续处理 if(temp.right != null){ treeQueue.add(temp.right); } } return root; }
``