系列文章导引
开源项目
本系列所有文章都将会收录到GitHub
中统一收藏与管理,欢迎ISSUE
和Star
。
GitHub传送门:Kiner算法算题记
前言
了解了二叉排序树
和AVL树
的基本概念,知道了前驱节点
和后继节点
的含义以及对于AVL树
的左旋
和右旋
操作有了基本的认知,接下来就通过一些相关的算法题深入巩固一下这些知识点。
面试题 04.06. 后继者
解题思路
题目说明了该二叉树时一个二叉排序树,那么,我们可以在中序遍历的过程中,访问到每一个节点,而当前节点的后继节点,其实就是在中序遍历的有序结果中的下一个节点,因此,我们可以记录遍历到的所有节点的上一个节点,然后在中序遍历的过程中,一旦上一个节点与目标值p匹配,则说明当前的节点就是我们要找的后继节点了。当然,这边还需要考虑后继节点在左子树和右子树的情况。
代码演示
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @return {TreeNode}
*/
// 用于记录当前节点上一个节点
let pre = null;
function inorder(root, p){
// 在中序遍历的过程中,我们可以拿到这个树的每个节点的值
// 若根节点为空,依据题意,直接返回null
if(!root) return null;
// 如果在左子树中找到了目标值,则直接返回
const nodeL = inorder(root.left, p);
if(nodeL) return nodeL;
// 如果上一个节点与p相等,说明当前节点就是后继节点,返回当前节点
if(pre === p) return root;
// 更新pre的值
pre = root;
// 如果在右子树中找到目标值,也返回
const nodeR = inorder(root.right, p);
if(nodeR) return nodeR;
// 左右子树都找不到则返回null
return null;
}
var inorderSuccessor = function(root, p) {
pre = null;
return inorder(root, p);
};
450. 删除二叉搜索树中的节点
解题思路
这题就是赤裸裸的二叉搜索树删除节点并维护二叉搜索树特性的问题,思路跟数据结构基础篇的代码实现中的删除代码一样,这里就不再赘述,直接上代码。(注意,由于删除度为2的节点可以找前驱,也可以找后继,因此答案不唯一,但只要将目标key删除并维护二叉搜索树的性质即可)。
代码演示
// @lc code=start
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
*/
function predeccessor(root: TreeNode): TreeNode {
let tmp = root.left;
while(tmp.right) tmp = tmp.right;
return tmp;
}
function deleteNode(root: TreeNode | null, key: number): TreeNode | null {
if(!root) return root;
if(key > root.val) root.right = deleteNode(root.right, key);
else if(key < root.val) root.left = deleteNode(root.left, key);
else {
if(!root.left || !root.right) {
const tmp = root.left ? root.left : root.right;
root = null;
return tmp;
} else {
const tmp = predeccessor(root);
root.val = tmp.val;
root.left = deleteNode(root.left, tmp.val);
}
}
return root;
};
// @lc code=end
1382. 将二叉搜索树变平衡
解题思路
要将一颗二叉排序树变得尽可能平衡,其实可以转换成着这样的一个问题来看:由于二叉排序树的中序遍历结果是一个有序序列,我们需要每次都尽可能的挑中间的节点作为根节点就可以保证二叉排序树尽可能平衡。
首先,使用中序遍历将每一个节点放入到一个数组中。然后,我们可以使用二分法,每次调中间的节点作为根节点,然后再递归的挑选左子树和右子树即可
// @lc code=start
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
*/
function inorder(root: TreeNode|null, nodes: TreeNode[]){
if(!root) return;
inorder(root.left, nodes);
nodes.push(root);
inorder(root.right, nodes);
}
function buildTree(nodes: TreeNode[], l: number, r: number): TreeNode|null {
if(l > r) return null;
const mid = (l + r) >> 1;
const root = nodes[mid];
root.left = buildTree(nodes, l, mid - 1);
root.right = buildTree(nodes, mid + 1, r);
return root;
}
function balanceBST(root: TreeNode | null): TreeNode | null {
// 要将一颗二叉排序树变得尽可能平衡,其实可以转换成着这样的一个问题来看:
// 由于二叉排序树的中序遍历结果是一个有序序列,我们只需要每次都尽可能的挑中间的节点
// 作为根节点就可以保证二叉排序树尽可能平衡
// 使用中序遍历将每一个节点放入到一个数组中
const nodes: TreeNode[] = [];
inorder(root, nodes);
// 使用二分法每次挑选中间的节点作为子树
return buildTree(nodes, 0, nodes.length-1);
};
// @lc code=end
108. 将有序数组转换为二叉搜索树
解题思路
这题思路跟上一题是一模一样的,还不用我们自己中序遍历获取节点数组,他直接就给我么一个有序的数字数组,我们只需要不断二分找寻中间值作为根节点,然后递归创建左右子树即可
代码演示
// @lc code=start
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
*/
function buildTree(nums: number[], l: number, r: number): TreeNode|null {
if(l > r) return null;
const mid = (l + r) >> 1;
const root = new TreeNode(nums[mid]);
root.left = buildTree(nums, l, mid - 1);
root.right = buildTree(nums, mid+1, r);
return root;
}
function sortedArrayToBST(nums: number[]): TreeNode | null {
return buildTree(nums, 0, nums.length-1);
};
// @lc code=end
98. 验证二叉搜索树
解题思路
我们要学会利用结构化思维思考问题,如果一棵树是二叉搜索树,那么他的中序遍历结果就一定是一个升序的序列,那么我们就可以在中序遍历的过程中,访问每一个值,看一下他是不是升序的。这其实跟我们判断一个的数组是否是升序的一样的,可以看成for循环遍历每一个值。
代码演示
// @lc code=start
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
*/
let pre = Number.MIN_SAFE_INTEGER;
function inorder(root: TreeNode | null){
if(!root) return true;
if(!inorder(root.left)) return false;
if(pre !== Number.MIN_SAFE_INTEGER && root.val <= pre) {
return false;
}
pre = root.val;
if(!inorder(root.right)) return false;
return true;
}
function isValidBST(root: TreeNode | null): boolean {
pre = Number.MIN_SAFE_INTEGER;
return inorder(root);
};
// @lc code=end
501. 二叉搜索树中的众数
解题思路
这道题依然是利用了二叉搜索树中序遍历时一个有序序列的特性,利用结构化思维,在中序遍历过程中,不断累加当前数字出现的次数,并与之前记录的最大出现次数做对比看当前数字是否是众数。
-
如果当前数字出现的次数与之前的最大次数相同,则说明当前数字是众数,加入到结果中。
-
如果当前数字出现的次数大于之前记录的最大次数,则说明我们之前都白费功夫了,之前记录的都不是真正的众数,我们需要重新更新最大次数,并清空之前加入到结果数组中的“伪众数”,然后将当前数字加入到结果数组中。
-
而如果当前次数小于之前最大的次数,那我们可以忽略不用处理,因为有可能后面还会有相同的次数出现,此时处理没有意义。
代码演示
// @lc code=start
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
*/
let maxCount = 0;
let curCount = 0;
let pre;
function inorder(root: TreeNode | null, ret: number[]) {
if(!root) return;
inorder(root.left, ret);
// 由于二叉搜索树的中序遍历结果是升序的,因此,相同的数肯定是紧挨着出现的
// 如果如果上一个节点的值等于当前节点的值,则累加出现次数
if(pre.val === root.val) {
curCount+=1;
} else {// 否则说明这个数时第一次出现,设置次数为一,并更新上一个节点为root
curCount = 1;
pre = root;
}
// 如果当前累计次数等于最大次数,说明我们又找到了一个众数,将这个数加入到结果数组中
if(curCount === maxCount) {
ret.push(root.val);
} else if(curCount > maxCount) {
// 如果当前累计次数大于之前记录的最大次数,则说明之前记录的都不是真正的众数,更新最大次数、
// 清空原结果数组,再重新将新的众数压入数组中
maxCount = curCount;
ret.length = 0;
ret.push(root.val);
}
inorder(root.right, ret);
}
function findMode(root: TreeNode | null): number[] {
maxCount = curCount = 0;
pre = root;
const ret: number[] = [];
inorder(root, ret);
return ret;
};
// @lc code=end
面试题 17.12. BiNode
解题思路
依然利用结构化思维,在中序遍历过程中,将每一个节点更新成链表节点
代码演示
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
*/
function inorder(root: TreeNode | null, info: {pre: TreeNode|null,head: TreeNode|null}){
if(!root) return;
inorder(root.left, info);
// 没有上一个节点,说明当前节点是头结点
if(info.pre === null) {
info.head = root;
} else {
// 将上一个节点的下一个节点指向当前节点
info.pre.right = root;
}
// 将当前节点的左节点置空
root.left = null;
// 更新上一个节点
info.pre = root;
inorder(root.right, info);
}
function convertBiNode(root: TreeNode | null): TreeNode | null {
// 用于记录上一个节点和链表头结点信息
let info = {
pre: null,
head: null
};
inorder(root, info);
return info.head;
};
剑指 Offer 33. 二叉搜索树的后序遍历序列
解题思路
我们假设输入数组就是一颗二叉搜索树的后续遍历结果,那么,既然他是一颗合法的二叉搜索树,那他的中序遍历结果必定是升序的,我们就可以对这棵树的中序遍历的过程中,验证他是否是升序的,如果是,则我们的假设是正确的,输入数组确实是二叉搜索树的后续遍历结果,否则,说明假设不成立。
在解题的过程中,需要掌握如何在一个后续遍历结果中准确找到二叉树的左子树、右子树和根节点,然后进行中序遍历。
在这里,我们无需使用实际的树形结构,根据二叉树的后序遍历的特性,我们知道他的结果是:左子树、右子树、根节点的顺序的,而又因为我们假设这个二叉树是二叉搜索树,所以,所有左子树的值都要小于根节点,右子树的值都要大于根节点,那么,我们要找到左子树、右子树、和根节点就很简单了:
- 根节点:序列最后一个节点就是根节点
- 左子树:从序列左侧左侧第一个大于根节点的节点的前一个节点为止就是左子树
- 右子树:从第一个大于根节点的节点开始到倒数第二个节点是右子树
确定了三个关键的区间,我们就可以对这个区间进行中序遍历,并在遍历的过程中校验其是否升序了。
代码演示
let pre;
function inorder(postorder: number[], l: number, r: number): boolean {
if(l > r) return true;
// 现在后续遍历序列中找到左右子树的区间范围,由于后续遍历是:左 右 根
let idx = 0;
// 如果idx所对应节点的值比根节点小,则右移,直到遇到第一个比根节点大的值为止,idx就停留到了右子树根节点的位置
while(postorder[idx] < postorder[r]) ++idx;
if(!inorder(postorder, l, idx - 1)) return false;
// 如果pre不是-1说明不是第一个节点,并且上一个值如果大于根节点的值,说明中序遍历结果不是一个升序的序列
// 也就说明输入的后续遍历结果不是一个二叉搜索树
if(pre !== -1 && postorder[pre] > postorder[r]) return false;
// 将上一个节点更新为当前根节点
pre = r;
if(!inorder(postorder, idx, r - 1)) return false;
return true;
}
function verifyPostorder(postorder: number[]): boolean {
// 代表当前节点上一个节点的索引
pre = -1;
return inorder(postorder, 0, postorder.length-1);
};
1008. 前序遍历构造二叉搜索树
解题思路
这道题本质上解题思路与上一题差不多,只是从后续遍历变成了前序遍历而已。
代码演示
// @lc code=start
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
*/
function buildTree(nums: number[], l: number, r: number): TreeNode|null {
// 由于输入的数组时二叉搜索树的前序遍历结果,因此他应该是:根节点、左子树、右子树的结构
if(l > r) return null;
// 查找右子树的第一个节点,方便区分出左子树区间和右子树区间,
let idx = l + 1;
// 如果节点值小于根节点,那么一定是属于左子树,继续往后走,直到找到第一个大于根节点的值,就是右子树的第一个节点
while(idx <= r && nums[idx] < nums[l]) ++idx;
// 首先创建根节点
const root = new TreeNode(nums[l]);
// 递归创建左子树
root.left = buildTree(nums, l + 1, idx - 1);
// 递归创建右子树
root.right = buildTree(nums, idx, r);
return root;
}
function bstFromPreorder(preorder: number[]): TreeNode | null {
return buildTree(preorder, 0, preorder.length - 1);
};
// @lc code=end
面试题 04.09. 二叉搜索树序列
解题思路
这道题要我们找出所有可以生成目标二叉搜索树的数组排列,那么,我们先从最简单的情况开始分析:
代码演示
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
*/
function mergeSequences(
lArr: number[], // 左子树可能的序列集合
lIdx: number, // 已经从左子树中挑选了几个元素参与最终的排列组合
rArr: number[], // 右子树可能的序列集合
rIdx: number, // 已经从右子树中挑选了几个元素参与最终的排列组合
buff: number[], // 缓冲区,用于临时存储一组排列组合
res: number[][]// 结果数组
) {
// 当我们已经将左右子树序列集合中的所有数都尝试组合过一遍之后,当前缓冲区当中存放的就是我们要的答案
if(lIdx === lArr.length && rIdx === rArr.length) {
// 注意:由于数组时引用,如果直接使用res.push(buff)的话,由于数组的引用相同,会与后续的操作发生冲突,导致无法得到正确的解
// 因此我们应该拷贝出一份数据出来放到结果数组中
res.push([...buff]);
return;
}
// 如果左子树序列集合还有元素没有被挑选,
if(lIdx < lArr.length) {
// 则将第一个没被挑选的元素加入到缓冲区中
buff.push(lArr[lIdx]);
// 然后再此基础上尝试继续匹配后续的元素(此时,左子树序列集合中已经多了一个被使用的元素,因此lIdx要加1)
mergeSequences(lArr, lIdx + 1, rArr, rIdx, buff, res);
// 在以当前的数字为起点继续匹配完成后,我们需要回溯,把当前元素从缓冲区中移除掉,消除这一轮求解的影响,方便下一轮的查找
buff.pop();
}
// 同理在右子树序列集合中挑选元素
if(rIdx < rArr.length) {
buff.push(rArr[rIdx]);
mergeSequences(lArr, lIdx, rArr, rIdx + 1, buff, res);
buff.pop();
}
}
function BSTSequences(root: TreeNode | null): number[][] {
let res: number[][] = [];
// 如果树为空,则返回[[]]
if(!root) {
res.push([]);
return res;
}
// 先递归查找左右子树可能组合的序列
const lArr = BSTSequences(root.left);
const rArr = BSTSequences(root.right);
// 然后将他们挨个排列组合
for(const l of lArr) {
for(const r of rArr) {
// 缓冲区,用于存放可能的某一组解
const buff: number[] = [];
// 根节点可以直接压入到缓冲区中,因为根节点必定要首先插入
buff.push(root.val);
// 将左右子树可能组合的序列按照排列组合的方式合并成一组组答案
mergeSequences(l, 0, r, 0, buff, res);
}
}
return res;
};