二叉搜索树1
Binary Search Tree,后文简写 BST
BST 的特性:
1、对于 BST 的每一个节点node
,左子树节点的值都比node
的值要小,右子树节点的值都比node
的值大。
2、对于 BST 的每一个节点node
,它的左侧子树和右侧子树都是 BST。
二叉搜索树并不算复杂,但它构建起了数据结构领域的半壁*,直接基于 BST 的数据结构有 AVL 树,红黑树等等,拥有了自平衡性质,可以提供 logN 级别的增删查改效率;还有 B+ 树,线段树等结构都是基于 BST 的思想来设计的。
从做算法题的角度来看 BST,除了它的定义,还有一个重要的性质:BST 的中序遍历结果是有序的(升序)。
也就是说,如果输入一棵 BST,以下代码可以将 BST 中每个节点的值升序打印出来:
void traverse(TreeNode root) {
if (root == null) return;
traverse(root.left);
// 中序遍历代码位置
print(root.val);
traverse(root.right);
}
leetcode.230 二叉搜索树第k小的元素
本题比较容易想到的方法就是利用二叉搜索树的这个性质:中序遍历可得升序排列
那么套用中序排列模板,因为要找到第k个最小的数,那么对每次递归中都把次数加一,当次数==k时,对应的值就是答案了
int kthSmallest(TreeNode root, int k) {
// 利用 BST 的中序遍历特性
traverse(root, k);
return res;
}
// 记录结果
int res = 0;
// 记录当前元素的排名
int rank = 0;
void traverse(TreeNode root, int k) {
if (root == null) {
return;
}
traverse(root.left, k);
/* 中序遍历代码位置 */
rank++;
if (k == rank) {
// 找到第 k 小的元素
res = root.val;
return;
}
/*****************/
traverse(root.right, k);
}
这里关于内部函数的用法值得记住。
如果按照我们刚才说的方法,利用「BST 中序遍历就是升序排序结果」这个性质,每次寻找第k
小的元素都要中序遍历一次,最坏的时间复杂度是O(N)
,N
是 BST 的节点个数。
要知道 BST 性质是非常牛逼的,像红黑树这种改良的自平衡 BST,增删查改都是O(logN)
的复杂度,让你算一个第k
小元素,时间复杂度竟然要O(N)
,有点低效了。
所以说,计算第k
小元素,最好的算法肯定也是对数级别的复杂度,不过这个依赖于 BST 节点记录的信息有多少。
我们想一下 BST 的操作为什么这么高效?就拿搜索某一个元素来说,BST 能够在对数时间找到该元素的根本原因还是在 BST 的定义里,左子树小右子树大嘛,所以每个节点都可以通过对比自身的值判断去左子树还是右子树搜索目标值,从而避免了全树遍历,达到对数级复杂度。
那么回到这个问题,想找到第k
小的元素,或者说找到排名为k
的元素,如果想达到对数级复杂度,关键也在于每个节点得知道他自己排第几。
比如说你让我查找排名为k
的元素,当前节点知道自己排名第m
,那么我可以比较m
和k
的大小:
1、如果m == k
,显然就是找到了第k
个元素,返回当前节点就行了。
2、如果k < m
,那说明排名第k
的元素在左子树,所以可以去左子树搜索第k
个元素。
3、如果k > m
,那说明排名第k
的元素在右子树,所以可以去右子树搜索第k - m - 1
个元素。
这样就可以将时间复杂度降到O(logN)
了。
那么,如何让每一个节点知道自己的排名呢?
这就是我们之前说的,需要在二叉树节点中维护额外信息。每个节点需要记录,以自己为根的这棵二叉树有多少个节点。
也就是说,我们TreeNode
中的字段应该如下:
class TreeNode {
int val;
// 以该节点为根的树的节点总数
int size;
TreeNode left;
TreeNode right;
}
有了size
字段,外加 BST 节点左小右大的性质,对于每个节点node
就可以通过node.left
推导出node
的排名,从而做到我们刚才说到的对数级算法。
当然,size
字段需要在增删元素的时候需要被正确维护,力扣提供的TreeNode
是没有size
这个字段的,所以我们这道题就只能利用 BST 中序遍历的特性实现了,但是我们上面说到的优化思路是 BST 的常见操作,还是有必要理解的。
leetcode538.把二叉搜索树转化为累加数
题目应该不难理解,比如图中的节点 5,转化成累加树的话,比 5 大的节点有 6,7,8,加上 5 本身,所以累加树上这个节点的值应该是 5+6+7+8=26。
TreeNode convertBST(TreeNode root);
按照二叉树的通用思路,需要思考每个节点应该做什么,但是这道题上很难想到什么思路。
BST 的每个节点左小右大,这似乎是一个有用的信息,既然累加和是计算大于等于当前值的所有元素之和,那么每个节点都去计算右子树的和,不就行了吗?
这是不行的。对于一个节点来说,确实右子树都是比它大的元素,但问题是它的父节点也可能是比它大的元素呀?这个没法确定的,我们又没有触达父节点的指针,所以二叉树的通用思路在这里用不了。
能利用的还是三点:1.二叉树 2.二叉树的性质 3.中序遍历可以得到升序排列
思考一下给出的感觉就是要得到排列:从那个点之后的值都要累加起来,这个实现起来很困难
所以可以换一种方法:如果能得到降序排列,即以一种方法递归得到降序排列,那就可以维护一个值,从递归中累加即可。
那么现在的关键在于如何获得二叉树的降序排列:
根据二叉搜索树的性质和中序遍历的特点(左根右)可得升序排列
那么就改变一下遍历的形式,改成右根左,那这样就出来了降序排列
只要把递归顺序改一下就行了:
void traverse(TreeNode root) {
if (root == null) return;
// 先递归遍历右子树
traverse(root.right);
// 中序遍历代码位置
print(root.val);
// 后递归遍历左子树
traverse(root.left);
}
这段代码可以从大到小降序打印 BST 节点的值,如果维护一个外部累加变量sum
,然后把sum
赋值给 BST 中的每一个节点,不就将 BST 转化成累加树了吗?
class Solution {
public TreeNode convertBST(TreeNode root) {
return traverse(root);
}
int sum;
TreeNode traverse(TreeNode root){
if(root==null){return null;}
traverse(root.right);
sum=sum+root.val;
root.val=sum;
traverse(root.left);
return root;
}
}
这里除了上面说的那些点之外
关键在于一些写法上,比如把这个操作函数写成内部函数,并且这里还有int sum;写的地方,很重要。
核心还是 BST 的中序遍历特性,只不过我们修改了递归顺序,降序遍历 BST 的元素值,从而契合题目累加树的要求。