JS/TS数据结构---二叉搜索树
二叉查找树(二叉搜索树)
在二叉查找树(Binary Search Tree,BST)中,每个结点的值都大于左子结点,小于右子结点。
当中序遍历BST时,就可在 O(n) 的时间复杂度内输出有序的结点。
BST的时间复杂度和树的高度成正比,即 O(height),经过推导后,完全二叉树的高度(height)小于等于 log2^n。
平衡二叉查找树的高度接近 logn,所以插入、删除、查找等操作的时间复杂度也比较稳定,都是 O(logn)。
1)操作
查找
在BST中查找一个结点的递归算法是(代码如下所示):
(1)如果被查找的结点和根结点的值相等,则查找命中,否则就递归地的在适当的子树中继续查找。
(2)如果被查找的结点值较小就选择左子树,否则就选择右子树。
find(data) {
//查找
let node = this.root;
while (node != null) {
if (data == node.data) {
return node;
}
data < node.data ? (node = node.left) : (node = node.right);
}
return null;
}
插入
BST插入结点的过程和查找差不多,依次比较结点值和左右子树的大小。
insert(parent, child) {
//插入
if (parent.data > child.data) {
parent.left === null
? (parent.left = child)
: this.insert(parent.left, child);
return;
}
parent.right === null
? (parent.right = child)
: this.insert(parent.right, child);
}
查找最大最小值
在BST中查找最大和最小的结点,以最小值为例,如果根结点的左链接为空,那么一棵BST中的最小值就是根结点;如果左链接非空,那么最小值就是左子树中的最小值。
min(node = this.root) {
//最小值
if (node.left == null) return node;
return this.min(node.left);
}
2)删除
针对删除结点的子结点个数的不同,需要分类讨论(代码如下所示):
(1)如果没有子结点,那么只需将父结点中,链接删除结点的指针置为 null。
(2)如果只有一个子结点,那么只需更新父结点中,链接删除结点的指针指向其子结点即可。
(3)如果包含两个子结点,那么需要先找到该结点右子树中的最小结点,替换要删除的结点;然后再删除该最小结点,由于最小结点肯定没有左子结点,因此可以使用上面两条规则删除它。
del(data) {
//删除
let p = this.root, //p指向要删除的结点,初始化指向根结点
parent = null; //父结点
while (p != null && p.data != data) {
parent = p;
data > p.data ? (p = p.right) : (p = p.left);
}
if (p == null) return; //没有找到
// 要删除的结点有两个子结点
if (p.left != null && p.right != null) {
//查找右子树中最小结点
let minP = p.right,
minParent = p; //minParent表示minP的父结点
while (minP.left != null) {
minParent = minP;
minP = minP.left;
}
p.data = minP.data; //将minP的数据替换到p中
p = minP; //下面就变成了删除minP了
parent = minParent;
}
// 删除结点是叶子结点或者仅有一个子结点
let child; //p的子结点
if (p.left != null) child = p.left;
else if (p.right != null) child = p.right;
else child = null;
if (parent == null) this.root = child;
// 删除的是根结点
else if (parent.left == p) parent.left = child;
else parent.right = child;
}
3)数据重复
要让BST支持重复数据,可以有两种处理方式。
(1)在每个结点中增加一个链表,把相同的值存储到链表中。
(2)将相同的值插入到结点的右子树中,作为大于这个结点来处理。
4)平衡二叉查找树
平衡二叉树是指任意一个结点的左右子树的高度相差不能大于 1,让整棵树左右看起来比较对称和平衡,不要出现左子树很高、右子树很矮的情况。
在下面的示例中,height()函数会自顶向下递归的计算结点的高度,isBalanced()函数会判断左右子树的高度差。
function isBalanced(root) {
if (root == null) return true;
if (Math.abs(height(root.left) - height(root.right)) > 1) {
return false;
}
return isBalanced(root.left) && isBalanced(root.right);
}
function height(root) {
if (root == null) return 0;
return Math.max(height(root.left) + 1, height(root.right) + 1);
}
leetcode题精选
---------------
[95] 不同的二叉搜索树 II
给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同 二叉搜索树 。可以按 任意顺序 返回答案。
示例 1:
输入:n = 3
输出:[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]示例 2:
输入:n = 1
输出:[[1]]
二叉搜索树关键的性质是根节点的值大于左子树所有节点的值,小于右子树所有节点的值,且左子树和右子树也同样为二叉搜索树。
因此在生成所有可行的二叉搜索树的时候,假设当前序列长度为 n,如果我们枚举根节点的值为 i,那么根据二叉搜索树的性质我们可以知道左子树的节点值的集合为 [1…i−1]
,右子树的节点值的集合为[i+1…n]
。而左子树和右子树的生成相较于原问题是一个序列长度缩小的子问题,因此我们可以想到用回溯的方法来解决这道题目。
我们定义 recur(start, end) 函数表示当前值的集合为 [start,end],返回序列 [start,end] 生成的所有可行的二叉搜索树。按照上文的思路,我们考虑枚举 [start,end] 中的值 i 为当前二叉搜索树的根,那么序列划分为了 [start,i−1] 和 [i+1,end] 两部分。我们递归调用这两部分,即 recur(start, i - 1) 和 recur(i + 1, end),获得所有可行的左子树和可行的右子树,那么最后一步我们只要从可行左子树集合中选一棵,再从可行右子树集合中选一棵拼接到根节点上,并将生成的二叉搜索树放入答案数组即可
function generateTrees(n: number): Array<TreeNode | null> {
if(n===0) return [];
function recur(start,end){
if(start>end) return [null];
let allTrees = [];
for(let i=start;i<=end;i++){
let leftTrees = recur(start,i-1);
let rightTrees = recur(i+1,end);
for(let left of leftTrees){
for(let right of rightTrees){
let cur = new TreeNode(i);
cur.left = left;
cur.right = right;
allTrees.push(cur);
}
}
}
return allTrees
}
return recur(1,n)
};
---------------
[98] 验证二叉搜索树------判断一个树是否是二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。示例 1:
输入:root = [2,1,3]
输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
辅助数组解决:
function isValidBST(root: TreeNode | null): boolean {
const traversalArr: number[] = [];
//可以递归中序遍历将二叉搜索树转变成一个数组,代码如下
function inorderTraverse(root: TreeNode | null): void {
if (root === null) return;
inorderTraverse(root.left);
traversalArr.push(root.val);
inorderTraverse(root.right);
}
inorderTraverse(root);
//然后只要比较一下,这个数组是否是有序的,注意二叉搜索树中不能有重复元素。
for (let i = 0, length = traversalArr.length; i < length - 1; i++) {
if (traversalArr[i] >= traversalArr[i + 1]) return false;
}
return true;
};
递归中解决:
- 陷阱1
不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了。(我们要比较的是 左子树所有节点小于中间节点,右子树所有节点大于中间节点。所以以上代码的判断逻辑是错误的。)
初始化比较元素为-Infinity
递归三部曲:
- 确定递归函数,返回值以及参数
- 确定终止条件
- 确定单层递归的逻辑
中序遍历,一直更新maxVal,一旦发现maxVal >= root->val,就返回false,注意元素相同时候也要返回false。
function isValidBST(root: TreeNode | null): boolean {
let maxVal = -Infinity;
function inorderTraverse(root: TreeNode | null): boolean {
if (root === null) return true;
//左
let leftValid: boolean = inorderTraverse(root.left);
if (!leftValid) return false;
// 中序遍历,验证遍历的元素是不是从小到大
if (maxVal < root.val) {
maxVal = root.val
} else {
return false;
}
//中
let rightValid: boolean = inorderTraverse(root.right);
return leftValid && rightValid;
}
return inorderTraverse(root);
};
[99] 恢复二叉搜索树
给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。
示例 1:
输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2]
解释:3 不能是 1 的左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。示例 2:
输入:root = [3,1,4,null,null,2]
输出:[2,1,4,null,null,3]
解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 和 3 使二叉搜索树有效。
中序遍历BST,依次访问的节点值是递增的,错误的BST会破坏递增性,从而能定位出错误。
我发现,错误有两种:
错误1:出现了两对不满足前小后大,需要交换第一对的第一个元素与第二对的第二个元素。
错误2:只出现一对不满足前小后大,交换这一对元素即可。
可以在的递归遍历过程中,将节点值推入一个数组,再遍历数组找出错误对。
但其实没必要。
只用比较前后访问的节点值,prev 保存上一个访问的节点,当前访问的是 root 节点。
每访问一个节点,如果prev.val>=root.val,就找到了一对“错误对”。
检查一下它是第一对错误对,还是第二对错误对。
遍历结束,就确定了待交换的两个错误点,进行交换
function recoverTree(root: TreeNode | null): void {
let perv = new TreeNode(-Infinity);
let err1, err2 = null;
const inOrder = (root) => {
if (root == null) {
return;
}
inOrder(root.left);
if (perv.val >= root.val && err1 == null) { // 当前是第一对错误
err1 = perv; // 记录第一个错误点
}
if (perv.val >= root.val && err1 != null) { // 第一个错误点已确定
err2 = root; // 记录第二个错误点
}
perv = root; // 更新 perv
inOrder(root.right);
};
inOrder(root);
const temp = err1.val;
err1.val = err2.val;
err2.val = temp;
};
---------------
[701] 二叉搜索树中的插入操作
给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
示例 1:
输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]
解释:另一个满足题目要求可以通过的树是:
示例 2:
输入:root = [40,20,60,10,30,50,70], val = 25
输出:[40,20,60,10,30,50,70,null,null,25]示例 3:
输入:root = [4,2,7,1,3,null,null,null,null,null,null], val = 5
输出:[4,2,7,1,3,5]
其实可以不考虑题目中提示所说的改变树的结构的插入方式。
只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了
递归三部曲:
- 确定递归函数参数以及返回值
参数就是根节点指针,以及要插入元素,这里递归函数要不要有返回值呢?
可以有,也可以没有,但递归函数如果没有返回值的话,实现是比较麻烦的,下面也会给出其具体实现代码。
有返回值的话,可以利用返回值完成新加入的节点与其父节点的赋值操作。(下面会进一步解释)
递归函数的返回类型为节点类型TreeNode 。
function insertIntoBSt(root,val)
- 确定终止条件
终止条件就是找到遍历的节点为null的时候,就是要插入节点的位置了,并把插入的节点返回。
if (root === null) return new TreeNode(val);
- 确定单层递归的逻辑
此时要明确,需要遍历整棵树么?
别忘了这是搜索树,遍历整棵搜索树简直是对搜索树的侮辱,哈哈。
搜索树是有方向了,可以根据插入元素的数值,决定递归方向。
if (root.val > val) {
root.left = insertIntoBST(root.left, val);
} else {
root.right = insertIntoBST(root.right, val);
}
到这里,大家应该能感受到,如何通过递归函数返回值完成了新加入节点的父子关系赋值操作了,下一层将加入节点返回,本层用root->left或者root->right将其接住。
function insertIntoBST(root: TreeNode | null, val: number): TreeNode | null {
if (root === null) return new TreeNode(val);
if (root.val > val) {
root.left = insertIntoBST(root.left, val);
} else {
root.right = insertIntoBST(root.right, val);
}
return root;
};
[450] 删除二叉搜索树中的节点
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
首先找到需要删除的节点;
如果找到了,删除它。示例 1:
输入:root = [5,3,6,2,4,null,7], key = 3
输出:[5,4,6,2,null,null,7]
解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
另一个正确答案是 [5,2,6,null,4,null,7]。
示例 2:
输入: root = [5,3,6,2,4,null,7], key = 0
输出: [5,3,6,2,4,null,7]
解释: 二叉树不包含值为 0 的节点
示例 3:输入: root = [], key = 0
输出: []
递归三部曲:
- 确定递归函数参数以及返回值
说道递归函数的返回值,在二叉树:搜索树中的插入操作 (opens new window)中通过递归返回值来加入新节点, 这里也可以通过递归返回值删除节点。
function deleteNode(root: TreeNode | null, key: number): TreeNode | null
- 确定终止条件
遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了
if (root === null) return null;
- 确定单层递归的逻辑
这里就把二叉搜索树中删除节点遇到的情况都搞清楚。
有以下五种情况:
- 第一种情况:没找到删除的节点,遍历到空节点直接返回了
- 找到删除的节点
- 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
- 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
- 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
- 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
第五种情况有点难以理解,看下面动画:
动画中棵二叉搜索树中,删除元素7, 那么删除节点(元素7)的左孩子就是5,删除节点(元素7)的右子树的最左面节点是元素8。
将删除节点(元素7)的左孩子放到删除节点(元素7)的右子树的最左面节点(元素8)的左孩子上,就是把5为根节点的子树移到了8的左孩子的位置。
要删除的节点(元素7)的右孩子(元素9)为新的根节点。.
这样就完成删除元素7的逻辑,最好动手画一个图,尝试删除一个节点试试。
if (root.val === key) {
if (root.left === null && root.right === null) return null;
if (root.left === null) return root.right;
if (root.right === null) return root.left;
let curNode: TreeNode = root.right;
while (curNode.left !== null) {
curNode = curNode.left;
}
curNode.left = root.left;
return root.right;
}
if (root.val > key) root.left = deleteNode(root.left, key);
if (root.val < key) root.right = deleteNode(root.right, key);
全部代码:
function deleteNode(root: TreeNode | null, key: number): TreeNode | null {
if (root === null) return null;
if (root.val === key) {
if (root.left === null && root.right === null) return null;
if (root.left === null) return root.right;
if (root.right === null) return root.left;
let curNode: TreeNode = root.right;
while (curNode.left !== null) {
curNode = curNode.left;
}
curNode.left = root.left;
return root.right;
}
if (root.val > key) root.left = deleteNode(root.left, key);
if (root.val < key) root.right = deleteNode(root.right, key);
return root;
};
---------------
[108] 将有序数组转换为二叉搜索树
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
示例 1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
示例 2:
输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
做这道题目之前大家可以了解一下这几道:
- 106.从中序与后序遍历序列构造二叉树(opens new window)
- 654.最大二叉树 (opens new window)中其实已经讲过了,如果根据数组构造一棵二叉树。
- 701.二叉搜索树中的插入操作(opens new window)
- 450.删除二叉搜索树中的节点
本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。
本题其实要比二叉树:构造二叉树登场! (opens new window)和 二叉树:构造一棵最大的二叉树 (opens new window)简单一些,因为有序数组构造二叉搜索树,寻找分割点就比较容易了。
分割点就是数组中间位置的节点。
let mid: number = Math.floor((left + right) / 2);
递归三部曲:
-
确定递归函数返回值及其参数
-
删除二叉树节点,增加二叉树节点,都是用递归函数的返回值来完成,这样是比较方便的。
相信大家如果仔细看了二叉树:搜索树中的插入操作 (opens new window)和二叉树:搜索树中的删除操作 (opens new window),一定会对递归函数返回值的作用深有感触。
那么本题要构造二叉树,依然用递归函数的返回值来构造中节点的左右孩子。
再来看参数,首先是传入数组,然后就是左下标left和右下标right,我们在二叉树:构造二叉树登场! (opens new window)中提过,在构造二叉树的时候尽量不要重新定义左右区间数组,而是用下标来操作原数组。
所以代码如下:
// 左闭右闭区间[left, right] function recur(nums: number[], left: number, right: number): TreeNode | null
这里注意,我这里定义的是左闭右闭区间,在不断分割的过程中,也会坚持左闭右闭的区间,这又涉及到我们讲过的循环不变量。
-
-
确定递归终止条件
这里定义的是左闭右闭的区间,所以当区间 left > right的时候,就是空节点了。
if (left > right) return null;
-
确定单层递归的逻辑
首先取数组中间元素的位置,不难写出
int mid = (left + right) / 2;
,这么写其实有一个问题,就是数值越界,例如left和right都是最大int,这么操作就越界了,在二分法 (opens new window)中尤其需要注意!所以可以这么写:
int mid = left + ((right - left) / 2);
但本题leetcode的测试数据并不会越界,所以怎么写都可以。但需要有这个意识!
取了中间位置,就开始以中间位置的元素构造节点,代码:
TreeNode* root = new TreeNode(nums[mid]);
。接着划分区间,root的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点。
最后返回root节点,单层递归整体代码如下:
let mid: number = Math.floor((left + right) / 2); const root: TreeNode = new TreeNode(nums[mid]); root.left = recur(nums, left, mid - 1); root.right = recur(nums, mid + 1, right); return root;
这里
int mid = left + ((right - left) / 2);
的写法相当于是如果数组长度为偶数,中间位置有两个元素,取靠左边的。
function sortedArrayToBST(nums: number[]): TreeNode | null {
function recur(nums: number[], left: number, right: number): TreeNode | null {
if (left > right) return null;
let mid: number = Math.floor((left + right) / 2);
const root: TreeNode = new TreeNode(nums[mid]);
root.left = recur(nums, left, mid - 1);
root.right = recur(nums, mid + 1, right);
return root;
}
return recur(nums, 0, nums.length - 1);
};
[109] 有序链表转换二叉搜索树
给定一个单链表的头节点 head ,其中的元素 按升序排序 ,将其转换为高度平衡的二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差不超过 1。
示例 1:
输入: head = [-10,-3,0,5,9]
输出: [0,-3,9,-10,null,5]
解释: 一个可能的答案是[0,-3,9,-10,null,5],它表示所示的高度平衡的二叉搜索树。示例 2:
输入: head = []
输出: []
中序遍历
function sortedListToBST(head: ListNode | null): TreeNode | null {
if(head===null) return null;
return helper([head],0,sizeC(head)-1);
};
//求链表的长度
function sizeC(head:ListNode|null){
let count = 0;
while(head!==null){
count++;
head = head.next;
}
return count
}
//
function helper(arr,left,right){
if(left>right) return null;
let mid = Math.floor((left+right)/2);
let leftNode = helper(arr,left,mid-1);
let node = new TreeNode(arr[0].val);
arr[0] = arr[0].next;
let rightNode = helper(arr,mid+1,right);
node.left = leftNode;
node.right = rightNode;
return node;
}
---------------
[230] 二叉搜索树中第K小的元素
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1 输出:1
示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3 输出:3
中序遍历得到数组,然后直接返回arr[k-1]
[530] 二叉搜索树的最小绝对差
给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。
差值是一个正数,其数值等于两值之差的绝对值。
示例 1:
输入:root = [4,2,6,1,3]
输出:1示例 2:
输入:root = [1,0,48,null,null,12,49]
输出:1
最简单方法-中序遍历操作数组
中序遍历将二叉搜索树转换为升序数组,然后定义一个最小值,遍历数组即可。时间复杂度O(n),空间复杂度O(n)。
function getMinimumDifference(root: TreeNode | null): number {
let arr = [];
function inorder(node){
if (node){
inorder(node.left);
arr.push(node.val);
inorder(node.right);
}
return arr;
}
inorder(root);
let minDiffValue = Number.MAX_SAFE_INTEGER;
for (let i = 0; i < arr.length - 1; i++){
minDiffValue = Math.min(minDiffValue, arr[i + 1] - arr[i]);
}
return minDiffValue;
};
递归中直接计算
以上代码是把二叉搜索树转化为有序数组了,其实在二叉搜素树中序遍历的过程中,我们就可以直接计算了。
需要用一个pre节点记录一下cur节点的前一个节点。
如图:
一些同学不知道在递归中如何记录前一个节点的指针,其实实现起来是很简单的,大家只要看过一次,写过一次,就掌握了。
function getMinimumDifference(root: TreeNode | null): number {
let preNode: TreeNode | null= null;
let resMin: number = Infinity;
function recur(root: TreeNode | null): void {
if (root === null) return;
recur(root.left);
if (preNode !== null) {
resMin = Math.min(resMin, root.val - preNode.val);
}
preNode = root;
recur(root.right);
}
recur(root);
return resMin;
};
[501] 二叉搜索树中的众数
给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。
如果树中有不止一个众数,可以按 任意顺序 返回。
假定 BST 满足如下定义:
结点左子树中所含节点的值 小于等于 当前节点的值
结点右子树中所含节点的值 大于等于 当前节点的值
左子树和右子树都是二叉搜索树示例 1:
输入:root = [1,null,2,2]
输出:[2]
示例 2:输入:root = [0]
输出:[0]
众数 ,一般利用对象的特点
运用对象不能有相同的键的属性
遍历树的每个节点,判断对象中是否有该 节点val的键,有则值加一,没有则对象添加一个键为节点的val值为1
function findMode(root: TreeNode | null): number[] {
let obj={}
let getMax=function(node){
if(!node) return;
if(obj.hasOwnProperty(node.val)){
obj[node.val]++
}else{
obj[node.val]=1
}
getMax(node.left)
getMax(node.right)
}
getMax(root)
let max=0;
let nums=[]
//得到众数
for(let item in obj){
if(obj[item]>max){
max=obj[item]
}
}
//若有多个众数,依次输出
for(let item in obj){
if(obj[item]===max){
nums.push(item)
}
}
return nums
};
递归中解决
既然是搜索树,它中序遍历就是有序的。
如图:
中序遍历代码如下:
void searchBST(TreeNode* cur) {
if (cur == NULL) return ;
searchBST(cur->left); // 左
(处理节点) // 中
searchBST(cur->right); // 右
return ;
}
遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。
关键是在有序数组上的话,好搞,在树上怎么搞呢?
这就考察对树的操作了。
在二叉树:搜索树的最小绝对差 (opens new window)中我们就使用了pre指针和cur指针的技巧,这次又用上了。
弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。
而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素。
代码如下:
if (pre == NULL) { // 第一个节点
count = 1; // 频率为1
} else if (pre->val == cur->val) { // 与前一个节点数值相同
count++;
} else { // 与前一个节点数值不同
count = 1;
}
pre = cur; // 更新上一个节点
此时又有问题了,因为要求最大频率的元素集合(注意是集合,不是一个元素,可以有多个众数),如果是数组上大家一般怎么办?
应该是先遍历一遍数组,找出最大频率(maxCount),然后再重新遍历一遍数组把出现频率为maxCount的元素放进集合。(因为众数有多个)
这种方式遍历了两遍数组。
那么我们遍历两遍二叉搜索树,把众数集合算出来也是可以的。
但这里其实只需要遍历一次就可以找到所有的众数。
那么如何只遍历一遍呢?
如果 频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中(以下代码为result数组),代码如下:
if (count == maxCount) { // 如果和最大值相同,放进result中
result.push_back(cur->val);
}
是不是感觉这里有问题,result怎么能轻易就把元素放进去了呢,万一,这个maxCount此时还不是真正最大频率呢。
所以下面要做如下操作:
频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集(以下代码为result数组),因为结果集之前的元素都失效了。
if (count > maxCount) { // 如果计数大于最大值
maxCount = count; // 更新最大频率
result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了
result.push_back(cur->val);
}
关键代码都讲完了,完整代码如下:(只需要遍历一遍二叉搜索树,就求出了众数的集合)
function findMode(root: TreeNode | null): number[] {
let preNode: TreeNode | null = null;
let maxCount: number = 0;
let count: number = 0;
let resArr: number[] = [];
function traverse(root: TreeNode | null): void {
if (root === null) return;
traverse(root.left);
if (preNode === null) { // 第一个节点
count = 1;
} else if (preNode.val === root.val) {
count++;
} else {
count = 1;
}
if (count === maxCount) {
resArr.push(root.val);
} else if (count > maxCount) {
maxCount = count;
resArr.length = 0;
resArr.push(root.val);
}
preNode = root;
traverse(root.right);
}
traverse(root);
return resArr;
};
[538] 把二叉搜索树转换为累加树
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
提醒一下,二叉搜索树满足下列约束条件:
节点的左子树仅包含键 小于 节点键的节点。
节点的右子树仅包含键 大于 节点键的节点。
左右子树也必须是二叉搜索树。注意:本题和 1038: https://leetcode-cn.com/problems/binary-search-tree-to-greater-sum-tree/ 相同
示例 1:
![]()
输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]
示例 2:输入:root = [0,null,1]
输出:[1,null,1]
示例 3:输入:root = [1,0,2]
输出:[3,3,2]
示例 4:输入:root = [3,2,4,1]
输出:[7,9,4,10]
其实这就是一棵树,大家可能看起来有点别扭,换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组,也就是[20, 18, 13],是不是感觉这就简单了。
为什么变成数组就是感觉简单了呢?
因为数组大家都知道怎么遍历啊,从后向前,挨个累加就完事了,这换成了二叉搜索树,看起来就别扭了一些是不是。
那么知道如何遍历这个二叉树,也就迎刃而解了,从树中可以看出累加的顺序是右中左,所以我们需要反中序遍历这个二叉树,然后顺序累加就可以了
递归三部曲
遍历顺序如图所示:
本题依然需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。
pre指针的使用技巧,我们在二叉树:搜索树的最小绝对差 (opens new window)和二叉树:我的众数是多少? (opens new window)都提到了,这是常用的操作手段。
- 递归函数参数以及返回值
这里很明确了,不需要递归函数的返回值做什么操作了,要遍历整棵树。
同时需要定义一个全局变量pre,用来保存cur节点的前一个节点的数值,定义为int型就可以了。
代码如下:
let pre = 0; // 记录前一个节点的数值
function traversal(node)
- 确定终止条件
遇空就终止。
if (node == NULL) return;
- 确定单层递归的逻辑
注意要右中左来遍历二叉树, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。
代码如下:
traversal(root.right); //右
root.val += pre; //中
pre = root.val;
traversal(root.left); //左
递归法整体代码如下:
function convertBST(root: TreeNode | null): TreeNode | null {
let pre: number = 0;
function recur(root: TreeNode | null): void {
if (root === null) return;
recur(root.right);
root.val += pre;
pre = root.val;
recur(root.left);
}
recur(root);
return root;
};
---------------
[235] 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: 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。
示例 2:输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
普通二叉树利用回溯从底向上搜索,遇到一个节点的左子树里有p,右子树里有q,那么当前节点就是最近公共祖先。
那么本题是二叉搜索树,二叉搜索树是有序的,那得好好利用一下这个特点。
在有序树里,如果判断一个节点的左子树里有p,右子树里有q呢?
因为是有序树,所有 如果 中间节点是 q 和 p 的公共祖先,那么 中节点的数组 一定是在 [p, q]区间的。即 中节点 > p && 中节点 < q 或者 中节点 > q && 中节点 < p。
那么只要从上到下去遍历,遇到 cur节点是数值在[p, q]区间中则一定可以说明该节点cur就是q 和 p的公共祖先。 那问题来了,一定是最近公共祖先吗?
如图,我们从根节点搜索,第一次遇到 cur节点是数值在[p, q]区间中,即 节点5,此时可以说明 p 和 q 一定分别存在于 节点 5的左子树,和右子树中。
此时节点5是不是最近公共祖先? 如果 从节点5继续向左遍历,那么将错过成为q的祖先, 如果从节点5继续向右遍历则错过成为p的祖先。
所以当我们从上向下去递归遍历,第一次遇到 cur节点是数值在[p, q]区间中,那么cur就是 p和q的最近公共祖先。
理解这一点,本题就很好解了。
而递归遍历顺序,本题就不涉及到 前中后序了(这里没有中节点的处理逻辑,遍历顺序无所谓了)。
如图所示:p为节点3,q为节点5
可以看出直接按照指定的方向,就可以找到节点4,为最近公共祖先,而且不需要遍历整棵树,找到结果直接返回!
递归三部曲如下:
- 确定递归函数返回值以及参数
参数就是当前节点,以及两个结点 p、q。
返回值是要返回最近公共祖先,所以是TreeNode * 。
代码如下:
function lowestCommonAncestor(root: TreeNode | null, p: TreeNode | null, q: TreeNode | null): TreeNode | null
- 确定终止条件
遇到空返回就可以了,代码如下:
if(root === null) {
return root;
}
其实都不需要这个终止条件,因为题目中说了p、q 为不同节点且均存在于给定的二叉搜索树中。也就是说一定会找到公共祖先的,所以并不存在遇到空的情况。
- 确定单层递归的逻辑
在遍历二叉搜索树的时候就是寻找区间[p->val, q->val](注意这里是左闭又闭)
那么如果 cur->val 大于 p->val,同时 cur->val 大于q->val,那么就应该向左遍历(说明目标区间在左子树上)。
需要注意的是此时不知道p和q谁大,所以两个都要判断
代码如下:
if(root.val>p.val&&root.val>q.val) {
// 向左子树查询
return root.left = lowestCommonAncestor(root.left,p,q);
}
细心的同学会发现,在这里调用递归函数的地方,把递归函数的返回值left,直接return。
在二叉树:公共祖先问题 (opens new window)中,如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树。
搜索一条边的写法:
if (递归函数(root.left)) return ;
if (递归函数(root.right)) return ;
搜索整个树写法:
left = 递归函数(root.left);
right = 递归函数(root.right);
left与right的逻辑处理;
本题就是标准的搜索一条边的写法,遇到递归函数的返回值,如果不为空,立刻返回。
如果 cur->val 小于 p->val,同时 cur->val 小于 q->val,那么就应该向右遍历(目标区间在右子树)。
if(root.val<p.val&&root.val<q.val) {
// 向右子树查询
return root.right = lowestCommonAncestor(root.right,p,q);
}
剩下的情况,就是root节点在区间(p.val <= root.val && root.val <= q.val)或者 (q.val <= root.val && root.val <= p.val)中,那么cur就是最近公共祖先了,直接返回cur。
代码如下:
return root;
function lowestCommonAncestor(root: TreeNode | null, p: TreeNode | null, q: TreeNode | null): TreeNode | null {
if (root.val > p.val && root.val > q.val)
return lowestCommonAncestor(root.left, p, q);
if (root.val < p.val && root.val < q.val)
return lowestCommonAncestor(root.right, p, q);
return root;
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具