树的操作基本和递归密不可分。一般主要考察二叉树的操作。
所以这里简单的归纳总结下,二叉树的基本操作。
像熟练基本战斗技能一样,这样虐杀敌人才得心应手~
什么是二叉树
二叉树(Binary Tree):每个结点都有且只有两个子节点,其中子节点可以为空节点。
// 前端用类表示
function TreeNode(val) {
this.val = val;
this.left = this.right = null;
}
二叉树的前中后序遍历:递归法
二叉树的三种遍历:前、中、后序遍历,是以根节点出现的位置作为前中后的。
递归写法,超简单,三种遍历可以一起来:
function traverse(tree){
if(!tree){
return []
}
let res = []
// 这行代码的位置决定了 前中后序遍历,此处是前,放下面一行就是中,放下下面一行就是后
res.push(tree.val)
tree.left && traverse(tree.left)
tree.right && traverse(tree.right)
}
二叉树的前中后序遍历:迭代写法
迭代写法其实稍显麻烦的,也不容易理解,万一不行就掌握递归法~
前序遍历:也就是 中左右,中先碰到就先出去,然后左右是反着的,因为是栈
- 将根结点入栈
- 取出栈顶结点,将结点值 push 进结果数组
- 若栈顶结点有右孩子,则将右孩子入栈
- 若栈顶结点有左孩子,则将左孩子入栈
循环二三四步,直到栈空。
function before(tree) {
if (!tree) {
return [];
}
let res = [];
let stack = [tree];
while (stack.length) {
const cur = stack.pop();
res.push(cur.val);
cur.right && stack.push(cur.right);
cur.left && stack.push(cur.left);
}
return res;
}
后序遍历类似,但是结果数组从前开始插入
function after(tree) {
if (!tree) {
return [];
}
let res = [];
let stack = [tree];
while (stack.length) {
const cur = stack.pop();
// 这里换成unshift
res.unshift(cur.val);
// 这里两个换顺序了
cur.left && stack.push(cur.left);
cur.right && stack.push(cur.right);
}
return res;
}
前序和后序都是先处理根结点,然后处理孩子结点。
中序遍历麻烦点:
中序遍历:
function middle(root) {
let res = [];
let stack = [];
let cur = root;
while (cur || stack.length) {
while (cur) {
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
res.push(cur.val);
cur = cur.right;
}
return res;
}
二叉树的层序遍历
层序遍历利用队列,一层层的扫,注意,每一层先进去之后,长度先记录下来:
var levelOrder = function (root) {
if (!root) {
return [];
}
let res = [];
let queue = [root];
while (queue.length) {
// 当前层
let curArr = [];
const len = queue.length;
// 核心:这层的长度是一定的,因为后期queue会变化,所以这里要用for
for (let i = 0; i < len; i++) {
// 这里要有shift
const cur = queue.shift();
curArr.push(cur.val);
cur.left && queue.push(cur.left);
cur.right && queue.push(cur.right);
}
res.push(curArr.slice());
}
return res;
};
二叉树的反转
其实就是递归,每棵树都是反转的。
var invertTree = function (root) {
if (!root) {
return null;
}
let right = root.right;
let left = root.left;
root.left = invertTree(right);
root.right = invertTree(left);
return root;
};
什么是二叉搜索树
二叉搜索树(Binary Search Tree)简称 BST,别名比如排序二叉树、二叉查找树等~
树的定义总是以递归的形式出现,二叉搜索树也不例外,它的递归定义如下:
- 是一棵空树
- 是一棵由根结点、左子树、右子树组成的树,同时左子树和右子树都是二叉搜索树,且左子树上所有结点的数据域都小于等于根结点的数据域,右子树上所有结点的数据域都大于等于根结点的数据域。
简单点就是左孩子 <= 根结点 <= 右孩子
二叉搜索树的特性
- 二叉搜索树的中序遍历是有序的
- 二叉搜索树的左子树最大的节点是 左子树的最右边的点
- 二叉搜索树的右子树最小的节点是 右子树的最左边的点
二叉搜索树的查找节点
简单的就递归:
function search(root, val) {
if (!root) return null;
if (root.val === val) return root;
if (root.left) return search(root.left, val);
if (root.right) return search(root.right, val);
}
二叉搜索树的添加节点
function add(root, val) {
if (!root) return new TreeNode(val);
if (root.val === val) return root;
if (val < root.val) return add(root.left, val);
if (val > root.val) return add(root.right, val);
}
二叉搜索树的删除节点
想要删除某个结点,首先要找到这个结点。在定位结点后,我们需要考虑以下情况:
- 结点不存在,定位到了空结点。直接返回即可。
- 需要删除的目标结点没有左孩子也没有右孩子——它是一个叶子结点,删掉它不会对其它结点造成任何影响,直接删除即可。
- 需要删除的目标结点存在左子树,那么就去左子树里寻找小于目标结点值的最大结点,用这个结点覆盖掉目标结点
- 需要删除的目标结点存在右子树,那么就去右子树里寻找大于目标结点值的最小结点,用这个结点覆盖掉目标结点
- 需要删除的目标结点既有左子树、又有右子树,这时就有两种做法了:要么取左子树中值最大的结点,要么取右子树中取值最小的结点。两个结点中任取一个覆盖掉目标结点,都可以维持二叉搜索树的数据有序性
最后一种情况图示:
var deleteNode = function (root, key) {
if (!root) return null;
if (root.val === key) {
if (!root.left && !root.right) {
return null;
}
if (root.left) {
// 左边节点的最右边节点 就是 左边最大的节点
let cur = findMax(root.left);
// 覆盖的时候,根节点换值
root.val = cur.val;
// 左节点直接执行删除操作
root.left = deleteNode(root.left, cur.val);
}
// 这里必须是else 和上面的操作互斥
else if (root.right) {
// 右边节点的最左边节点 就是 左边最小的节点
let cur = findMin(root.right);
root.val = cur.val;
root.right = deleteNode(root.right, cur.val);
}
}
key < root.val && (root.left = deleteNode(root.left, key));
key > root.val && (root.right = deleteNode(root.right, key));
return root;
};
// 寻找左子树最大值
function findMax(root) {
while (root.right) {
root = root.right;
}
return root;
}
// 寻找右子树的最小值
function findMin(root) {
while (root.left) {
root = root.left;
}
return root;
}
二叉搜索树的判断
空树的判定比较简单,关键在于非空树的判定:需要递归地对非空树中的左右子树进行遍历,检验每棵子树中是否都满足 左 < 根 < 右 这样的关系(注意题中声明了不需要考虑相等情况)。
var isValidBST = function (root) {
function dfs(root, min, max) {
if (!root) {
return true;
}
if (root.val <= min || root.val >= max) {
return false;
}
return dfs(root.left, min, root.val) && dfs(root.right, root.val, max);
}
return dfs(root, -Infinity, Infinity);
};
用有序数组构建二叉搜索树
有序的数组从中间拧出来根节点即可,然后就是递归。
var sortedArrayToBST = function (nums) {
if (!nums.length) {
return null;
}
const middle = Math.floor(nums.length / 2);
let root = new TreeNode(nums[middle]);
root.left = sortedArrayToBST(nums.slice(0, middle));
root.right = sortedArrayToBST(nums.slice(middle + 1));
return root;
};
二叉搜索树下的特殊子分类:平衡树
平衡树:二叉搜索树的任意结点的左右子树高度差绝对值都不大于 1
抓住其中的三个关键字:
- 任意结点
- 左右子树高度差绝对值都不大于 1
- 二叉搜索树
结论:递归!
var isBalanced = function (root) {
if (!root) {
return true;
}
// 先设置true,有一个高度不对就设置false,并返回
let flag = true;
dfs_height(root);
function dfs_height(root) {
if (!root) {
return 0;
}
let left_height = dfs_height(root.left);
let right_height = dfs_height(root.right);
// 高度不对,就return 0 不影响高度值,层层往上,最后外层函数就收到flag了
if (Math.abs(left_height - right) > 1) {
flag = false;
return 0;
} else {
return Math.max(left_height, right_height) + 1;
}
}
return flag;
};
构建平衡树
- 二叉搜索树的中序遍历是有序的
- 有序数组构建二叉搜索树
var balanceBST = function (root) {
let nums = middle(root);
return buildTree(nums);
};
function middle(root) {
if (!root) {
return [];
}
let res = [];
root.left && res.push(...middle(root.left));
res.push(root.val);
root.right && res.push(...middle(root.right));
return res;
}
function buildTree(nums) {
if (nums.length === 0) {
return null;
}
let middle = Math.floor(nums.length / 2);
let root = new TreeNode(nums[middle]);
root.left = buildTree(nums.slice(0, middle));
root.right = buildTree(nums.slice(middle + 1));
return root;
}