0. 框架模型
基本的框架已经在基础知识介绍过了,比如前中后序的递归,非递归遍历,层次遍历等。
a. 向左右子树要信息的框架
我们的这款框架中要列出所有的可能性,比如说和根节点有关 和 跟节点无关两种。下面介绍解题思路步骤,
- 首先根据二叉树递归解法的前提,假定可以向左右子树要的信息
- 考虑得到答案的可能性?列出可能性,和根节点有关么?
- 根据可能性,确定左右子树分别需要什么信息
- 信息汇总,求全集定信息体
- 写递归
写递归时的步骤如下,
- 定义好Info结构体
- 递归函数process的返回Info
- base case也就是空树返回
- 从左右子树要信息
- 根据列出的可能性,把Info中所有的信息加工出来,如果base case处理返回是null节点,这时候就要注意判空!
b. 一次性遍历一层的层次遍历
在一些问题中我们希望,如果能一次性的把一层的节点遍历完,把这一层节点暂存在队列中再作处理。
private void widthOfBinaryTree(TreeNode root) {
// 层次遍历中的,一次能遍历一层的框架
if (root == null){
return;
}
// 选用双端队列实现队列,可以做到对队首和队尾元素的选取
Deque<TreeNode> queue = new LinkedList<NodeWithIndex>();
queue.offer(root);
int size = 0;
while (!queue.isEmpty()){
// 统计这一层节点的数目
size = queue.size();
// 一次性的遍历完
for (int i = 0; i < size; i++){
cur = queue.poll();
if (cur.left!=null){
queue.offer(cur.left);
}
if (cur.right!=null){
queue.offer(cur.right);
}
}
// 做处理
dosomething;
}
} c. BST的框架
查找框架不涉及到修改二叉树,返回值为void
void BST(TreeNode root, int target) {
if (root.val == target)
// 找到目标,做点什么
// 类比于有序数组的二分查找,root.val就是arr[mid]
if (root.val < target)
BST(root.right, target);
if (root.val > target)
BST(root.left, target);
} 插入和删除涉及到修改二叉树,返回值为TreeNiode,一般返回修改后的root node
TreeNode insertIntoBST(TreeNode root, int val) {
// 找到空位置插入新节点
if (root == null) return new TreeNode(val);
if (root.val < val)
root.right = insertIntoBST(root.right, val);
if (root.val > val)
root.left = insertIntoBST(root.left, val);
return root;
}
TreeNode deleteNode(TreeNode root, int key) {
if (root == null){
// 如果没有找到要删除的节点
return;
}
if (root.val == key) {
// 找到啦,进行删除
} else if (root.val > key) {
// 去左子树找
root.left = deleteNode(root.left, key);
} else if (root.val < key) {
// 去右子树找
root.right = deleteNode(root.right, key);
}
return root;
} 1. 递归框架
1.1 采用基本框架
二叉树镜像
ZJ-27 : 请完成一个函数,输入一个二叉树,该函数输出它的镜像。
解题思路:
递归和遍历都是一样的思路,遍历每个点,交换它的左右子树。
时间复杂度:O(N);空间复杂度:O(N)
方法一: 递归方法
public TreeNode mirrorTree(TreeNode root) {
if(root == null){
return root;
}
process(root);
return root;
}
public static void process(TreeNode node){
if(node == null){
return;
}
// 方法一: 先序遍历
// 根据递归序,第一次来到这个节点,交换左右子树
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
process(node.left);
process(node.right);
// 方法二: 中序遍历
// 递归序,第二次来到这个节点,注意是遍历两次left节点,因为交换过了
process(node.left);
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
process(node.left);
// 方法三: 后序遍历
process(node.left);
process(node.right);
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
} 方法二: 非递归, 和递归思路一样,人工压栈,但还是每个节点交换左右子数
// 层次遍历
public TreeNode mirrorTree(TreeNode root) {
if(root == null){
return root;
}
// 层次遍历的方法
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
// 遍历队列
while(!queue.isEmpty()){
TreeNode cur = queue.poll();
if(cur.left!=null){
queue.offer(cur.left);
}
if(cur.right!=null){
queue.offer(cur.right);
}
// 交换左右子树
TreeNode temp = cur.left;
cur.left = cur.right;
cur.right = temp;
}
return root;
}
// 非递归中序遍历
public TreeNode mirrorTree(TreeNode root) {
if(root == null){
return root;
}
// 非递归的中序遍历
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while(cur!=null || !stack.isEmpty()){
if(cur!=null){
stack.push(cur);
cur = cur.left;
}else{
TreeNode curTemp = stack.pop();
// 先记录原本的右子树
cur = curTemp.right;
// 交换左右子树
TreeNode temp = curTemp.left;
curTemp.left = curTemp.right;
curTemp.right = temp;
// ***** 也可以这样 *****
cur = stack.pop();
// 交换左右子树
TreeNode temp = cur.left;
cur.left = cur.right;
cur.right = temp;
// 再次左分解,左子树,其实是右子树
cur = cur.left;
}
}
return root;
} 1.2 单独考虑根节点
直径的二叉树
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
示例 :
给定二叉树
1
/ \
2 3
/ \
4 5 返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
注意:两结点之间的路径长度是以它们之间边的数目表示。
解题思路:
注意题目中的提示,这个最大路径可能穿过也可能不穿过根节点,也就是说得到答案可能性有多种,路径的最大值可能不需要经过根节点计算,所以我们需要一个全局变量记录最大值。
如上图所示,和根节点无关,答案是来自右子树的最大距离。假设我们知道对于该节点的左儿子向下遍历经过最多的节点数 L(即以左儿子为根的子树的深度) 和其右儿子向下遍历经过最多的节点数 R (即以右儿子为根的子树的深度),那么以该节点为起点的路径经过节点数的最大值即为 L+R+1 。我们得到经过该节点的最多路径节点数是dNode,那么其直径就是dNode - 1,因为路径长度是以它们之间边的数目表示,边和点的数目正好差一。
我们想要找到最大直接,就要找到整颗树上对每个点计算其经过的最大经过的节点数,用一个全局变量去找到最大值,这里值得注意的就是,根节点并不代表着最大值。
最后的算法流程为:
我们定义一个递归函数 depth(node) 计算最大经过的节点数 ,函数返回该节点为根的深度。先递归调用左儿子和右儿子求得它们为根的树的深度 L 和 R ,则该节点为根的子树的深度即为max(L,R)+1,该节点的最大经过的节点数为L+R+1。
递归搜索每个节点并设一个全局变量ans,记录并比较每个节点的最大经过节点数,最后找到最大的减一返回。
class Solution {
// 假设初始化一个节点,不要随意使用static变量
int ans;
public int diameterOfBinaryTree(TreeNode root) {
ans = 1;
if (root == null){
return 0;
}
process(root);
// 最长路径就是最大节点数-1
return ans - 1;
}
private int process(TreeNode root){
// base case: 空节点返回0
if (root == null){
return 0;
}
// 深度代表了最大经历的节点数目
int leftInfo = process(root.left);
int rightInfo = process(root.right);
// 根节点的信息是,左数目+右数目+1
// 最大节点数就是左节点数+右节点数+自己(1)
// 再比较每个节点的信息,找到最大值
ans = Math.max(ans, leftInfo+rightInfo+1);
// 求根节点的深度, 左右深度最大值 + 1
return Math.max(leftInfo, rightInfo) + 1;
}
} 1.3 复杂度优化
完全二叉树的节点个数
力扣222,给出一个完全二叉树,求出该树的节点个数。
说明:
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。
示例:输入:
1
/
2 3
/ \ /
4 5 6输出: 6
解题思路:
这道题要结合完全二叉树的性质来做,时间复杂度要小于O(N)。完全二叉树(Complete Binary Tree)的定义可以理解为,它是一颗满二叉树或者正在依次逐层成为满二叉树,这里的依次是指从左子树到右子树。它的性质就是,一颗完全二叉树的左右子树,至少有一颗是满二叉树,下图参考labuladong公众号
public static int countNodes(TreeNode root) {
TreeNode leftNode = root;
TreeNode rightNode = root;
int leftHeight = 0;
int rightHeight = 0;
// 统计左子树的高度
while (leftNode != null){
leftNode = leftNode.left;
leftHeight++;
}
// 统计右子树的高度
while (rightNode != null){
rightNode = rightNode.right;
rightHeight++;
}
// 如果是左右等高,则是满二叉树
if (rightHeight == leftHeight){
return (int)Math.pow(2, rightHeight)-1;
}
// 如果不等高,根左右累加起来
// 复杂度计算,只会有一个递归到底,所以复杂度是log(N)
// 每次递归时,计算复杂度是log(N)
// 所以最后的复杂度是,(h)ˆ2,h=log(N)
return 1 + countNodes(root.left) + countNodes(root.right);
} 分析上述代码的时间复杂度,最后一行的左右子树递归中,只会有一个递归到底,另一个会因为是满树而提前返回。比如说root.left是满树,关于它的递归函数,只会一次执行中间的判断树的高度的代码,从而证明他是满树直接通过公式计算返回节点数目,其复杂度是O(log(N))。
root.right不是满树的话,最多会递归h层,h就是树的高度,每层会执行log(N)的操作,因此复杂是O(log(h)ˆ2),是远远低于O(N)的复杂度。所以说,完全二叉树这个概念还是有它存在的原因的,不仅适用于数组实现二叉堆,而且连计算节点总数这种看起来简单的操作都有高效的算法实现。
备注:
// 普通二叉树计算节点个数,复杂度是O(N)
public int countNodes(TreeNode root) {
if (root == null) return 0;
return 1 + countNodes(root.left) + countNodes(root.right);
}
// 如果是一棵满二叉树,节点总数就和树的高度呈指数关系,时间复杂度 O(logN)
public int countNodes(TreeNode root) {
int h = 0;
// 计算树的高度
while (root != null) {
root = root.left;
h++;
}
// 节点总数就是 2^h - 1
return (int)Math.pow(2, h) - 1;
} 1.4 层次遍历是更好解法
BT的最大宽度
给定一个二叉树,编写一个函数来获取这个树的最大宽度。树的宽度是所有层中的最大宽度。这个二叉树与满二叉树(full binary tree)结构相同,但一些节点为空。
每一层的宽度被定义为两个端点(该层最左和最右的非空节点,两端点间的null节点也计入长度)之间的长度。
示例 1:
输入:
1
/ \
3 2
/ \ \
5 3 9 输出: 4
解释: 最大值出现在树的第 3 层,宽度为 4 (5,3,null,9)。
解题思路:
设想给每个节点编号,其子节点的编号就是left = 2*index, right = 2*index+1,再利用一次性宽度的层次遍历框架,计算这一层的第一个节点和最后一个节点的差值就是这层的宽度,最后再找到所有层中的最大宽度即可。
private static int widthOfBinaryTree(TreeNode root) {
// 层次遍历中的,一次能遍历一层的框架
if (root == null){
return 0;
}
// 选用双端队列实现队列,可以做到对队首和队尾元素的选取
Deque<NodeWithIndex> queue = new LinkedList<NodeWithIndex>();
queue.offer(new NodeWithIndex(root, 1));
NodeWithIndex cur = null;
int index = 0;
int size = 0;
int width = 0;
while (!queue.isEmpty()){
// 最大宽度就是,找到每层中最后节点减去首节点再加一的最大值
width = Math.max(width, queue.peekLast().index - queue.peekFirst().index + 1);
size = queue.size();
for (int i = 0; i < size; i++){
cur = queue.poll();
root = cur.node;
index = cur.index;
if (root.left!=null){
queue.offer(new NodeWithIndex(root.left, 2*index));
}
if (root.right!=null){
queue.offer(new NodeWithIndex(root.right, 2*index + 1));
}
}
//width = Math.max(width, queue.peekLast().index - queue.peekFirst().index + 1);
}
return width;
}
// 对现有节点改造下,加入一个index的新属性
private static class NodeWithIndex{
TreeNode node;
int index;
public NodeWithIndex(TreeNode node, int index) {
this.node = node;
this.index = index;
}
} 此外也可以用深度优先遍历解决,也需要给每个节点编号,并需要一个depth来记录当前节点的深度,并巧妙的利用一个map来记录,当每层的第一次加入节点的编号,因为是先序框架,所以第一个节点一定是最左边的节点,在后序的递归过程中,可以查询表,让当前层的所有节点依次去计算和这个最左节点的距离,找到最大值就是最大宽度。
// 查询表
static HashMap<Integer, Integer> map;
static int ans;
private static int widthOfBinaryTreeRecursive(TreeNode root) {
if (root == null){
return 0;
}
map = new HashMap<Integer, Integer>();
// 根节点
map.put(0, 0);
ans = 0;
dfs(root, 0, 0);
return ans;
}
private static void dfs(TreeNode root, int depth, int pos){
if (root == null){
return;
}
// 如果depth不存在,就按照方程加入pos,保证加入的都是每一层的最左pos
map.computeIfAbsent(depth, x->pos);
// 最奇妙的地方,每次查询depth得到当前层的最左节点!
int left = map.get(depth);
ans = Math.max(ans, pos - left + 1);
dfs(root.left, depth+1, 2*pos);
dfs(root.right, depth+1, 2*pos+1);
} 2. 序列化框架
参考资料,东哥公共号lobuladong
序列化和反序列化的目的,以某种固定格式组织字符串,使得数据可以独立于编程语言。那么假设现在有一棵用 Java 实现的二叉树,我想把它序列化字符串,然后用 C++ 读取这棵并还原这棵二叉树的结构,怎么办?这就需要对二叉树进行序列化和反序列化了。
可以解决题目LC297,给你输入一棵二叉树的根节点 root,要求你实现如下一个类:
public class Codec {
// 把一棵二叉树序列化成字符串
public String serialize(TreeNode root) {}
// 把字符串反序列化成二叉树
public TreeNode deserialize(String data) {}
} 我们可以用 serialize 方法将二叉树序列化成字符串,用 deserialize 方法将序列化的字符串反序列化成二叉树,至于以什么格式序列化和反序列化,这个完全由你决定。
serialize 方法也许会把它序列化成字符串 2,1,#,6,3,#,#,其中 # 表示 null 指针,,表示分隔符,代表一个值的结束,必须有分隔符,它可以唯一的区分一棵二叉树,否则在序列中123,根节点是12还是23呢?把这个字符串再输入 deserialize 方法,依然可以还原出这棵二叉树。也就是说,这两个方***成对儿使用,你只要保证他俩能够自洽就行了。
PS:一般语境下,单单前序遍历结果是不能还原二叉树结构的,因为缺少空指针的信息,至少要得到前、中、后序遍历中的两种才能还原二叉树。但是这里的 node 列表包含空指针的信息,所以只使用 node 列表就可以还原二叉树。
2.1 前中后序遍历
前序方法:
序列化的方法很简单,就是在前序遍历的框架上加上添加字符的部分,注意在base case时当节点为空,添加 # 。采用StringBuilder()可以更高效稳定的拼接字符串。
// 把一棵二叉树序列化成字符串
private static String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
preTraversal(root, sb);
return sb.toString();
}
// 序列化是需要递归的,需要一个辅助函数
private static void preTraversal(TreeNode root, StringBuilder sb){
// base case:
if (root == null){
sb.append("#").append(",");
return;
}
// 加入字符,用分隔符分开
// 前序遍历框架
sb.append(root.val).append(",");
preTraversal(root.left, sb);
preTraversal(root.right, sb);
} 反序列化的方法,参考接收到的序列化后的结果,首先根据分隔符就可以得到节点列表,观察就可以发现第一个节点就是根节点,这样我们就可以借助一个队列采用递归的方式恢复出这个二叉树。下图作者是dong哥,其图片来自其公众号,水印是labuladong。
先确定根节点 root,然后遵循前序遍历的规则,递归生成左右子树。这里巧妙的是,因为序列化的时候记录了空节点,所以当递归恢复叶子节点时,递归函数会在执行到叶子节点的左右子树时,返回空,这样叶子节点就只会返回它本身。序列化时是采用前序遍历,反序列化时也采用根,左孩子,右孩子的顺序递归恢复,只需要额外的把队列中的元素从头到尾依次弹出。
// 把字符串反序列化成二叉树
private static TreeNode deserialize(String data) {
// 把接收到的字符串,先分割再依次装入到队列中
Queue<String> str = new LinkedList<String>();
for(String string : data.split(",")){
str.offer(string);
}
return deserialize_process(str);
}
// 反序列化是需要递归的,需要一个辅助函数
private static TreeNode deserialize_process(Queue<String> str){
// 力扣做题防止空输入
if (str.isEmpty()){
return null;
}
String cur = str.poll();
// base case:
if (cur.equals("#")){
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(cur));
root.left = deserialize_process(str);
root.right = deserialize_process(str);
return root;
} 后序方法:
序列化的方法和前序的一致,注意添加字符的顺序放在遍历之后,根据递归序,根节点会最后加入序列。
// Encodes a tree to a single string.
private static String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
serialize_process(root, sb);
return sb.toString();
}
// 后序遍历的序列化辅助递归函数
private static void serialize_process(TreeNode root, StringBuilder sb){
// base case:
if (root == null){
sb.append("#").append(",");
return;
}
serialize_process(root.left, sb);
serialize_process(root.right, sb);
// 后序遍历
sb.append(root.val).append(",");
} 反序列化的方法,要根据序列化的顺序做出改变,因为根节点在序列的最后,我们应该从后往前取出列表元素,先用最后一个元素构造 root,然后递归调用生成 root 的左右子树。注意,根据上图,从后往前在 nodes 列表中取元素,一定要先构造 root.right 子树,后构造 root.left 子树。
我们在从序列化的结果恢复成节点列表时,采用栈这个容器,就可以保证本来在末尾的根节点可以第一个被处理,从而完成从后往前的从列表中取元素。
// Decodes your encoded data to tree.
// 反序列化
private static TreeNode deserialize(String data) {
// 先把字符串分割提取,针对后序遍历要用栈,因为根节点是最后加入的
Deque<String> stack = new ArrayDeque<String>();
for (String str : data.split(",")){
stack.push(str);
}
return deserialize_process(stack);
}
// 后序遍历反序列化需要借助递归辅助函数
private static TreeNode deserialize_process(Deque<String> stack){
if (stack.isEmpty()){
return null;
}
String cur = stack.pop();
if (cur.equals("#")){
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(cur));
// 因为后序遍历的顺序,所以递归先建立出的是右子树,再是左子树
root.right = deserialize_process(stack);
root.left = deserialize_process(stack);
return root;
} 中序方法:
中序方法无法做到反序列化,原因是,不同于前序和后序遍历,中序遍历的根节点在中间无法定位到他的具***置。
至此可以小结下,这种生成一个二叉树的模版就是,先要生成根节点,再递归的产生左右子树。
// base case,基于某个条件返回null
if (cur.equals("#")){
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(cur));
// 递归建立出右子树,左子树
root.right = deserialize_process(stack);
root.left = deserialize_process(stack);
return root; 2.2 层次遍历
序列化的方法,和前后序遍历很相似,就是在遍历节点的过程中加入转换字符的功能,在标准的二叉树层级遍历框架中,从上到下,从左到右打印每一层二叉树节点的值,可以看到,队列 q 中不会存在null指针。不过我们在反序列化的过程中是需要记录空指针null的,所以可以把标准的层级遍历框架略作修改:
// 把一棵树序列化,再逆序列化,采用层次遍历模式
// 借助层次遍历的框架,需要一个队列
private static String serialize(TreeNode root) {
// 如果是空树,直接返回
if (root == null){
return new String("#,");
}
StringBuilder sb = new StringBuilder();
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
// 注意根节点要单独的在这里要添加字符!
sb.append(root.val).append(",");
TreeNode cur = null;
while (!queue.isEmpty()){
cur = queue.poll();
if (cur.left!=null){
sb.append(cur.left.val).append(",");
queue.offer(cur.left);
}else {
sb.append("#,");
}
if (cur.right!=null){
sb.append(cur.right.val).append(",");
queue.offer(cur.right);
}else{
sb.append("#,");
}
}
return sb.toString();
} 反序列化方法,类比前后遍历递归的恢复树,层次遍历方法没有递归时的系统栈,因此需要自己额外建立一个队列帮助自己。
可以看到,每一个非空节点都会对应两个子节点,那么反序列化的思路也是用队列进行层级遍历,同时用索引 i 记录对应子节点的位置,序列化时操作二叉树节点 TreeNode,反序列化时我们的函数在操作 nodes[i]。
// 反序列化
// 序列化是在二叉树上层次遍历,反序列化是在数组上利用下标i遍历左右节点
// 反序列化要借助辅助队列
private static TreeNode deserialize(String data) {
// 根据分隔符获得节点数组
String[] nodes = data.split(",");
int i = 0;
// 第一个元素就是根节点
TreeNode root = generateNodeByStr(nodes[i++]);
Queue<TreeNode> queue = new LinkedList<TreeNode>();
TreeNode cur = null;
// 头节点不是空是,才加入队列
if (root!=null){
queue.offer(root);
}
while(!queue.isEmpty()){
cur = queue.poll();
// 生产左右子树
TreeNode left = generateNodeByStr(nodes[i++]);
TreeNode right = generateNodeByStr(nodes[i++]);
cur.left = left;
cur.right = right;
// 检查左右子树
if (left!=null){
queue.offer(left);
}
if (right!=null){
queue.offer(right);
}
}
return root;
}
private static TreeNode generateNodeByStr(String str){
if (str.equals("#")){
return null;
}else {
return new TreeNode(Integer.parseInt(str));
}
} 2.3 应用
寻找重复的子树
给定一棵二叉树,返回所有重复的子树。对于同一类的重复子树,你只需要返回其中任意一棵的根结点即可。两棵树重复是指它们具有相同的结构以及相同的结点值。
以下图的树为例,应该返回节点4和2,分别代表子树[4]和[2,4]。
解题思路,二叉树的老套路,先思考,对于某一个节点,它应该做什么。如果你想知道以自己为根的子树是不是重复的,是否应该被加入结果列表中,你需要知道什么信息?
你需要知道以下两点:
1、以我为根的这棵二叉树(子树)长啥样?
2、以其他节点为根的子树都长啥样?
针对第一个问题,可以采用之前学习到的序列化的知识,可以把这颗二叉树唯一的序列化表示。可以采用后序框架,此时可以完整的描述以“我”为根的这个树的序列化,至于按什么顺序拼接序列化的字符串,这个随意。
针对第二个问题,可以借助外部查询表,因为是找到重复的子树,所以表中重复的元素就是想要的答案,此外还要注意,题目要求只把重复的节点加入一次,所以只需要一个小技巧,就是当加入第二次时,才把元素加入答案容器中。
static HashMap<String, Integer> memo;
static List<TreeNode> list;
private static List<TreeNode> findDuplicateSubtrees(TreeNode root) {
// 建立外部变量
memo = new HashMap<String, Integer>();
list = new ArrayList();
traversal(root);
return list;
}
private static String traversal(TreeNode root){
// base case:
if (root == null){
return "#";
}
// 选择后序遍历
String res = root.val + "," + traversal(root.left) + "," + traversal(root.right);
if (!memo.containsKey(res)){
memo.put(res, 1);
}else {
int times = memo.get(res);
// 说明已经一次记录,这次就会重复第一次
if (times == 1){
list.add(root);
}
memo.put(res, times+1);
}
// 可以简化查询代码,getOrDefault方法就是没有查询到key的记录时,返回用户设置的defaultValue
memo.put(res, memo.getOrDefault(res, 0) + 1);
if (memo.get(res) == 2)
list.add(root);
return res;
} 3. 重建二叉树
4. 二叉搜索树
二叉搜索树(BST)的定义:
1、对于 BST 的每一个节点node,左子树节点的值都比node的值要小,右子树节点的值都比node的值大。
2、对于 BST 的每一个节点node,它的左侧子树和右侧子树都是 BST。
此外还有一个重要的性质:BST 的中序遍历结果是有序的(升序)。
4.1 增删改查
BST的第K大节点
力扣题号: 54 - 给定一棵二叉搜索树,请找出其中第k大的节点。
解题思路:
正常的中序遍历是,左中右,遍历下来的数组是从小到大,而题目要求是第K大,我们可以自己加工一个中序遍历是,右中左,这样数组是从大到小,遍历到第K个就是想要的答案。
public int kthLargest(TreeNode root, int k) {
// 自己构造一种遍历方法,右-中-左
// 用非递归的方法中序遍历二叉树
TreeNode cur = root;
int index = 1;
int KthValue = -1;
Deque<TreeNode> stack = new ArrayDeque<>();
while(cur!=null || !stack.isEmpty()){
// 不停地压入右孩子
if(cur!=null){
stack.push(cur);
cur = cur.right;
}else{
// 弹出栈顶
cur = stack.pop();
if(index == k){
KthValue = cur.val;
}
index++;
cur = cur.left;
}
}
return KthValue;
} 在java官网中,推荐我们不要使用Stack类,而是使用Deque类
Deque<TreeNode> stack = new ArrayDeque<>(); // 压入 stack.push(cur); // 弹出 cur = stack.pop(); // 栈顶元素 cur = stack.peek();
当然也可以采用中序遍历的递归框架:
int res = 0;
int rank = 0;
public int kthLargest(TreeNode root, int k) {
inOrderTraverse(root, k);
return res;
}
public void inOrderTraverse(TreeNode root, int k){
if(root == null){
return;
}
// 稍微改造下,先遍历右边
inOrderTraverse(root.right, k);
rank++;
if(rank == k){
res = root.val;
return;
}
inOrderTraverse(root.left, k);
} BST的第K小节点
给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。
说明:
你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。
解题思路:
利用 BST 的中序遍历其实就是升序排序,找到第 K 个元素,可以采用递归和非递归框架。
int res = 0;
int nodeInd = 0;
public int kthSmallest(TreeNode root, int k) {
process(root, k);
return res;
}
// 返回值类型是void,突发性的返回一个值
private void process(TreeNode root, int k){
if (root == null){
return;
}
process(root.left, k);
// 递归序中,第二次来到自己时才更新序号
nodeInd++;
if (nodeInd == k){
res = root.val;
return;
}
process(root.right, k);
} 进阶思路:
参考大道至简
如果我们可以改造节点,让每个节点知道自己的排名,就可以采用二分的方法去查找。如何知道自己的排名呢,需要统计每个节点,以自己为根的二叉树有多少个节点,对于每个节点node就可以通过node.left推导出node的排名。node的排名等于getSize(node.left)+1。
// 第三种进阶方法,知道每个节点的size,就是以这个节点为根的子树的节点个数
// 前提:一定会有这个节点的
public int kthSmallest(TreeNode root, int k) {
int num = getNodeNum(root.left);
while (true){
if (num+1 == k){
return root.val;
}else if (num+1 > k){
// root.left的排名大于k,则在left子树中寻找
root = root.left;
num = getNodeNum(root.left);
}else { // num+1 < k
// root.left的排名小于k,则在right子树中寻找
root = root.right;
// k要更新为 在右子树中的相对kth,因为k已经比整个左子树的节点数大
k = k - (num+1);
num = getNodeNum(root.left);
}
}
}
// 以当前节点为根的子树的节点总数
public int getNodeNum(TreeNode node){
if (node == null){
return 0;
}
return 1 + getNodeNum(node.left) + getNodeNum(node.right);
} BST的搜索
给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。
解题思路:
public TreeNode searchBST(TreeNode root, int val) {
if(root == null){
return null;
}
if(val == root.val){
return root;
}else if(root.val > val){ // 中间值大于目标值,在左半部分寻找
return searchBST(root.left, val);
}else{
return searchBST(root.right, val);
}
}
// 迭代框架
public TreeNode searchBST(TreeNode root, int val) {
// 迭代模版的
while(root!=null && root.val!=val){
root = root.val > val ? root.left : root.right;
}
return root;
} BST的插入
给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
解题思路:
public TreeNode insertIntoBST(TreeNode root, int val) {
// 找到空位置,插入
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;
} BST的删除
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
首先找到需要删除的节点;
如果找到了,删除它。
说明: 要求算法时间复杂度为 O(h),h 为树的高度。
解题思路:
先采用查找的框架,去寻找这个节点,如果它不存在就不作修改,返回原来的根节点。如果找到了要删除的节点,则需要分情况讨论:
情况一:node恰好是末端节点,两个子节点都为空,可以直接删除,图片来自力扣,下同
情况二:node只有一个孩子,则直接让它的孩子接替它的位置
情况三:node有两个孩子,必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己
// 返回二叉搜索树(有可能被更新)的根节点的引用。
private TreeNode deleteNode(TreeNode root, int key) {
// 框架是,查找加上修改
if (root == null){
return null;
}
if (key > root.val){
root.right = deleteNode(root.right, key);
}else if (key < root.val){
root.left = deleteNode(root.left, key);
}else {
// 找到了待删除的节点了,分情况讨论
// 情况1:叶子结点 和 情况二:单孩子节点
if (root.left == null){
return root.right;
}
if (root.right == null){
return root.left;
}
// 情况三:左右孩子都不为空
// 1. 记录待删除的节点
TreeNode t = root;
// 2. 找到左子树中的最小节点,接替它
root = getMin(t.right);
// 3. 左子树中的最小节点,它的左孩子一定为空,但还要处理它可能不为空的右孩子
root.right = deleteMin(t.right);
root.left = t.left;
}
// 返回递归修改后的根
return root;
}
// 找到并返回,二叉树以node为根的最小节点,也就是最左节点
private TreeNode getMin(TreeNode node){
if (node == null){
return null;
}
while (node.left != null){
node = node.left;
}
return node;
}
// 以node为根,找到树上的最小节点,并把它的右孩子接替它的位置,并返回根节点(可能更新了)
private TreeNode deleteMin(TreeNode node){
// 如果node的left为空,就
if (node.left == null){
return node.right;
}
// 跨越两层节点
node.left = deleteMin(node.left);
return node;
// 非递归的容易理解
// 记录原始节点
TreeNode root = node;
// 如果第一个节点就是最右节点,直接返回它的右孩子
if (node.left == null){
return node.right;
}
// 递推更新,保留到祖父节点
while(node.left.left!=null){
node = node.left;
}
// 看图说话
node.left = node.left.right;
return root;
}
4.2 利用BST的性质
验证BST
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
解题思路:
利用BST中序遍历是生序的性质,依次判断遍历的数,如果不符合立刻返回false
long pre = Long.MIN_VALUE;
boolean res = true;
public boolean isValidBST(TreeNode root) {
if(root == null){
return true;
}
process(root);
return res;
}
public void process(TreeNode root){
if(root == null){
return;
}
process(root.left);
// 比较当前遍历值 和 之前的值
if(pre >= root.val){
res = false;
}
// 更新当前值
pre = root.val;
process(root.right);
} 非递归框架:
public boolean isValidBST(TreeNode root) {
if (root==null){
return true;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode node = root;
double pre = - Double.MAX_VALUE;
while(!stack.isEmpty() || node!=null){
// 左遍历
if(node!=null){
stack.push(node);
node = node.left;
}
// 退出左遍历,先弹出一个节点,在对右节点执行左遍历
else{
node = stack.pop();
if(node.val <= pre){
return false;
}
pre = node.val;
node = node.right;
}
}
return true;
} 最低公共祖先
力扣236:给定一个二叉树, 找到该树中两个指定节点p和q(一定在树上)的最近公共祖先。
解题思路:
1.递归方法
参考力扣题解Krahets , Anish.Hui 和 labuladong
递归套路思考:
- 本身函数需要返回什么?最低公共父节点
- 整合左右子树返回的信息?以子树为头节点的整棵树的最低公共父节点
- 整合规则?
递归解析:
终止条件(base case):
- 当到达叶子结点的子树时,返回
Null - 当
root=q / p时,返回根节点本身
- 当到达叶子结点的子树时,返回
递推工作:
- 开启递归左子节点,返回值记为
left - 开启递归右子节点,返回值记为
right
- 开启递归左子节点,返回值记为
整合规则:
- 当左右子树的返回都是空,说明查询节点都不在左右子树上,则返回空
- 当左右子树都不是空,说明
p和q恰好分布在左右子树上,此时root就是他们的最低公共祖先,并返回 - 当左子树不为空,右子树为空,则返回左子树,说明
p和q都分布在左子树上,p和q可能均匀的分布在左子树为根的树的两侧,也可能其中一个就是左子树 - 同理,右子树不为空时,返回右子树
对于整合情况二中,left和right非空,分别是p和q,可以说明root是它们的公共祖先,但能确定root就是「最近」公共祖先吗?这就是一个巧妙的地方了,因为这里是二叉树的后序遍历!前序遍历可以理解为是从上往下,而后序遍历是从下往上,就好比从p和q出发往上走,第一次相交的节点就是这个root,也当然是最近公共祖先!
时间复杂度:O(N),其中N 是二叉树的节点数。二叉树的所有节点有且只会被访问一次,因此时间复杂度为 O(N)。
空间复杂度:O(N),其中N 是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N,因此时间复杂度为 O(N)。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// base case:
// 1. 如果是叶子节点的子节点(null)返回null
// 2. 如果遇到p 或者 q 直接返回他们
if (root == null){
return null;
}
if (root == p || root ==q){
return root;
}
// 左右子树返回的信息,后序遍历框架
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 加工处理左右信息
// 1. 如果左不为空,右不为空,则返回root,因为p,q恰好分布在左右两边
// 此时root就是他们的最近公共父节点
if (left != null && right != null){
return root;
}
// 如果左为空,则返回右,p,q都在右子树中
if (left == null){
return right;
}
// 同理,如果右为空,则返回左,p,q都在左子树中
if (right == null){
return left;
}
// 左右子树没有,则返回空
return null;
} 2.批量查询方法
先花较大的力气建立一种记录,以后执行每次查询时就可以完全根据记录进行查询。
- 利用层次遍历,记录每个节点和它的父节点,用哈希表建立映射关系
- 当要查询时,从
node1开始,把node1自身和其所有的祖先节点放入一个无序集合中,最后一个祖先是根节点,他的祖先是null - 从
node2开始,检查node2是否在之前无序集合中,如果没有node2更新为它的父节点再检查 - 最后返回
node2,就是lowest Common ancestor
比如我们在下图中,查询node(4)和node(8)的LCA
| 节点 | 父节点 |
|---|---|
| Node 8 | Node 7 |
| Node 7 | Node 3 |
| Node 6 | Node 3 |
| Node 5 | Node 2 |
| Node 4 | Node 2 |
| Node 3 | Node 1 |
| Node 2 | Node 1 |
| Node 1 | Null |
其中node(4)的父节点路径是APath(4) = {4, 2, 1},然后node(8)去在这个路径中查询自己,第一次不在,查询node(8)的父节点node(7),也不在,依次向上查询,最后找到node(1)!
结构一建立记录的过程时间复杂度为O(N)、额外空间复杂度为O(N)。查询操作时,时间复杂度为O(h),其中,h为二叉树的高度。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null){
return null;
}
// 建立祖先查询表
HashMap<TreeNode, TreeNode> ancestorMap = new HashMap<>();
// 建立单个节点查询表
HashSet<TreeNode> ancestorPath = new HashSet<>();
// 建立辅助队列,进行层次遍历
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
// 根节点的祖先是 null
ancestorMap.put(root, null);
// 制作查询表
while(!queue.isEmpty()){
root = queue.poll();
if (root.left!=null){
queue.offer(root.left);
ancestorMap.put(root.left, root);
}
if (root.right!=null){
queue.offer(root.right);
ancestorMap.put(root.right, root);
}
}
// 制作单个节点的 祖先路径
TreeNode path = p;
while (path != null){
ancestorPath.add(path);
// 依次把p 和 p的所有祖先节点放入set中
path = ancestorMap.get(path);
}
// 查询节点
path = q;
while (!ancestorPath.contains(path)){
path = ancestorMap.get(path);
}
return path;
} BST的LCA
上面介绍了普通二叉树的LCA,结下来利用BST的性质,可以更快速的求解。
解题思路:
- 我们从根节点开始遍历
- 如果待查询的两个节点的值都大于根节点,说明这两个节点在右子树,则在左子树中寻找
- 如果待查询的两个节点的值都小于根节点,说明这两个节点在右子树,则在左子树中寻找
- 如果当前节点的值不满足上述两条要求,那么说明当前节点就是「分岔点」。此时这两个节点要么在当前节点的不同的子树中,要么其中一个就是当前节点。返回分岔点就是答案。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null){
return null;
}
while (true){
if (p.val > root.val && q.val > root.val){
root = root.right;
}
else if (p.val < root.val && q.val < root.val){
root = root.left;
}else {
break;
}
}
// 跳出循环时,说明p,q均匀分布在左右子树,或者有一个在根,另一个在其子树上
return root;
} 转化成累加树
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
解题思路:
观察给出的实例图片可知,从大到小降序打印 BST 节点的值,如果维护一个外部累加变量sum,然后把sum赋值给 BST 中的每一个节点,就可以将 BST 转化成累加树。
public TreeNode convertBST(TreeNode root) {
if (root == null){
return null;
}
convertBST_process(root);
return root;
}
int sum = 0;
public void convertBST_process(TreeNode root) {
if (root == null){
return;
}
// 传统中序遍历的倒叙
convertBST_process(root.right);
sum += root.val;
root.val = sum;
convertBST_process(root.left);
} 非递归框架:
public TreeNode convertBST(TreeNode root) {
if (root == null){
return null;
}
// 借助辅助栈
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode node = root;
int sum = 0;
while (!stack.isEmpty() || node!=null){
if (node != null){
stack.push(node);
node = node.right;
}else {
node = stack.pop();
sum += node.val;
node.val = sum;
node = node.left;
}
}
return root;
} 
京公网安备 11010502036488号