翻转二叉树
链接: 226.翻转二叉树
想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。
关键在于遍历顺序,前中后序应该选哪一种遍历顺序?
遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。
注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果
这道题目使用前序遍历和后序遍历都可以,维度中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转两次
递归法:
递归三部曲:
- 确定递归函数的参数和返回值
参数就是要传入节点的指针,不需要其他参数了
【通常此时定下来主要参数,如果再写递归的逻辑中发现还需要其他参数的时候,随时补充】
返回值的话其实也不需要,但题目中给出得了要返回root节点的指针,可以直接使用题目定义好的函数,所以函数的返回类型为TreeNode
public TreeNode invertTree(TreeNode root)
- 确定终止条件
当前节点为空的时候,就返回
if (root == NULL) return root;
- 确定单层递归的逻辑
因为是先前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树
swap(root.left, root.right);
invertTree(root.left);
invertTree(root.right);
代码
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
swap(root);
invertTree(root.left);
invertTree(root.right);
return root;
}
void swap(TreeNode root){
TreeNode tempNode = root.left;
root.left = root.right;
root.right = tempNode;
}
}
迭代法
深度优先遍历
普通迭代法
二叉树:听说递归能做的,栈也能做! (opens new window)中给出了前中后序迭代方式的写法,所以本题可以很轻松的写出如下迭代法的代码:
//利用了前序遍历的普通迭代法 入栈顺序:中-右-左
//想想入栈出栈顺序就能理解
class Solution {
public TreeNode invertTree(TreeNode root) {
Stack<TreeNode> st = new Stack<>();
if (root == null) return root;
st.push(root);
while (!st.isEmpty()){
TreeNode node = st.peek();
st.pop();
swap(node);
if (node.right != null){
st.push(node.right);
}
if (node.left != null){
st.push(node.left);
}
}
return root;
}
void swap(TreeNode root){
TreeNode tempNode = root.left;
root.left = root.right;
root.right = tempNode;
}
}
统一迭代法
我们在二叉树:前中后序迭代方式的统一写法 (opens new window)中介绍了统一的写法,所以,本题也只需将文中的代码少做修改便可。
class Solution {
public TreeNode invertTree(TreeNode root) {
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
swap(node);
}
}
return root;
}
void swap(TreeNode root){
TreeNode tempNode = root.left;
root.left = root.right;
root.right = tempNode;
}
}
广度优先遍历
也就是层序遍历,层数遍历也是可以翻转这棵树的,因为层序遍历也可以把每个节点的左右孩子都翻转一遍,代码如下:
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return root;
Queue<TreeNode> que = new LinkedList<TreeNode>();
que.offer(root);
while (!que.isEmpty()){
int len = que.size();
while (len > 0){
TreeNode tempNode = que.poll();
swap(tempNode);
//你以后还是别复制了,复制完总是忘了改东西
if (tempNode.left != null) que.add(tempNode.left);
if (tempNode.right != null) que.add(tempNode.right);
len--;
}
}
return root;
}
void swap(TreeNode root){
TreeNode tempNode = root.left;
root.left = root.right;
root.right = tempNode;
}
}
总结
针对翻转二叉树,本笔记给出了一种递归,三种迭代(两种模拟深度优先遍历,一种层序遍历)的写法,都是之前我们讲过的写法,融汇贯通一下而已。
对称二叉树
链接: 101. 对称二叉树
首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!
对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了其实我们要比较的是两个树(这两个树是根节点的左右子树),所以在递归遍历的过程中,也是要同时遍历两棵树。
那么如何比较呢?
比较的是两个子树的里侧和外侧的元素是否相等。如图所示:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nx2x18Dx-1680261093414)(7.对称二叉树.assets/20210203144624414.png)]
本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
但都可以理解算是后序遍历,尽管已经不是严格上在一个树上进行遍历的后序遍历了。
其实后序也可以理解为是一种回溯
递归法
思路
递归三部曲
- 确定递归函数的参数和返回值
因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。
返回值自然是bool类型。
bool compare(TreeNode left,TreeNode right)
- 确定终止条件
要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。
节点为空的情况有:(注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点)
- 左节点为空,右节点不为空,不对称,return false
- 左不为空,右为空,不对称 return false
- 左右都为空,对称,返回true
此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空:
- 左右都不为空,比较节点数值,不相同就return false
此时左右节点不为空,且数值也不相同的情况我们也处理了。
代码如下:
if (left == NULL && right != NULL) return false;
else if (left != NULL && right == NULL) return false;
else if (left == NULL && right == NULL) return true;
else if (left->val != right->val) return false; // 注意这里我没有使用else
注意上面最后一种情况,我没有使用else,而是else if, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。
- 确定单层递归的逻辑
此时才进入单层递归的逻辑,单层递归的逻辑就是处理 左右节点都不为空,且数值相同的情况。
-
比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
-
比较内测是否对称,传入左节点的右孩子,右节点的左孩子。
-
如果左右都对称就返回true ,有一侧不对称就返回false 。
-
bool outside = compare(left.left, right.right); // 左子树:左、 右子树:右 bool inside = compare(left.right, right.left); // 左子树:右、 右子树:左 bool isSame = outside && inside; // 左子树:中、 右子树:中(逻辑处理) return isSame;
如上代码中,我们可以看出使用的遍历方式,左子树左右中,右子树右左中,所以我把这个遍历顺序也称之为“后序遍历”(尽管不是严格的后序遍历)。
代码
public boolean isSymmetric1(TreeNode root) {
return compare(root.left, root.right);
}
private boolean compare(TreeNode left, TreeNode right) {
if (left == null && right != null) {
return false;
}
if (left != null && right == null) {
return false;
}
if (left == null && right == null) {
return true;
}
if (left.val != right.val) {
return false;
}
// 比较外侧
boolean compareOutside = compare(left.left, right.right);
// 比较内侧
boolean compareInside = compare(left.right, right.left);
return compareOutside && compareInside;
}
二叉树的最大深度
链接: 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;
}
}
完全二叉树的节点个数
链接: 222.完全二叉树的节点个数
思路
本题只考虑递归解法中针对完全二叉树的解法
完全二叉树都可以拆解为满二叉树的形式
而满二叉树中节点的个数可以用公式求出
关键:一个完全二叉树满足什么条件才是满二叉树(慢慢思考,而不是全靠回忆)
左子树的深度left.left(一直向左遍历) = 右子树的深度right.right(一直向右遍历)
代码
class Solution {
/**
* 针对完全二叉树的解法
*
* 满二叉树的结点数为:2^depth - 1
*/
public int countNodes(TreeNode root) {
if (root == null) return 0;
TreeNode left = root.left;
TreeNode right = root.right;
int leftDepth = 0, rightDepth = 0; // 这里初始为0是有目的的,为了下面求指数方便
while (left != null) { // 求左子树深度
left = left.left;
leftDepth++;
}
while (right != null) { // 求右子树深度
right = right.right;
rightDepth++;
}
if (leftDepth == rightDepth) {
return (2 << leftDepth) - 1; // 注意(2<<1) 相当于2^2,所以leftDepth初始为0
}
return countNodes(root.left) + countNodes(root.right) + 1;
}
}
平衡二叉树
链接: 110.平衡二叉树
思路
递归三步曲分析:
- 明确递归函数的参数和返回值
参数:当前传入节点。 返回值:以当前传入节点为根节点的树的高度。
那么如何标记左右子树是否差值大于1呢?
如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了。
所以如果已经不是二叉平衡树了,可以返回-1 来标记已经不符合平衡树的规则了。
代码如下:
// -1 表示已经不是平衡二叉树了,否则返回值是以该节点为根节点树的高度
int getHeight(TreeNode node)
- 明确终止条件
递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的树高度为0
代码如下:
if (node == null) {
return 0;
}
- 明确单层递归的逻辑
分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则返回-1,表示已经不是二叉平衡树了。
代码如下:
int leftHeight = getHeight(node.left); // 左
if (leftHeight == -1) return -1;
int rightHeight = getHeight(node.right); // 右
if (rightHeight == -1) return -1;
int result;
if (abs(leftHeight - rightHeight) > 1) { // 中
result = -1;
} else {
result = 1 + max(leftHeight, rightHeight); // 以当前节点为根节点的树的最大高度
}
return result;
代码
class Solution {
/**
* 递归法
*/
public boolean isBalanced(TreeNode root) {
return getHeight(root) != -1;
}
private int getHeight(TreeNode root) {
if (root == null) {
return 0;
}
int leftHeight = getHeight(root.left);
if (leftHeight == -1) {
return -1;
}
int rightHeight = getHeight(root.right);
if (rightHeight == -1) {
return -1;
}
// 左右子树高度差大于1,return -1表示已经不是平衡树了
if (Math.abs(leftHeight - rightHeight) > 1) {
return -1;
}
return Math.max(leftHeight, rightHeight) + 1;
}
}
二叉树的所有路径
链接: 257. 二叉树的所有路径
思路
本道题目的思路很简单,仍需一些注意点
- 回溯思想的应用
- 终止条件的注意
- List数据结构和StringBuilder数据结构的一些方法
代码
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> result = new ArrayList<>();
if (root == null) return result;
List<Integer> path = new ArrayList<>();
traversal(root,path,result);
return result;
}
public void traversal(TreeNode node,List<Integer> path,List<String> result){
path.add(node.val);
if (node.left == null && node.right == null){
// 终止逻辑
// StringBuilder对象需要被创建
// StringBuilder sPath;
StringBuilder sPath = new StringBuilder();
for (int i = 0;i < path.size() - 1;i++){
// sPath += path[i];
sPath.append(path.get(i));
// sPath += "->";
sPath.append("->");
}
// sPath += path[path.size() - 1];
sPath.append(path.get(path.size() - 1));
String ssPath = sPath.toString();
result.add(ssPath);
return;
}
// 递归本就是不断循环,不需要while了
if (node.left != null){
traversal(node.left,path,result);
// path.remove(path[path.size() - 1]);
// 移除的是该序号代表的元素
path.remove(path.size() - 1);
}
if (node.right != null){
traversal(node.right,path,result);
// path.remove(path[path.size() - 1]);
path.remove(path.size() - 1);
}
}
}
注意点
List
List.remove(序号):移除某某序号的元素
List.add(元素):将元素添加进list中
List.get(序号):得到某个序号的元素
【特别注意,List不是数组,不可以List[i]获取元素】
StringBuilder
该对象需要被创建:StringBuilder sb = new StringBuilder();
StringBuilder.append(元素)
【特别注意是append方法,不可以使用+=】
左叶子之和
链接: 404.左叶子之和
思路
本题的思路不难,重点在于对于左叶子的理解,以及在代码中如何表现左叶子
本题重点是对左叶子的定义:节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点
代码
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
if (root == null) return 0;
int leftLeaves = sumOfLeftLeaves(root.left);
if (root.left != null && root.left.left ==null && root.left.right == null){
leftLeaves = root.left.val;
}
int rightLeaves = sumOfLeftLeaves(root.right);
return leftLeaves + rightLeaves;
}
}
找树左下角的值
链接: 513.找树左下角的值
思路
利用的是递归方法,本题找左下角的值,需要考虑深度,同时也要回溯,思路并不难
代码
递归法
有点绕
class Solution {
int maxDepth = -1;
int result;
void traversal(TreeNode root,int depth){
if (root.left == null && root.right == null){
// 叶节点处理逻辑
if (depth > maxDepth){
maxDepth = depth;
result = root.val;
}
return;
}
if (root.left != null){
depth++;
traversal(root.left,depth);
depth--;
}
if (root.right != null){
depth++;
traversal(root.right,depth);
depth--;
}
return;
}
public int findBottomLeftValue(TreeNode root) {
traversal(root,0);
return result;
}
}
迭代法
class Solution {
public int findBottomLeftValue(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int res = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode poll = queue.poll();
if (i == 0) {
res = poll.val;
}
if (poll.left != null) {
queue.offer(poll.left);
}
if (poll.right != null) {
queue.offer(poll.right);
}
}
}
return res;
}
}
路径总和
链接: 112. 路径总和
【参考卡哥内容】
可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树
- 确定递归函数的参数和返回类型
参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。
再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:
如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先 (opens new window)中介绍)
如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)
而本题我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回
本题中,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示。
代码如下:
bool traversal(treenode cur, int count) // 注意函数的返回类型
- 确定终止条件
首先计数器如何统计这一条路径的和呢?
不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。
如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。
如果遍历到了叶子节点,count不为0,就是没找到。
递归终止条件代码如下:
if (!cur.left && !cur.right && count == 0) return true; // 遇到叶子节点,并且计数为0
if (!cur.left && !cur.right) return false; // 遇到叶子节点而没有找到合适的边,直接返回
- 确定单层递归的逻辑
因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。
递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。
代码如下:
if (root.left != null) {
boolean left = haspathsum(root.left, targetsum);
if (left) { // 已经找到
return true;
}
}
if (root.right != null) {
boolean right = haspathsum(root.right, targetsum);
if (right) { // 已经找到
return true;
}
}
代码
class solution {
public boolean haspathsum(treenode root, int targetsum) {
if (root == null) {
return false;
}
targetsum -= root.val;
// 叶子结点
if (root.left == null && root.right == null) {
return targetsum == 0;
}
if (root.left != null) {
boolean left = haspathsum(root.left, targetsum);
if (left) { // 已经找到
return true;
}
}
if (root.right != null) {
boolean right = haspathsum(root.right, targetsum);
if (right) { // 已经找到
return true;
}
}
return false;
}
}