前言
最近把树的算法做了一个小小滴总结。这些题目来自leetcode,都是一些代表二叉树算法思想的经典题目。
比如高度平衡二叉树
,二叉搜索树BST
,tire树
等数据结构,深度广度优先遍历
,递归
,迭代
等算法思想。如果对于递归
不熟悉可以看看我的算法第一篇《雾都孤儿》图文并茂,手刃算法,也可以评论里留言,下一秒玲珑就来回复各位兄弟姐妹的问题😉。
我也是一个算法渣渣,刚开始看着别人的讲解都没法理解递归是什么意思,就像之前不明白之前倒车入库方向盘左打车为什么车往左边走一样(捂脸)。总之自己就是算法白痴😂。但是我想坚持下去一定会有结果的,只是一个时间的问题。
下面的题目其实自己我做了两到三遍才做到自己独立解决😅,分享出来一方面是想给在算法练习的掘友们一些参考,一方面是也是对自己二叉树算法的一个归纳总结吧(给自己的五一礼物)。
代码基本上都是javaScript
实现,因为重要的是思想并且代码不算复杂,由于在最后一题里面用到了原型链的知识,因此最后一题我也用java
写了一遍方便非前端的掘友参考。当然思路都是一样的。
树的基本概念
如果你对树的结构烂熟于心那么就直接看看习题,开启你的五一算法之旅吧~
概念介绍
- 满二叉树:满二叉树就是每一个结点都有左子树和右子树,并且所有叶子结点都在同一层。
- 完全二叉树:是满二叉树的子集。
- 平衡二叉树:结点的左右子树高度差不超过1,后面的题目有出现
- 二叉搜索树:二叉搜索树的中序遍历有序,左孩子都小于根结点,右孩子都大于根节点
- 前缀树:前缀树也叫字典树,来一个前缀树的图吧,来自百度百科
每一个结点都可能有26个不同的子节点,结点值是a-z
26个字母。
另外还有一些基本概念可以作为了解:
- 结点的度:结点的度就是该结点射出的分支数,是横向的。射出的分支数是0那么结点的度就是0。
- 树的度:结点度的最大值就是树的度。
- 树的深度:结点层次最大值就是树的深度
- 结点的层次:根节点到这个结点的分支数条数,只有一个结点的数中,该结点的层次是0
这四个概念是相对的,如果分两类就是从横向和纵向进行区分。我简记为:“横度纵生
”。这四个字的意思是结点的度那么就是代表横向;树的深(生)度就是代表纵向。
2.二叉树的性质
虽然下面的算法题很少用到,但是我觉得这作为基本素养是需要知道滴。
- 性质1:非空二叉树的第i层上最多有2^i(第一层我们的i=0)。
- 性质2:满二叉树的结点个数是2^(k+1)-1。k是结点层次存在第0层。该结论可以根据等比数列计算。
- 非空二叉树的叶节点树n0,度为2结点树为n2,那么存在关系n0=n2+1.根据分支射入和分支射出和总结点的关系可以判断出来。
3.二叉树的遍历
二叉树由三部分组成,根节点,左子树,右子树。根据根节点的访问次序可以分为三种。即前序遍历,中序遍历,后续遍历。除了按结点类型遍历,还有同一层先按左子树再右子树遍历的层序遍历。
- 前序遍历 前序遍历先遍历根节点,再左子树,最后右子树。
- 中序遍历 中序遍历先遍历左子树,再根结点,再右子树。
- 后续遍历 后续遍历先遍历左子树,在右子树,最后根结点。
- ps:前序遍历和后续遍历不能确定一棵树。比如前序AB,后续BA,不知道树是ABnull还是AnulB
- 层序遍历:层序遍历利用队列,先进先出思想。一层层遍历。根节点入队列,取出队首元素,如果有左节点和右结点就让左右节点入队列,再取出队首元素...直到队列为空。这样有点抽象,后面我们结合题目来看比如19题。
非递归实现遍历
借助堆栈实现,堆栈有先进后出的特点。非递归实现二叉树的前序遍历先让根节点入栈,取出根节点元素,如果左子树非空就让左子树入栈,然后弹出栈顶元素,看该元素是否有右节点,如果有右节点入栈,再弹出栈顶元素直到栈为空。后面也会有相关的题目例如12题。
leetcode原题
树的基本性质
第一题:树的最大高度-104
- 题目:给一棵树返回树的最大高度,也就是树的深度。如下:
Given binary tree [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
return its depth = 3.
分析: 求树的高度我们可以找出左子树的高度和右子树的高度,两者较大的值+1就是当前树的高度。左子树的高度可以用左左子树的高度和右右子树的高度中的较大值+1求解。我们就找到了递归的规律,什么时候返回呢?也就是什么情况下是出口,当遍历到结点为空的时候就递归到了最后一层。所以root==null就是出口了。
解题
var maxDepth = function(root) {
if(root==null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right) )+1;
};
第二题-平衡树110
- 题目:给你一棵树判断是否是平衡二叉树
Given a binary tree, determine if it is height-balanced.
Given the following tree [3,9,20,null,null,15,7]:
3
/ \
9 20
/ \
15 7
平衡二叉树就是结点的左右子树的高度差<=1
- 分析:
与前面的类似,递归找到左右子树的高度,然后左右子树高度差如果大于1就返回false,否则为true。所以我们需要知道
当前结点左右子树的高度差<=1
&&当前结点的左孩子的左右子树高度差<=1
&&当前结点的右孩子的左右子树高度差<=1
。当着三个条件都满足的时候时候就是true. - 解题:depth函数求左右子树的高度。
var isBalanced = function(root) {
if(root==null) return true;
var cur= Math.abs(depth(root.left)-depth(root.right))>1? false : true;
return cur&&isBalanced(root.left) && isBalanced(root.right);
}
function depth(root) {
if(root ==null) return 0;
var l = depth(root.left);
var r = depth(root.right);
return Math.max(l,r)+1
}
第三题-翻转树226
- 题目:
翻转一棵二叉树。
示例:
输入:
4
/ \
2 7
/ \ / \
1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 1
- 分析:也可以用递归实现,交换左子树翻转的结果和右子树翻转的结果。左子树翻转的结果是左左子树和右右子翻转的结果进行交换得到...直到最后一个结点的左右子树为空返回。
- 解题:
var invertTree = function(root) {
if(!root) return null;
//左子树是左子树反转后的结果
root.left = invertTree(root.left);
//右子树是右子树翻转的结果
root.right = invertTree(root.right);
var temp = root.left;
root.left = root.right;
root.right = temp;
return root;
};
第四题-归并两棵树617
- 题目:
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
输入:
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
输出:
合并后的树:
3
/ \
4 5
/ \ \
5 4 7
- 解析:二叉树合并可以看做是一个树的结点值加上另外一棵树的结点值。使用递归。当前结点值相加+左子树合并的结果+右子树合并的结果就是最后要返回的树。左子树合并的结果=左孩子当前结点值+左左子树合并的结果+右右子树合并的结果。直到两棵树上的结点都遍历完就返回。
- 解题:
var mergeTrees = function(t1, t2) {
//如果左右子树都空
if(!t1&&!t2) return null;
//如果左右子树有一个空
if(!t1||!t2) return t1||t2;
t1.val = t1.val+t2.val;
t1.left = mergeTrees(t1.left, t2.left);
t1.right = mergeTrees(t1.right, t2.right);
return t1;
};
二叉树路径问题
第五题-树的最长路径543
- 题目:
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
示例 :
给定二叉树
1
/ \
2 3
/ \
4 5
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
注意:两结点之间的路径长度是以它们之间边的数目表示。
- 分析:这道题我们要知道树的直径就是一棵树一个结点到另外一个结点的最大值。题目的例子中很容易让我们误解为最大长度就是左子树的高度+右子树的高度。但是不完全是这样的,我们遍历每个结点的时候要记录当前直径的最大值,然后判断下一个结点的最大值和当前最大值的大小,如果下一个结点的直径最大值大于当前最大值,那么就要更新最大值。
比如下面的例子中:值为-5的结点的直径是7,根节点-9的直径是6。所以直径最大值不一定是左子树深度和右子树深度和
- 解题
var diameterOfBinaryTree = function(root) {
if(!root) return 0;
var max = 0;
function maxdepth(root){
if(!root) return 0;
var left = maxdepth(root.left);
var right = maxdepth(root.right);
//更新直径的最大值
max = Math.max((left+ right),max);
return Math.max(left, right)+1
}
maxdepth(root);
return max;
}
时间复杂度为O(n):遍历结点的数量
空间复杂度O(height):树的高度
第六题-判断路径和是否等于一个数112
- 题目:
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。
- 解析:遍历树结点。当遍历完且遍历的所有结点和等于sum就返回true
- 解题:
<!--//错误的方法,不满足全局性:左子树如果不满足遍历完返回false不会遍历右子树-->
<!--var hasPathSum = function(root, sum) {-->
<!-- if(!root)return false;-->
<!-- if(root.val == sum&& root.left==null&&root.right==null) return true;-->
<!-- // return hasPathSum(root.left, sum-root.val)||hasPathSum(root.right,sum-root.val);-->
<!-- if(!roo.left)hasPathSum(root.left, sum-root.val);-->
<!-- if(!root.right)hasPathSum(root.right,sum-root.val);-->
<!--};-->
//正确的解答
var hasPathSum = function(root, sum) {
if(!root) return false;
if(root.val == sum&& root.left==null&&root.right==null) return true;
return hasPathSum(root.left, sum-root.val)||hasPathSum(root.right,sum-root.val);
};
第七题-统计一棵树满足要求的路径数量437
- 题目:
给定一个二叉树,它的每个结点都存放着一个整数值。
找出路径和等于给定数值的路径总数。
路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。
示例:
root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8
10
/ \
5 -3
/ \ \
3 2 11
/ \ \
3 -2 1
返回 3。和等于 8 的路径有:
1. 5 -> 3
2. 5 -> 2 -> 1
3. -3 -> 11
解析:核心也是树的深度优先遍历,但是这个遍历不一样,一条路径就可以从任意结点开始,所以说每一个结点都是根结点。拿上面的例子来看我们把10当做根结点遍历没有找到一条满足要求的,然后就把5当做根结点遍历找到了两条满足要求的路径,然后把3当做根结点来看有没有满足要求的....
那么如何看是否满足要求呢,就是当前的结点值value是否和sum相等,如果相等就找到了一组,然后继续遍历找到以根结点开始所有满足题目要求的路径。这也是一个递归,递归的等式是满足要求的路径数count=根结点root是否满足
value==sum
&&root.left是否满足value == sum-root.value&&root.right 是否满足value==sum-root.left。对应于path函数。解题:
//双重递归
var pathSum = function(root, sum) {
if(!root) return 0;
function path(root, sum) {
var count = 0;
if(!root) return 0;
if(root && root.val == sum) count++;
count += path(root.left, sum-root.val)+path(root.right, sum-root.val);
return count;
}
return path(root, sum)+pathSum(root.left, sum)+pathSum(root.right, sum);
};
第八题-最小路径111
- 题目
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最小深度 2.
- 分析:这个题目可能第一感觉和104求最大深度一样,如果是这样那么[1,2]就不满足了,所以当左右子树其中有一个为零的时候要取非0值再加1.
- 代码实现
var minDepth = function(root) {
if(!root) return 0;
let l = minDepth(root.left);
let r= minDepth(root.right);
//取左或右子树有一个为0的时候取非0的值
if(l==0&&r!=0||r==0&&l!=0) return (l||r)+1;
return Math.min(l, r)+1;
};
- 时间复杂度:我们访问每个节点一次,时间复杂度为 O(N)O(N) ,其中 NN 是节点个数。
- 空间复杂度:最坏情况下,整棵树是非平衡的,例如每个节点都只有一个孩子,递归会调用 N(树的高度)次,因此栈的空间开销是 O(N) 。但在最好情况下,树是完全平衡的,高度只有 log(N),因此在这种情况下空间复杂度只有O(log(N)) 。
方法二
采用层次遍历加两层循环,不用遍历完每一个结点。当当前结点是叶子结点的时候就返回当前结点的层数。
var minDepth = function(root) {
if(!root) return 0;
if(!root.left&&!root.right) return 1;
let quene = [];//队列
let level = 0;
quene.push(root);
while(quene.length){
let size = quene.length
while(size--){
let cur = quene.shift();
if(cur.left!=null){
quene.push(cur.left);
}
if(cur.right!=null) {
quene.push(cur.right);
}
//找到叶子结点
if(!cur.left&&!cur.right){
return level+1;
}
}
level++;
}
};
第九题-124统计最大路径和(hard)
- 题目
给定一个非空二叉树,返回其最大路径和。
本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
示例 1:
输入: [1,2,3]
1
/ \
2 3
输出: 6
示例 2:
输入: [-10,9,20,null,null,15,7]
-10
/ \
9 20
/ \
15 7
输出: 42
- 解析:这个题目要弄清楚路径和是什么意思,不是路径的条数,而是经过结点的value相加的最大者,弄懂题目的意思后和543,687一样的道理。可能更严谨的是路径和必须不小于0,树最大路径和可能会出现负值的情况(当结点的value是负)
- 解题
var maxPathSum = function(root) {
let max = Number.MIN_SAFE_INTEGER;;
const path = (root) => {
if(root == null) return 0;
let left = Math.max(0,path(root.left));
let right =Math.max(0,path(root.right));
max = Math.max(max, left+right+root.val);
// return left+right+root.val;不满足测试二
return Math.max(left,right)+root.val;
}
path(root);
return max;
};
第十题-相同结点值的最大路径长度687
给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。 这条路径可以经过也可以不经过根节点。
注意:两个节点之间的路径长度由它们之间的边数表示。
示例 1:
输入:
5
/ \
4 5
/ \ \
1 1 5
输出:
2
示例 2:
输入:
1
/ \
4 5
/ \ \
4 4 5
输出:
2
注意: 给定的二叉树不超过10000个结点。 树的高度不超过1000。
- 解析:这个题目最开始想的是两层递归遍历,把每一个结点当做根结点。然后记录当前的最大值然后比较更新最大值。这个题有点像最大路径数,也就是leetcode543.与之不同的是多了连个参数leftpath和rightpath来记录相同值的结点,返回值也不一样,但是大致思路一样。
- 解题
var longestUnivaluePath = function(root) {
// //当前树的路径数
if(!root) return 0;
let max = 0;
const depth = (root)=>{
if(!root) return 0;
let left = depth(root.left);
let right = depth(root.right);
let leftpath = (root.left&&root.left.val==root.val)? left+1:0;
let rightpath = (root.right&&root.right.val==root.val)? right+1:0;
max = Math.max(max, leftpath+rightpath);
return Math.max(leftpath,rightpath)
}
depth(root);
return max;
};
《----------------------------------------------------------------》
与之类似的还有129求路径的结点和,中等难度
var sumNumbers = function(root) {
let sum = 0;
let cur = 0;
if(!root) return 0;
const path = (root,cur)=>{
cur = cur*10 + root.val;
//叶子结点返回当前
//根节点的值加左子树数字和加右子树数字和
if(root.left) path(root.left, cur);
if(root.right) path(root.right, cur);
//叶子结点的时候和为cur
if(!root.left&&!root.right) sum += cur;
}
path(root,0);
return sum;
};
第十一题-子树572
- 题目:这个题与第七题-437解法类似,也是双重递归,因此将它放在这。
给定两个非空二叉树 s 和 t,检验 s 中是否包含和 t 具有相同结构和节点值的子树。s 的一个子树包括 s 的一个节点和这个节点的所有子孙。s 也可以看做它自身的一棵子树。
示例 1:
给定的树 s:
3
/ \
4 5
/ \
1 2
给定的树 t:
4
/ \
1 2
返回 true,因为 t 与 s 的一个子树拥有相同的结构和节点值。
示例 2:
给定的树 s:
3
/ \
4 5
/ \
1 2
/
0
给定的树 t:
4
/ \
1 2
返回 false。
- 解析:和上面一题很像,双重递归。第一层递归是遍历每个结点,把s每个结点当做根结点,然后第二层递归判断两棵树是否相同。相同是树是根结点到叶子结点都相同。比如题目中第二个例子返回false因为叶子结点不同。
- 代码
var isSubtree = function(s, t) {
if(!s) return false;
//判断两棵树是否相同
let judege = function(s, t) {
if(!s&&!t) return true;
if(!s||!t) return false;
if(s.val != t.val){
return false;
}
return judege(s.left,t.left)&&judege(s.right, t.right);
}
//三者只要一个成立即可
return judege(s,t)|| isSubtree(s.left, t)|| isSubtree(s.right, t)
};
二叉树遍历
第十二题-非递归实现二叉树前序遍历144
- 题目
给定一个二叉树,返回它的 前序 遍历。
示例:
输入: [1,null,2,3]
1
\
2
/
3
输出: [1,2,3]
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
- 解析:前序遍历,遍历根结点,再遍历左子树,再遍历右子树。这里写一下迭代算法,也就是利用栈非递归实现二叉树的遍历
- 解答
var preorderTraversal = function(root) {
let result = [];
if(!root) return result;
//递归
// const helper = (root) => {
// if(root) result.push(root.val);
// if(root.left) helper(root.left);
// if(root.right) helper(root.right);
// }
// helper(root);
// return result;
//迭代
let stack = [root];
while(stack.length!=0){
let node = stack.pop();
result.push(node.val);
if(node.right) stack.push(node.right);
if(node.left) stack.push(node.left);
}
return result;
};
第十三题-非递归实现二叉树的后续遍历145(hard)
- 题目
给定一个二叉树,返回它的 后序 遍历
示例:
输入: [1,null,2,3]
1
\
2
/
3
输出: [3,2,1]
- 解析:与上面的方法类似,可以递归先序遍历然后逆序输出。也可以迭代每次更新栈顶元素得到先序遍历结果再逆序输出。值得注意的是后序遍历
左孩子,右孩子,根结点,
。那么先序的顺序是根节点,右孩子,左孩子
而不是根节点,左孩子,右孩子
- 代码
/**
* @param {TreeNode} root
* @return {number[]}
*/
var postorderTraversal = function(root) {
//先序遍历逆序后就是后序遍历
let result = [];
if(!root) return result;
// const helper = (root) => {
// if(root) result.push(root.val);
// if(root.right) helper(root.right);
// if(root.left) helper(root.left);
// }
// helper(root);
// return result.reverse();
let stack = [root];
while(stack.length){
let len = stack.length;
while(len--){
let node = stack.pop();
result.push(node.val)
if(node.left) stack.push(node.left);
if(node.right) stack.push(node.right);
}
}
return result.reverse();
};
第十四题-非递归实现二叉树的中序遍历94(中等)
- 题目
给定一个二叉树,返回它的中序 遍历。
示例:
输入: [1,null,2,3]
1
\
2
/
3
输出: [1,3,2]
- 思想:虽然这个题目是中等难度,但是我认为中序遍历是三者难度系数较大的。通过node指针先将根节点的左子树全部入栈,取出栈顶元素并将结点值保存到结果中。如果取出的栈顶元素有右子树就让右子树按序入栈再取出栈顶元素。比较难理解的两个点在代码里面都有注释
- 代码
var inorderTraversal = function(root) {
let result = [];
if(!root) return result;
let stack = [root];
let node = root.left;
//添加node存在是处理根节点没有左子树的情况。没有左子树stack.length=0的时候海曙要将右子树入栈
while(stack.length || node) {
while(node) {
//结点入栈
stack.push(node);
node = node.left
}
node = stack.pop();
result.push(node.val);
node = node.right;//node.right存在就将node.right这个树的左节点入栈
}
return result;
};
第十五题-树的对称101
- 题目
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/ \
2 2
\ \
3 3
进阶:你可以运用递归和迭代两种方法解决这个问题吗?
- 解析:首先我们递归实现,如果树是对称的那么左子树和右子树对称,右子树和左子树对称。
镜像对称=左子树的第一个孩子的value和右子树第一个孩子的value相等+左左子树和右右子树对称+右右子树和左左子树对称
- 代码
//递归
var isSymmetric = function(root) {
if(!root) return true;
let same = function(root1, root2) {
//遍历到最后还没有false并且结点空
if(!root1&&!root2) return true;
//任意一个是空或者两个结点的value不等。
if(!root1||!root2||root1.val != root2.val) return false;
return same(root1.left,root2.right)&&same(root1.right, root2.left);
}
return same(root.left,root.right);
};
当然还有迭代法利用队列,每次取队列队首的两个元素,然后比较值是否相等。如果两节点为空进行下一轮循环,如果一个空或者两者val不等就false,由于队列先进先出入队的四个结点的顺序要注意一下。
//非递归
var isSymmetric = function(root) {
//非递归实现
let quene = [];
if(root.left.val!=root.right.val) return false;
quene.push(root.left);
quene.push(root.right);
while(quene.length){
let left = quene.shift();
let right = quene.shift();
// if(left.left!=right.right||left.right!=right.left) return false;逻辑错误
if(!left&&!right) continue;
if(!left||!right || left.val != right.val) return false;
//入队列
if(left.left) quene.push(left.left);
if(right.right) quene.push(right.right);
if(left.right) quene.push(left.right);
if(right.left) quene.push(right.left);
}
return true;
};
第十六题-统计左叶子结点的和404
- 题目
计算给定二叉树的所有左叶子之和。
示例:
3
/ \
9 20
/ \
15 7
在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24
- 解析:递归实现,如果左子树是叶子结点那就返回左子树的值加对右子树处理的结果。如果左子树不是叶子结点就返回对左子树和右子树处理的结果,如何判断左子树是不是叶子结点的条件有三个,一个是该结点存在,二是该结点无左孩子,三是该结点没有右孩子。
- 代码
var sumOfLeftLeaves = function(root) {
if(root==null) return 0;
if(root.left&&!root.left.left&&!root.left.right) return root.left.val+sumOfLeftLeaves(root.right);
else return sumOfLeftLeaves(root.left)+sumOfLeftLeaves(root.right);
};
第十七题-间隔遍历(打家劫舍2-好题)337
- 题目
聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:
输入: [3,4,5,1,3,null,1]
3
/ \
4 5
/ \ \
1 3 1
输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
解析:这里使用递归的方法遍历树,动态规划放到动态规划的专栏去讲解。这个题可以理解为二叉树间隔结点值的和最大是多少。所以有两种情况,第一种将root作为当前最大值,那么root的left和right不能算进去。总的最大值就是
root
+root.left.left+root.left.right
+root.right.left+root.right.right
的结点和。第二种情况就是root不算进去,将root.left+root.right的结点和作为最大值。 最后的最大值应该是第一种和第二种的最大值。对于递归的出口就是当结点不存在返回0.
解答:
var rob = function(root) {
if(!root) return 0;
let result1 =root.val ;
//第一种
if(root.left)result1 += rob(root.left.left) + rob(root.left.right);
if(root.right) result1 +=rob(root.right.left)+ rob(root.right.right);
//第二种
let result2 = rob(root.left) + rob(root.right);
return Math.max(result1, result2);
};
- 补充:这个题目我开始递归也没有想出来,感觉自己太菜了,休息一会发现自己想到啦,遍历当前结点,找出当前两种情况的最大值,当前情况最大值也是两种情况。找出他们的最大值。最后结果是result1和result2对比的结果。resul2结果
result2 = rob(root.left) + rob(root.right);
rob(root.left)的结果是root.left作为根节点两种情况对比的结果。rob(root.right)是root.right作为根节点两种结果对比后的结果。
第十八题-找出二叉树中第二小的结点671
- 题目
给定一个非空特殊的二叉树,每个节点都是正数,并且每个节点的子节点数量只能为 2 或 0。如果一个节点有两个子节点的话,那么这个节点的值不大于它的子节点的值。
给出这样的一个二叉树,你需要输出所有节点中的第二小的值。如果第二小的值不存在的话,输出 -1 。
示例 1:
输入:
2
/ \
2 5
/ \
5 7
输出: 5
说明: 最小的值是 2 ,第二小的值是 5 。
示例 2:
输入:
2
/ \
2 2
输出: -1
说明: 最小的值是 2, 但是不存在第二小的值。
- 思路:开始以为第二小值一定在根节点的第一个左孩子和右孩子之中,所以有了错误的解法。但是根据报错信息知道第二小值可能出现在子树的任何一个位置(当孩子结点值与根节点相同的时候),因而要进行递归。
- 第二小值不可能是根节点,于是找左右子树的较小者作为第二小值
- 当left.val==root.val的时候,要找到左子树的第二小值作为left的值。同理right.val==root.val的时候要找到右子树第二小值作为right值。
- 然后比较right和left哪个大。
- 错误思路
//错误的思路:考虑不周部分测试用例通过,比如[1,1,3,11,34,3,1,1,1,3,8,4,8,3,3,1,2]这棵树就不成立。
//有0或2个叶子结点。
var findSecondMinimumValue = function(root) {
//没有结点
if(!root) return -1;
//只有一个根节点
if(!root.left&&!root.right) return -1;
//有一个叶子结点
if(!root.left||!root.right) return (root.left.val||root.right.val)> root.val?(root.left.val||root.right.val):-1;
//有两个叶子结点找到两个叶子结点的小者
if(root.left.val ==root.val&&root.right.val == root.val) return -1;
if(root.left.val==root.val) return root.right.val;
if(root.right.val == root.val) return root.left.val;
return Math.min(root.left.val,root.right.val)
};
- 正确解法
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
//有0或2个叶子结点。
var findSecondMinimumValue = function(root) {
//没有结点
if(!root) return -1;
//只有一个根节点
if(!root.left&&!root.right) return -1;
//有两个叶子结点
let left = root.left.val;
let right = root.right.val;
if(left == root.val) left = findSecondMinimumValue(root.left);
if(right == root.val) right = findSecondMinimumValue(root.right);
//左右子树都存在
if(left != -1 && right !=- 1) return Math.min(left, right);
// 只存在左子树
if(left != -1) return left;
//只存在右子树
return right;
};
第十九题-一棵树的每层结点平均值637
- 题目:
给定一个非空二叉树, 返回一个由每层节点平均值组成的数组.
示例 1:
输入:
3
/ \
9 20
/ \
15 7
输出: [3, 14.5, 11]
解释:第0层的平均值是 3, 第1层是 14.5, 第2层是 11. 因此返回 [3, 14.5, 11].
- 解析:层序遍历,利用一个队列。两层循环,第一层循环表示此时的层次,第二次循环是该层的结点树。将第一层的结点入队列,取出计算平均值。然后将第二层的结点入队列,取出计算平均值。直到队列为空。
- 解答
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
层次遍历(同第10题的最小路径111-思路2)
*/
var averageOfLevels = function(root) {
let quene = [root];
let result = [];
while(quene.length){
let size = quene.length;let sum = 0;let node =null;
for(let i = 0; i< size; i++){
node= quene.shift();
sum+=node.val;
if(node.left) quene.push(node.left);
if(node.right) quene.push(node.right);
}
result.push(sum/size);
}
return result;
};
- 解答二:利用DFS遍历实现:先序遍历,每次遍历的时候记录深度,构建一个二维数组arr[i][j],i表示当前深度,j表示当前深度的结点值。最后对二维数组的每一项reduce方法求得总和再利用map方法求出平均值。
var averageOfLevels = function(root) {
let arr = [];
if(!root) return arr;
const Deepfs = (root, level) => {
(arr[level]||(arr[level]=[])).push(root.val);
if(root.left)Deepfs(root.left, level+1);
if(root.right)Deepfs(root.right, level+1);
}
Deepfs(root, 0);
return arr.map(item => { return item.reduce((pre, cur)=> pre+cur)/item.length});
};
第二十题-得到左下角的结点513
- 题目
给定一个二叉树,在树的最后一行找到最左边的值。
示例 1:
输入:
2
/ \
1 3
输出:
1
- 解析:与上面的题目类似。采用深度优先或者广度优先遍历树。广度优先遍历先让右子树入队列,再让左子树入队列。取出队列的最后一个元素就是最左边的子节点。这里主要想说深度优先遍历,一种很巧妙的思维就是即使更新树的深度,用result记录当前最深的第一个结点值就是最左边的结点值,当树遍历完后就返回result
- 代码
var findBottomLeftValue = function(root) {
let maxLevel = -1;
let result = root.val
const DFS = (root, level) => {
if(level>maxLevel) {
maxLevel = level;
result = root.val;
}
if(root.left) DFS(root.left, level+1);
if(root.right) DFS(root.right, level+1);
}
DFS(root, 0);
return result;
};
二叉查找树
第二十一题-修剪二叉查找树669
- 题目
给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。
示例 2:
输入:
3
/ \
0 4
\
2
/
1
L = 1
R = 3
输出:
3
/
2
/
1
- 解析:这个题目首先要知道二叉搜索数是什么。二叉搜索数的根结点大小是中间值,也就是根结点的左边都比根结点小,根结点的右边都比根结点大,修剪的结果=左子树修剪的结果和右子树修剪的结果。右子树修剪的结果就是右右子树修剪结果加右左子树修剪的结果。
- 代码
//L和R就是左右边界
var trimBST = function(root, L, R) {
if(!root) return null;
if(root.val> R){
return trimBST(root.left, L, R);
}else if(root.val< L){
return trimBST(root.right, L, R);
}
root.left = trimBST(root.left, L, R);
root.right = trimBST(root.right, L, R);
return root;
};
第二十二题.寻找二叉搜索树的第k个元素230
- 题目
给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。
说明:
你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。
示例 1:
输入: root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
2
输出: 1
解析:二叉搜索数的特点就是中序遍历有序,因此二叉搜索树中序遍历后,第k-1个结果就是二叉搜索树的第k小值。
代码
var kthSmallest = function(root, k) {
let result=[];
if(!root) return ;
const helper = (root) => {
if(root.left) helper(root.left);
result.push(root.val);
if(root.right) helper(root.right);
}
helper(root);
return result[k-1];
};
第二十三题-把二叉查找树的每个结点的值加上比他大的结点值538
- 题目
给定一个二叉搜索树(Binary Search Tree),把它转换成为累加树(Greater Tree),使得每个节点的值是原来的节点值加上所有大于它的节点值之和
例如:
输入: 原始二叉搜索树:
5
/ \
2 13
输出: 转换为累加树:
18
/ \
20 13
- 解析:首先肯定是要遍历树的,怎么遍历。要知道最大值。我们可以反中序遍历。
右孩子,根结点,左孩子
的顺序进行遍历。遍历的时候记录当前结点值用sum表示。下一个结点root的结点值就是自身的val和sum的和。
比如结点8的val就是8+0,sum = val =8;然后遍历结点7的val就是7+sum=15,sum=val=15.然后结点4也是一样的。
- 代码
if(!root) return root;
let sum = 0;
//遍历函数
const helper = (root)=>{
if(!root) return;
helper(root.right);
root.val+=sum;
sum = root.val;
helper(root.left);
}
helper(root);
return root
};
第二十四题-二叉查找树的最近公共祖先235
- 题目
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
- 思路:可以递归也可以迭代。当pq的值小于根节点的值就在左子树中寻找,如果pq的值大于根节点的值就在右子树中寻找。这里要注意的是p和q是两个结点,不是结点值,比较的时候要用p.val。
- 代码(非递归的迭代方法)
var lowestCommonAncestor = function(root, p, q) {
//与上面逻辑类似,但是是不断迭代更新root
while(root){
if(root.val > q.val && root.val > p.val) root = root.left;
else if(root.val < q.val && root.val < p.val) root = root.right;
else{
return root;
}
}
};
第二十五题-从有序数组中构造二叉查找树108
- 题目
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:
给定有序数组: [-10,-3,0,5,9],
一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:
0
/ \
-3 9
/ /
-10 5
- 解析:这个题目要求构造高度平衡的二叉树,就是有序数组的中间值作为根节点。然后将数组分为两半,分别构造高度平衡的左之二叉树和高度平衡的右子二叉树。数组长度是奇数取中间值,数组长度是偶数取中间两个任意一个。
- 代码
var sortedArrayToBST = function(nums) {
if(nums.length==0) return null;
const helper = (nums) => {
if(nums.length==0) return null;
let len = nums.length;
let i = Math.floor((len)/2);
let root = new TreeNode(nums[i]);
root.left = helper(nums.slice(0, i));
root.right = helper(nums.slice(i + 1));
return root;
}
return helper(nums);
};
第二十六题-根据有序链表构造二叉搜索树109
- 题目
给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:
给定的有序链表: [-10, -3, 0, 5, 9],
一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树:
0
/ \
-3 9
/ /
-10 5
- 思想:根据有序链表构造二叉树没有有序数组灵活,因为链表不可以像数组一样通过下标快速定位。所以我们通过快慢指针,快指针每次前移两个结点,慢指针每次移动一个结点。当快指针是最后一个结点(链表长度奇数)快指针是倒数第二个结点(链表长度是偶数)的时候不再寻找,此时慢指针就是中间位置。对于左边通过同样的方式找到左边链表的中间值,同样的方式找到右边链表的中间值。
- 不知道有没有掘友有和我一样的疑问,
root.right = helper(slow.next, tail);//tail不能写成null
为什么tail不直接写成null。毕竟对于右边的尾结点就是null啊。经过一番分析后,第一次找到中间值右边的链表尾结点确实是tail,但是对于第一次中间值左边的链表尾结点tail就是slow指向的位置啦。因此写null是不对的。 - 代码
/**
* @param {ListNode} head
* @return {TreeNode}
*/
var sortedListToBST = function(head) {
if(!head) return null;
const helper = (head, tail)=> {
if(head == tail) return null;
let fast = head, slow = head;
//对应链表是偶数和奇数两种情况
while(fast!=tail && fast.next != tail){
fast = fast.next.next;
slow = slow.next;
}
let root = new TreeNode(slow.val);
root.left = helper(head, slow);
root.right = helper(slow.next, tail);//tail不能写成null
return root;
}
return helper(head, null);
};
第二十七题-在二叉查找树中寻找两个结点,使他们的和为一个给定的值653
- 题目
给定一个二叉搜索树和一个目标结果,如果 BST 中存在两个元素且它们的和等于给定的目标结果,则返回 true。
案例 1:
输入:
5
/ \
3 6
/ \ \
2 4 7
Target = 9
输出: True
- 解析:两种思路,第一种将平衡树结点值转换成有序数组,然后利用快慢指针。第二种思路是遍历树的每一个结点,用Set记录遍历的每一个结点值。如果k-当前结点值的结果存在与Set结构中。那么就说明存在这样的两个结点。如果结点遍历完都还没有就返回false。
- 代码
var findTarget = function(root, k) {
if(!root) return false;
let s = new Set();
const helper = (root, k, s) => {
if(!root) return false;
if(s.has(k-root.val)) return true;
s.add(root.val);
return helper(root.left, k, s) || helper(root.right, k,s);
}
return helper(root, k, s)
};
第二十八题-在二叉查找树中查两个结点的最小绝对值530
- 题目
给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。
示例:
输入:
1
\
3
/
2
输出:
1
解释:
最小绝对差为 1,其中 2 和 1 的差的绝对值为 1(或者 2 和 3)。
- 解析:绝对值差的最小值一定出现在二叉搜索树中序遍历任意相邻的两个结点中。因此对二叉树中序遍历,并将当前结点值减去上一个结点值作为差的最小值。遍历每一个结点跟新最小值min。
- 代码
var getMinimumDifference = function(root) {
// if(!root) return Number.MAX_VALUE;
let min = Number.MAX_VALUE;
let prenode = null;
const helper = (root) => {
if(root.left) helper(root.left);
if(prenode) min = Math.min(min, root.val - prenode.val);
prenode = root;
if(root.right) helper(root.right);
}
helper(root);
return min;
};
第二十九题-寻找二叉查找树中次数出现最多的值501
- 题目:在一根二叉搜索树中返回出现结点次数最多的结点。假设二叉查找树的定义使左子树小于等于根节点,右子树大于等于根节点。
- 解析:中序遍历的结果是有序的。不可能出现
[1,2,2,3,4,2,2,5]
这样的序列,因此遍历每一个结点。如果当前结点val等于前一个结点的val。那么就将出现次数count++,如果不等就是一个新的值,让count=1。再通过count和max比较大小。max是当前众数结点出现的次数。如果count比max还要大就找到了一个新的结果。如果相等就找到了一样的结果。如果count比max小就继续遍历下一个结点。对下一个结点进行相同的判断直到结点遍历完。 - 代码
var findMode = function(root) {
// 1.中序遍历当前值和上一个值比较是否相同。相同则加1,不相同count就是1表示当前是一个新元素
//没比较一次就要看是否更新最大值
//如果count>max就更新最大值,清空结果数组,添加新的数据到结果数组
//如果count= max就说明当前出新次数和之前出现的次数一样多,直接添加新数据到结果数组
let prenode = null;
let count = 1;
let max = 1; let res = [];
const helper = (root) => {
if(!root) return;
helper(root.left);
if(prenode) {
if(root.val==prenode.val) count++;
else count=1;
}
if(count > max){
max = count;
res = [];
res.push(root.val);
}else if(count == max) res.push(root.val);
prenode = root;
helper(root.right);
}
helper(root);
return res;
};
公共祖先
还记得第二十四题的二叉搜索树的公共祖先吗,上面我们使用迭代的方式实现,这里使用递归实现普通二叉树的公共祖先
第三十题-二叉树的最近公共祖先236
- 题目:给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 leetcode第236题
- 分析:看当前结点值是否等于q和p中任意一个的结点值。如果等于那么公共祖先就是当前结点,如果不等看看左子树和右子树中是否存在p和q,判断逻辑在代码的注释中,另外根据题目我们可以知道不会出现p和q不属于二叉树结点的情况。
- 代码
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
*/
var lowestCommonAncestor = function(root, p, q) {
if(!root || root.val == p.val || root.val == q.val) return root;
let left = lowestCommonAncestor(root.left, p, q);
let right = lowestCommonAncestor(root.right, p, q);
//如果left不存在p或q就返回right的结果。如果left存在,right不存在就返回left结果。如果left和right都存在就返回根节点
if(left == null) return right;
else if(right == null) return left;
return root;
};
前缀树
第三十一题-实现一个tire208(mid)
- 题目
实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。
示例:
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 true
trie.search("app"); // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app");
trie.search("app"); // 返回 true
说明:
你可以假设所有的输入都是由小写字母 a-z 构成的。
保证所有输入均为非空字符串。
- 分析:前缀树又称字典树。文章开头也有说明
❝又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。----《百度百科》
❞
解析:这可能是这些题目里面最复杂的一道题,因为要写三个函数,其实最关键的的insert函数能够写出来那么后面的两个函数也是一样的。我将代码里面写上注释,如果有地方一定要在评论区call我。
代码
js实现
//定义trie树的结构
function Trie(val) {
//每一个结点都最多有26个孩子,用数组存
this.next = new Array(26);
//是否是叶子结点,默认false
this.isEnd = false;
//结点值,是一个字符类型
this.val = val;
};
/**
* Inserts a word into the trie.
* @param {string} word
* @return {void}
*/
Trie.prototype.insert = function(word) {
let len = word.length;
//node表示当前的trie结点
let node = this;
for(let i = 0; i< len; i++) {
//word[i]是一个字符串类型,charCodeAt方法将他转换成Ascii码
//比如字符b的ascii码是98,那么n=1;
let n = word[i].charCodeAt()-97;
//如果node孩子结点中已经存在这个字符就让当前的trie等于这个结点的孩子trie
//如果node孩子节点数组中不存在我们就构造这样的一个trie
if(node.next[n] && node.next[n].val==word[i]){
node = node.next[n]
}else {
node.next[n] = new Trie(word[i]);
node= node.next[n];
}
}
//构造完毕让最后一个trie的isEnd属性标记为true,表示最后一个结点
node.isEnd = true;
};
Trie.prototype.search = function(word) {
if(!word) return false;
let len = word.length;
let node = this;
let result = true;
for(let i = 0; i < len; i++) {
let n = word[i].charCodeAt()-97;
if(node.next[n]&&node.next[n].val==word[i]) {
node = node.next[n];
}
else {
result = false;
break;
}
}
//search的时候如果次数result是true还要判断此时node是否是前缀树的叶子结点,如果不是还是会返回false
if(result) result = node.isEnd;
return result;
};
//search明白了这个函数也就明白了
Trie.prototype.startsWith = function(prefix) {
let len = prefix.length;
let node = this;
let result = true;
for(let i = 0; i < len; i++) {
let n = prefix[i].charCodeAt()-97;
if(node.next[n]) {
node = node.next[n];
}
else {
result = false;
break;
}
}
return result;
};
- java实现
class Trie {
// 当前节点的值
public char value;
//a-z有26个字母,需要访问时由于a的ASCII码为97,所以所有字母访问的对应下标皆为 字母的ASCII码-97
public Trie[] children=new Trie[26];
// 标识此节点是否为某个单词的结束节点
public boolean endAsWord=false;
public Trie() {
}
public void insert(String word) {
if(word!=null){
//分解成字符数组
char[] charArr=word.toCharArray();
//模拟指针操作,记录当前访问到的树的节点
Trie currentNode=this;
for(int i=0;i<charArr.length;i++){
char currentChar=charArr[i];
//根据字符获取对应的子节点
Trie node=currentNode.children[currentChar-97];
if(node!=null && node.value==currentChar){//判断节点是否存在
currentNode=node;
}else{//不存在则创建一个新的叶子节点,并指向当前的叶子节点
node=new Trie();
node.value=currentChar;
currentNode.children[currentChar-97]=node;
currentNode=node;
}
}
//这个标识很重要,将最后叶子结点标记true在后面的serach中有用
currentNode.endAsWord=true;
}
}
public boolean search(String word) {
boolean result=true;
if(word!=null && !word.trim().equals("")){
char[] prefixChar=word.toCharArray();
Trie currentNode=this;
for(int i=0;i<prefixChar.length;i++){
char currentChar=prefixChar[i];
Trie node=currentNode.children[currentChar-97];
if(node!=null && node.value==currentChar){//判断节点是否存在
currentNode=node;
}else{
result=false;
break;
}
}
if(result){
result=currentNode.endAsWord;
}
}
return result;
}
public boolean startsWith(String prefix) {
boolean result=true;
if(prefix!=null && !prefix.trim().equals("")){
char[] prefixChar=prefix.toCharArray();
Trie currentNode=this;
for(int i=0;i<prefixChar.length;i++){
char currentChar=prefixChar[i];
Trie node=currentNode.children[currentChar-97];
if(node!=null && node.value==currentChar){//判断节点是否存在
currentNode=node;
}else{
result=false;
break;
}
}
}
return result;
}
}
额外补充:尾递归
大家都知道递归容易栈溢出,因为函数入栈后还没有销毁又入栈,递归的次数决定了你开辟栈的空间,所以如果递归次数很多就容易栈溢出。
于是就有了尾递归。那么什么是尾递归,尾递归来源于尾调用,尾调用在《ES6标准入门一书》里面有详细说明,就是说一个函数A的尾部返回的一个函数B,并且函数B不会用到函数A的内部变量,由于是A函数的最后一步,那执行B函数的时候就销毁A的执行栈。因为调用位置和内部变量都不会再用到了,直接使用内层函数的调用栈取代外层函数就可以。
这有一篇我觉得讲的很好理解的尾递归和时间复杂度的文章分享给大家 尾递归
最后
最后说一点个人对于算法的感受。
算法刚开始真的很难,但是到后面入门了会发现有了思维会觉得算法式一件很有趣的事情,特别是当空间复杂度和时间复杂度击败100%的时候挺开心的。
刚开始的时候根本没法理解递归的思维,我只能人脑入栈,手动画图写代码,把每一步都写出来。结果一天就过去了,有时候能把自己写晕,俺就是个渣渣🤷♂️
另外还有两点想说
- 树的遍历与五种情况,上面所有的题目归根到底都是树的遍历。
- 写算法题要细心 ,做算法要耐心和坚持
如果上面的题目您还没有练习,那就花大量的时间去练习吧。这些题目如果自己能写出来,我想后面做算法会越来越快滴(大佬请绕行👀👍)
如果对于里面的题目有哪些地方不理解,直接评论呼叫我,如果讲不清除骂我渣男就好了~
最后送上一个迟到的五一国际劳动节快乐,愿大家拥有一个充实的假期💕🐱🏍~
本文使用 mdnice 排版
附:曾经人脑入栈进行递归的样子🤷♀️...️