二叉树算法题解集合
1-二叉树的中序遍历
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
bash
var inorderTraversal = function(root) {
let res = []
const dfs = (root)=>{
if(root === null) return
dfs(root.left)
res.push(root.val)
dfs(root.right)
}
dfs(root)
return res
};
2-二叉树的最大深度
给定一个二叉树 root ,返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
bash
var maxDepth = function(root) {
// 层序遍历
// num记录深度
// 使用队列记录(队列的特点是先进先出)
let num = 0
let queue = [root]
while(queue.length && root){
let n = queue.length // n记录当前层待遍历的节点数量
while(n--){
let node = queue.shift() // 取队列头部的节点
// 若当前节点有左孩子,加入队列(下一层待遍历的节点)
node.left && queue.push(node.left)
// 若当前节点有右孩子,加入队列(下一层待遍历的节点)
node.right && queue.push(node.right)
}
//当前层遍历完毕,深度加1
num++
}
return num
};
深度是 "从根往下数",高度是 "从叶往上数"
自上而下(根 → 目标节点) 自下而上(目标节点 → 叶子)
使用前序求的就是深度,使用后序求的是高度 整棵二叉树的最大深度,就是这棵树的高度
3-翻转二叉树
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
bash
var invertTree = function(root) {
// 层序遍历
let queue = [root]
while(queue.length && root){
let n = queue.length
while(n--){
// 取出当前节点
let node = queue.shift()
// 交换当前节点的左右子节点
let temp = node.left
node.left = node.right
node.right = temp
// 下一层要处理的是"已经交换后的子节点"
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
}
return root
};
把每一个节点的左右孩子交换
遍历到节点时直接交换它的左右子节点,再把交换后的左右子节点加入队列,继续处理下一层
层序遍历是从上往下,先交换左右节点,再翻转左右子树
bash
var invertTree = function(root) {
// 前序遍历
if(!root) return null
// 保留原来的右子树,方便后续对右子树做翻转
const rightNode = root.right
// 先翻转左子树,再赋值给右节点
root.right = invertTree(root.left)
// 先翻转右子树,再赋值给左节点
root.left = invertTree(rightNode)
return root
};
前序遍历是从下往上,先翻转左右子树再交换左右节点
如果是先翻转右子树,就要先暂存左节点,因为翻转后的右子树被赋值给左节点,会覆盖原来的左节点的值,找不到原来的左子树
4-对称二叉树
给定一个二叉树,检查它是否是镜像对称的。
bash
var isSymmetric = function(root) {
// 层序遍历
if(!root) return true
// 将根节点的左右子节点入队
let queue = []
queue.push(root.left)
queue.push(root.right)
// 层序遍历:成对处理队列中的节点(每次出队2个,检查是否对称)
while(queue.length){
// 成对出队:每次取队列前两个节点(这是一对需要检查的对称节点)
let leftNode = queue.shift()
let rightNode = queue.shift()
// 情况1:两个节点都为null → 对称,跳过后续检查,继续下一对
if(leftNode === null && rightNode === null) continue
// 情况2:一个为null,一个不为null → 不对称,直接返回false;两个节点都存在但值不相等,直接返回false
if(leftNode === null || rightNode === null || leftNode.val !== rightNode.val) return false
// 情况3:两个节点对称(都非null且值相等)→ 继续检查它们的子节点
// 按对称规则入队下一层的待处理节点
// 左->左 右->右 左->右 右->左
queue.push(leftNode.left)
queue.push(rightNode.right)
queue.push(leftNode.right)
queue.push(rightNode.left)
}
// 所有成对节点都检查完,没有发现不对称 → 返回true
return true
};
对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的
层序遍历
bash
var isSymmetric = function(root) {
// 后序遍历思想,先递归处理所有子节点对,再回溯判断当前节点对
if(!root) return true
// 辅助递归函数
const compareNode = (leftNode,rightNode)=>{
// 判断结构是否对称,包含两种情况:
// 1.两个节点都为空->对称true;
// 2.一个节点为空,一个节点不为空->不对称false
if(leftNode === null || rightNode === null) return leftNode === rightNode
// 3.两个节点都存在判断值是否相等
if(leftNode.val !== rightNode.val) return false
// 当前节点对是对称的,单层递归逻辑,继续检查子节点
// outSide:left.left和right。right
// inSide:left.right和right.left
let outSide = compareNode(leftNode.left, rightNode.right)
let inSide = compareNode(leftNode.right, rightNode.left)
// // 外侧和内侧都对称,才说明当前两个节点对称
return outSide&&inSide
}
return compareNode(root.left,root.right)
};
用递归函数逐对检查对称位置的节点
5- 二叉树的直径
给你一棵二叉树的根节点,返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点
root 。 两节点之间路径的 长度 由它们之间边数表示。
bash
var diameterOfBinaryTree = function(root) {
// 记录最大直径
// 二叉树的直径 = 所有节点的「左子树深度 + 右子树深度」的最大值
let maxDepth = 0
// 计算节点的深度(边数),同时更新最大直径
const getDepth = (node)=>{
if(!node) return 0
// 左子树深度
const leftDepth = getDepth(node.left)
// 右子树深度
const rightDepth = getDepth(node.right)
// 更新最大直径
maxDepth = Math.max(maxDepth, leftDepth + rightDepth)
// 返回当前节点的深度
return 1 + Math.max(leftDepth,rightDepth)
}
getDepth(root)
return maxDepth
};
二叉树的直径 = 所有节点的「左子树深度 + 右子树深度」的最大值
6-二叉树的层序遍历
bash
var levelOrder = function(root) {
// 层序遍历
// res记录最终结果
let res = []
let queue = [root]
// 循环入口条件queue.length和root不为null
// 判断条件必须加上root,root = null时queue=[null],queue.length > 0
// 不加root会出现找不到node.val的情况
while(queue.length && root){
// 当前层待遍历的节点数量
let n = queue.length
// 记录当前层遍历的值
let curLevel = []
while(n--){
let node = queue.shift()
curLevel.push(node.val)
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
// 把遍历完的层的节点记录加入最终记录
res.push(curLevel)
}
return res
};
7-将有序数组转化为二叉搜索树
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。
bash
var sortedArrayToBST = function(nums) {
// 中序遍历 BST 的结果是升序序列
// 左子树所有节点值 < 根节点值;右子树所有节点值 > 根节点值
// 选择数组中间元素作为根节点
// 辅助递归函数
const buildTree =(arr,left,right)=>{
// 递归终止条件:左边界>右边界,无可构建的节点
if (left > right) return null
// 取数组中间元素作为根节点
let mid = Math.floor(left+(right-left)/2)
// 递归构建左右左子树
// 左子树的数组索引范围[left,mid-1]
let root = new TreeNode(arr[mid])
root.left = buildTree(arr,left,mid-1)
// 右子树的数组索引范围[mid+1,right]
root.right = buildTree(arr,mid+1,right)
// 返回当前子树的根节点
return root
}
return buildTree(nums,0,nums.length-1)
};
二叉搜索树(BST)的关键特性
- 左子树所有节点值 < 根节点值;
- 右子树所有节点值 > 根节点值;
- 中序遍历 BST的结果是升序序列(反过来:升序数组可以完美对应 BST 的中序遍历)。
最优策略是:选数组的中间元素作为根节点
- 选中间元素当根节点:让左右子树节点数均衡,保证平衡;
- 递归构建左子树:处理数组「左半部分」,作为根节点的左子树;
- 递归构建右子树:处理数组「右半部分」,作为根节点的右子树;
- 终止条件:当左边界 > 右边界时,说明当前子树无节点,返回 null。
8-验证二叉搜索树
bash
var isValidBST = function(root) {
// 借助数组辅助
// 二叉搜索树的中序遍历会得到升序数组
// 判断中序遍历得到的数组是否升序即可
let arr = []
const buildArr = (root)=>{
// 中序遍历
if(root){
buildArr(root.left)
arr.push(root.val)
buildArr(root.right)
}
}
buildArr(root)
for(let i = 0; i < arr.length; i++){
// 非升序(小于或者等于)
if(arr[i] <= arr[i-1]){
return false
}
}
return true
};
bash
var isValidBST = function(root) {
// 递归函数判断
// 当前节点的值必须大于其左子树中所有节点的值,并且小于其右子树中所有节点的值
const validate = (node,max,min)=>{
// 空节点符合
if(node === null) return true
// 节点值不符合,当前节点值必须在 (min, max) 范围内
if(node.val >= max || node.val <= min) return false
// 对左子树进行递归,更新最大值为当前节点值。
// 对右子树进行递归,更新最小值为当前节点值。
return validate(node.left,node.val,min) && validate(node.right,max,node.val)
}
// 最大值正无穷,最小值负无穷
return validate(root,Infinity,-Infinity)
};
递归判断( 当前节点的值必须大于其左子树中所有节点的值,并且小于其右子树中所有节点的值。 )
- 对左子树进行递归,更新最大值为当前节点值。
- 对右子树进行递归,更新最小值为当前节点值。
9-二叉搜索树中第k小的元素
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(k 从 1 开始计数)。
bash
var kthSmallest = function(root, k) {
// 借助数组辅助
// 二叉搜索树的中序遍历可以得到一个升序数组
let arr = []
const buildArr = (root)=>{
if(root){
buildArr(root.left)
arr.push(root.val)
buildArr(root.right)
}
}
buildArr(root)
// k从1开始计数,数组索引从0开始
return arr[k-1]
};
和上一道题思路一致
10-二叉树的右视图
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
bash
var rightSideView = function(root) {
// 层序遍历,记录每一层的最后一个节点
// 二叉树的「右视图」本质是:每一层的最后一个节点的值组成的数组
let res = []
let queue = [root]
while(queue.length && root){
let n = queue.length
while(n--){
let node = queue.shift()
// 记录当前层的最后一个节点值
if(n === 0){
res.push(node.val)
}
// 入队下一层待遍历的节点
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
}
return res
};
二叉树的「右视图」本质是:每一层的最后一个节点的值组成的数组
11-二叉树展开为链表
给你二叉树的根结点 root ,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
- 展开后的单链表应该与二叉树 先序遍历 顺序相同。
bash
var flatten = function(root) {
// 先序遍历顺序是「根→左→右」,它的左子树所有节点,必须出现在「根」之后、「原右子树」之前;
// 需要找到左子树的「最后一个节点」(先序遍历的最后一个),把原右子树接在它后面
// curr记录当前子树的根节点
let curr = root
while(curr){
// 当左子树存在时
if(curr.left){
// predecessor记录当前左子树的最右侧节点
let predecessor = curr.left
while(predecessor.right){
predecessor = predecessor.right
}
// 将原来的右子树挂载到最右侧节点的右子树上
predecessor.right = curr.right
// 将修改后的原左子树移动到原来右子树的位置
curr.right = curr.left
// 原左子树节点值置空
curr.left = null
}
// 一直向右修改遍历
curr = curr.right
}
};
先序遍历
12-从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder是同一棵树的中序遍历,请构造二叉树并返回其根节点。
bash
var buildTree = function(preorder, inorder) {
// 前序遍历:根左右;中序遍历:左根右
// 取前序遍历序列的第一个元素作为根节点,找到其在中序遍历序列中的索引,左边为左子树序列,右边为右子树序列
if(preorder.length === 0) return null
// 先序遍历序列的第一个元素是根节点的值
const rootVal = preorder.shift()
// 找到根节点在中序遍历序列中的索引,中序遍历中根节点左边的为左子树[0,rootIdx-1],右边为右子树[rootIdx+1,inorder.length-1]
const rootIdx = inorder.indexOf(rootVal)
const root = new TreeNode(rootVal)
//分割左子树和右子树
// slice(start,end):start和end的可选,start默认为0,end默认为数组长度,取值范围为[start,end)
root.left = buildTree(preorder.slice(0,rootIdx),inorder.slice(0,rootIdx))
// 由于第一步取先序遍历序列的第一个元素为根节点,先序遍历序列长度-1
// 先序遍历序列中,左子树[0,rootIdx-1],右子树为[rootIdx,preorder.length-1]
root.right = buildTree(preorder.slice(rootIdx),inorder.slice(rootIdx+1))
return root
};
- 前序遍历的顺序是「根节点 → 左子树 → 右子树」,所以前序数组的第一个元素一定是当前子树的根节点。
- 中序遍历的顺序是「左子树 → 根节点 → 右子树」,所以找到根节点在中序数组中的位置后,左侧是左子树的中序数组,右侧是右子树的中序数组。
13-路径总和Ⅲ
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
bash
var pathSum = function(root, targetSum) {
// 前缀和:使用哈希表
// 如果两段路径的总和的差值 = targetNum ; 那么之间的子路径就是一条目标路径
// 哈希表中存放的是从根到当前节点的路径上,所有已经走过的节点的前缀和,以及每个前缀和出现了多少次
// key是前缀和,value是前缀和出现的次数
// count:符合条件的目标路径的条数
const prefixSumMap = new Map()
prefixSumMap.set(0,1)
const backtrack = (node,currentSum)=>{
if(!node) return 0
// currentSum当前节点的前缀和
currentSum += node.val
// 找历史前缀和 = currentSum - targetSum,统计次数
let count = prefixSumMap.get(currentSum-targetSum) || 0
// 更新哈希表:当前前缀和的次数+1
prefixSumMap.set(currentSum, (prefixSumMap.get(currentSum) || 0)+1)
// 递归遍历左右子树,累加路径数
count += backtrack(node.left,currentSum)
count += backtrack(node.right,currentSum)
// 回溯:恢复哈希表(当前节点遍历完,移除其前缀和)
prefixSumMap.set(currentSum, (prefixSumMap.get(currentSum) || 0)-1)
// 8. 返回当前节点的总路径数
return count
}
return backtrack(root,0)
};
这个题有点绕,需要思考
初始化哈希表:防止漏掉只有一个节点,节点值=targetNum的情况
14-二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为:"对于有根树 T 的两个节点p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。"
bash
var lowestCommonAncestor = function(root, p, q) {
// 递归函数辅助
const travelTree = (node,p,q)=>{
// 递归终止条件
if(node === null || node === p || node === q){
return node
}
// 后序遍历,在左右子树中寻找是否有p和q节点
let left = travelTree(node.left,p,q)
let right = travelTree(node.right,p,q)
// 单层递归逻辑
// 如果左子树和右子树中各找到一个目标节点,当前节点就是最近公共祖先
if(left !== null && right !== null){
return node
}
// 如果左子树中没找到目标节点,目标节点在右子树,返回右子树
if(left === null){
return right
}
// 右子树没找到,目标节点都在左子树,返回左子树的结果
return left
}
return travelTree(root,p,q)
};
这个有点绕,需要仔细思考
15-二叉树中的最大路径和
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个节点,且不一定经过根节点。 路径和 是路径中各节点值的总和。 给你一个二叉树的根节点 root ,返回其 最大路径和
bash
var maxPathSum = function(root) {
// 如果有一条合法的路径,那一定有一个最高点(最接近根节点的)
// 最高转折点可以同时向它的左右子树延申,其他节点只能选一边加入路径
// 初始化最大路径和为负无穷
let maxSum = -Infinity
// 递归函数计算最大路径和
const maxGain = (node)=>{
// 节点为空,贡献值为0
if(node === null) return 0
// 计算当前节点的左子树的贡献值,和0比较,节点值可能为负数
let leftGain = Math.max(maxGain(node.left),0)
// 计算当前节点的右子树的贡献值
let rightGain = Math.max(maxGain(node.right),0)
// 计算当前节点作为最高转折点的路径和
// 左子树贡献 + 当前节点值 + 右子树贡献
let currentPathSum = node.val + leftGain + rightGain
// 更新最大路径和
maxSum = Math.max(currentPathSum,maxSum)
// 返回节点值提供给其父节点的最大贡献
// 只能选左或右其中一条更长的路径
return node.val+Math.max(leftGain,rightGain)
}
maxGain(root)
return maxSum
};