Day20-21-二叉树-leetcode235,701,450 - 669,108,538
- 二叉搜索树的最近公共祖先
- 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
- 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
- 说明:
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉搜索树中。
- 思路
var lowestCommonAncestor = function(root, p, q) {
// 使用递归的方法
// 1. 使用给定的递归函数lowestCommonAncestor
// 2. 确定递归终止条件
if(root === null) {
return root;
}
if(root.val > p.val && root.val > q.val) {
// 向左子树查询
return root.left = lowestCommonAncestor(root.left,p,q);
}
if(root.val < p.val && root.val < q.val) {
// 向右子树查询
return root.right = lowestCommonAncestor(root.right,p,q);
}
return root;
};
var lowestCommonAncestor = function(root, p, q) {
// 使用迭代的方法
while(root) {
if(root.val > p.val && root.val > q.val) {
root = root.left;
}else if(root.val < p.val && root.val < q.val) {
root = root.right;
}else {
return root;
}
}
return null;
};
- 二叉搜索树中的插入操作
-
给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
-
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
-
提示:
- 树中的节点数将在 [0, 104]的范围内。
- -108 <= Node.val <= 108
- 所有值 Node.val 是 独一无二 的。
- -108 <= val <= 108
- 保证 val 在原始BST中不存在。
-
思路
- 遍历二叉搜索树,找到空节点 插入元素
- 递归:
- (1)确定递归函数参数及返回值,参数=》根节点指针、要插入的元素,返回=》节点node
- (2)确定终止条件,找到遍历的节点为null时,即要插入节点的位置,并把插入的节点返回
- (3)确定单层递归的逻辑,搜索树是有方向的,可以根据插入元素的值,决定递归方向
// 有返回值的递归写法
var insertIntoBST = function (root, val) {
const setInOrder = (root, val) => {
// 如果当前节点 root 为 null,说明找到了插入位置,创建新节点并返回。
if (root === null) {
let node = new TreeNode(val);
return node;
}
/**
递归逻辑
如果 val 小于当前节点值,递归插入到左子树。
如果 val 大于当前节点值,递归插入到右子树。
每次递归返回的节点都赋值给对应的 left 或 right,保证插入后树结构正确。
*/
if (root.val > val)
root.left = setInOrder(root.left, val);
else if (root.val < val)
root.right = setInOrder(root.right, val);
return root;
}
return setInOrder(root, val);
};
// 无返回值的递归
/**
1. parent 变量
用于记录当前节点的父节点,方便在找到插入位置时,把新节点挂到父节点的 left 或 right。
2. preOrder 递归函数
参数:当前节点 cur,要插入的值 val。
如果 cur 为 null,说明找到了插入位置,创建新节点,并根据 parent 的值大小挂到左或右。
如果没到插入位置,递归遍历左或右子树,并更新 parent。
3. 特殊情况处理
如果 root 为空,直接新建根节点。
4. 递归查找插入位置
从根节点开始递归查找,直到找到空位插入。
5. 返回根节点
最后返回更新后的根节点。
*/
var insertIntoBST = function (root, val) {
// 用于记录当前节点的父节点
let parent = new TreeNode(0);
const preOrder = (cur, val) => {
if (cur === null) {
// 找到插入位置,创建新节点
let node = new TreeNode(val);
// 挂到父节点的左
if (parent.val > val)
parent.left = node;
// 挂到父节点的右
else
parent.right = node;
return;
}
// 更新父节点
parent = cur;
// 递归左子树
if (cur.val > val)
preOrder(cur.left, val);
// 递归右子树
if (cur.val < val)
preOrder(cur.right, val);
}
// 特殊情况:树为空,直接新建根节点
if (root === null)
root = new TreeNode(val);
// 从根节点开始递归查找插入位置
preOrder(root, val);
// 返回根节点
return root;
};
// 迭代
/**
步骤说明
1. 特殊情况处理
如果 root 为空,直接新建一个节点作为根节点返回。
2. 查找插入位置
用 cur 指针从根节点开始遍历,parent 记录当前节点的父节点。
如果 val 小于当前节点值,往左子树走;否则往右子树走。
直到 cur 变为 null,说明找到了插入位置。
3. 插入新节点
新建节点 node。
判断插入到 parent 的左还是右,根据 val 和 parent.val 的大小关系决定。
4. 返回根节点
最后返回更新后的根节点。
*/
var insertIntoBST = function (root, val) {
// 如果树为空,直接新建根节点
if (root === null) {
root = new TreeNode(val);
} else {
// 用于记录当前节点的父节点
let parent = new TreeNode(0);
let cur = root;
// 循环查找插入位置
while (cur) {
// 记录父节点
parent = cur;
// 往左找
if (cur.val > val)
cur = cur.left;
// 往右找
else
cur = cur.right;
}
// 创建新节点
let node = new TreeNode(val);
// 挂到父节点的左或右
if (parent.val > val)
parent.left = node;
else
parent.right = node;
}
// 返回根节点
return root;
};
- 删除二叉搜索树中的节点
-
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索- 树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
-
一般来说,删除节点可分为两个步骤:
-
首先找到需要删除的节点;
-
如果找到了,删除它。
-
思路
-
(1)确定递归函数参数以及返回值,参数=》root,返回值=》node
-
(2)确定终止条件,遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了
-
(3)确定单层递归的逻辑
- 第一种情况:没找到删除的节点,遍历到空节点直接返回了
- 找到删除的节点
- 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
- 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
- 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
- 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
- 第一种情况:没找到删除的节点,遍历到空节点直接返回了
// 递归
/**
递归查找要删除的节点。
删除时分三种情况:叶子节点、只有一个孩子、两个孩子。
两个孩子时,用右子树最小节点替换当前节点,再删除右子树中的最小节点,保证 BST 性质不变。
*/
var deleteNode = function(root, key) {
/**
如果当前节点为空,说明没找到要删除的节点,直接返回 null。
如果 key 比当前节点值大,递归去右子树找。
如果 key 比当前节点值小,递归去左子树找。
如果相等,说明找到了要删除的节点,进入下一步处理。
*/
if (!root) return null;
if (key > root.val) {
root.right = deleteNode(root.right, key);
return root;
} else if (key < root.val) {
root.left = deleteNode(root.left, key);
return root;
} else {
/**
* 删除节点的三种情况
* 场景1:叶子节点(无左右孩子),直接删除,返回 null。
* 场景2:只有一个孩子,只有左孩子,返回左孩子,右孩子同理。这样父节点会直接指向被删除节点的唯一孩子。
* 场景3:左右孩子都存在,找到右子树的最小节点(即后继节点)。用这个最小节点的值替换当前节点的值。然后递归删除右子树中这个最小节点(因为它已经被“提上来”了)。
*/
// 场景1: 该节点是叶节点
if (!root.left && !root.right) {
return null
}
// 场景2: 有一个孩子节点不存在
if (root.left && !root.right) {
return root.left;
} else if (root.right && !root.left) {
return root.right;
}
// 场景3: 左右节点都存在
const rightNode = root.right;
// 获取最小值节点
const minNode = getMinNode(rightNode);
// 将待删除节点的值替换为最小值节点值
root.val = minNode.val;
// 删除最小值节点
root.right = deleteNode(root.right, minNode.val);
return root;
}
};
// 辅助函数:获取最小节点
// 一直往左走,直到没有左孩子,返回该节点
function getMinNode(root) {
while (root.left) {
root = root.left;
}
return root;
}
// 迭代
/**
先用迭代方式找到要删除的节点及其父节点。
用辅助函数处理三种情况(无右子树、有右子树、叶子节点)。
通过父节点指针完成删除操作,保证 BST 结构正确。
*/
var deleteNode = function (root, key) {
/**
*辅助函数 deleteOneNode
用于实际删除节点并返回新的子树根节点。
如果 target 没有右孩子,直接返回左孩子(左孩子可能为 null)。
如果有右孩子,找到右子树最左边的节点(即右子树的最小节点),把 target 的左子树接到这个最小节点的左边,然后返回 target.right 作为新的根节点。
*/
const deleteOneNode = target => {
if (!target) return target
if (!target.right) return target.left
/**
用 cur 指针遍历树,pre 记录父节点。
找到值等于 key 的节点,跳出循环。
*/
let cur = target.right
while (cur.left) {
cur = cur.left
}
cur.left = target.left
return target.right
}
if (!root) return root
let cur = root
let pre = null
while (cur) {
if (cur.val === key) break
pre = cur
cur.val > key ? cur = cur.left : cur = cur.right
}
/**
* 删除节点
如果 pre 为 null,说明要删除的是根节点,直接返回 deleteOneNode(cur) 的结果。
如果要删除的是父节点的左孩子,则 pre.left = deleteOneNode(cur)。
如果要删除的是父节点的右孩子,则 pre.right = deleteOneNode(cur)。
最后返回根节点。
*/
if (!pre) {
return deleteOneNode(cur)
}
if (pre.left && pre.left.val === key) {
pre.left = deleteOneNode(cur)
}
if (pre.right && pre.right.val === key) {
pre.right = deleteOneNode(cur)
}
return root
}
- 修剪二叉搜索树
-
给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案 。
-
所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。
-
思路
-
- 确定递归函数的参数以及返回值,参数=》root、low、hight,返回=》root
-
- 确定终止条件,遇到空节点返回
-
- 确定单层递归的逻辑,如果当前节点的元素小于low的值,应该递归右子树,并返回右子树符合条件的头节点,如果root(当前节点)的元素大于high的,那么应该递归左子树,并返回左子树符合条件的头结点。
// 递归
var trimBST = function (root, low, high){
// 1. 终止条件: 如果当前节点为 null,直接返回 null,递归结束。
if (root === null) {
return null
}
// 2. 当前节点小于 low
// 如果 root.val < low,说明当前节点和它的左子树所有节点都不在区间内(因为 BST 左子树都更小)。
// 递归修剪右子树,并返回修剪后的右子树。
if (root.val < low) {
let right = trimBST(root.right, low, high)
// 把节点的右孩子返回给上一层,相当于删除节点
return right
}
// 3. 当前节点大于 high
// 如果 root.val > high,说明当前节点和它的右子树所有节点都不在区间内(因为 BST 右子树都更大)。
// 递归修剪左子树,并返回修剪后的左子树。
if (root.val > high) {
let left = trimBST(root.left, low, high)
return left
}
// 4. 当前节点在区间内
// 如果 root.val 在 [low, high] 区间内,递归修剪左右子树,并把结果分别赋值给 root.left 和 root.right。
// 返回当前节点 root。
// 用节点的左孩子 把上一层返回的节点的右孩子接住
root.left = trimBST(root.left, low, high)
root.right = trimBST(root.right, low, high)
return root
}
---
108. 将有序数组转换为二叉搜索树
- 给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。
- 思路
- 如何构造二叉树?
- 整体思路:选取一个中间的节点,将这个数组分为左区间和右区间,递归遍历左区间去构成左子树,递归遍历右区间去构成右子树,
- 本题:选取中间节点时,选取数组中间位置的节点,只有选取中间位置的节点作为我们的中节点,可以把第一个节点作为根节点,只有这样选,它的左区间节点和数量和右区间节点的数量才能保证是相同的,这样构造的二叉树才能保证是平衡的;将其分割之后,左区间再去左右分隔,右区间也去选中间节点,再去进行左右分割,这样就保证了每一层它下面的左右子树的数量是相同的,然后根据二叉搜索树节点的性质(右子树大于中节点,左子树小于中节点)去构造二叉树。本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。分割点就是数组中间位置的节点。不断中间分割,然后递归处理左区间,右区间。通过递归函数的返回值来增删二叉树
- 递归
- (1)确定递归函数参数及返回值,用递归函数的返回值来构造中节点的左右孩子,参数=》nums、left、right,返回=》root
- (2)确定递归终止条件,[left, right],left 》right,空节点
- (3)确定单层递归的逻辑,首先取中间元素的位置,以中间位置的元素构造节点,并进行左右区间的划分,节点的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点,最后返回root节点
- 迭代法
- 迭代法可以通过三个队列来模拟,一个队列放遍历的节点,一个队列放左区间下标,一个队列放右区间下标。模拟的就是不断分割的过程。
```js
// 递归
var sortedArrayToBST = function(nums) {
const buildTree = (arr, left, right) =>{
// 区间无元素时,返回空节点
if (left > right) return null
// 防止整数溢出
let mid = Math.floor(left + (right - left) / 2)
// 构造根节点
let root = new TreeNode(arr[mid])
// 递归左区间,构建左子树
root.left = buildTree(arr, left, mid - 1)
// 递归右区间,构建右子树
root.right = buildTree(arr, mid + 1, right)
// 每次递归返回当前子树的根节点
return root
}
return buildTree(nums, 0, nums.length - 1)
}
- 把二叉搜索树转换为累加树
-
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
-
提醒一下,二叉搜索树满足下列约束条件:
-
节点的左子树仅包含键 小于 节点键的节点。
-
节点的右子树仅包含键 大于 节点键的节点。
-
左右子树也必须是二叉搜索树。
-
思路
-
需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。
-
递归
-
(1)递归函数参数及返回值,参数=》cur
-
(2)确定终止条件,遇到空就终止 cur===null
-
(3)确定单层递归逻辑,右中左来遍历二叉树, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。
-
迭代法其实就是中序模板题了
// 递归 利用二叉搜索树的有序性,反中序遍历+累加变量 pre,实现累加树的构建。
/**
* 右中左遍历(反中序遍历)
二叉搜索树中序遍历是递增序列,反中序遍历(右中左)就是递减序列。
这样可以保证每次访问到的节点,都是比当前节点大的节点已经处理过了。
* pre 变量
用于记录当前节点右侧所有节点值的累加和。
每访问一个节点,就把 pre 加到当前节点上,然后更新 pre。
* 递归过程
递归到最右边(最大值节点),开始累加。
每访问一个节点,累加 pre,并更新 pre。
递归左子树,继续累加。
*/
var convertBST = function(root) {
let pre = 0
const ReverseInOrder = (cur) => {
if (cur) {
ReverseInOrder(cur.right) // 1. 先递归右子树(从大到小遍历)
cur.val += pre // 2. 当前节点累加上 pre
pre = cur.val // 3. 更新 pre 为当前节点的新值
ReverseInOrder(cur.left) // 4. 再递归左子树
}
}
ReverseInOrder(root)
return root
}

浙公网安备 33010602011771号