3.3 二叉树操作集锦
- 二叉树算法的设计总路线:明确一个节点要做的事情,然后剩下的事情抛给递归框架
void traverse(TreeNode root){
//root需要做什么?
//其他的不需要root操作,抛给递归
traverse(root.left);
traverse(root.right);
}
- 如何把二叉树所有节点的值+1?
void plusOne(TreeNode root){
if(root == null){
return ;
}
root.val+=1;
plusOne(root.left);
plusOne(root.right);
}
- 如何判断两科二叉树是否完全相同?
boolean isSameTree(TreeNode root1,TreeNode root2){
//都为空的话,显然就是相同的
if(roo1 == null && root2 == null){
return true;
}
//一个非空,一个空,显然就不同
if(root1 == null || root2 == null){
return false;
}
//两个都不是空的,假如说val值不一样就证明不同,就不用继续遍历检测了
if(root1.val != root2.val){
return false;
}
//root1和root2该比的都把它给比完
return isSameTree(root1.left,root2.left) && isSameTree(root1.right,root2.right);
}
- 二叉搜索树(Binary Search Tree,简称BST)是一种很常用的二叉树,它的定义是:一个二叉树中,任意节点的值要大于等于左子树所有节点的值,而且要小于等于右子树的所有节点的值。
3.3.1 判断BST的合法性
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于
当前节点的数。
节点的右子树只包含 大于
当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
- 根据二叉搜索树的含义以及我们之前的结论:root节点需要做的是要和整棵左子树和右子树的所有节点比较,观察题干函数的结构,只给了两个参数,这样的比较是远远不够的,对于这种情况可以使用辅助函数,增加函数参数列表,在参数中携带额外的信息
public class IsValidBST {
public boolean isValidBST(TreeNode root) {
return this.isValidBST(root,null,null);
}
/**
* max和min相当于给子树上的所有节点添加了一个min和max的边界,约束root的左子树节点值不超过root的值
* 右子树的节点值不小于root的值,也就符合BST定义
* @param root
* @param min
* @param max
* @return
*/
private boolean isValidBST(TreeNode root,TreeNode min,TreeNode max){//保证root.val在(min,max)之间
//一行一行代码来解释
if(root == null){//如果检索到空节点,那么就证明这条路径上的所有节点都是符合BST定义的,我们认为这一次的搜索是合法的
return true;
}
//min节点代表一个下界,也就是当前检索的节点不得小于等于这个节点的值
if(min!= null && root.val<= min.val){
return false;
}
//max节点代表一个上界,也就是当前检索的节点不得大于等于这个节点的值
if(max!=null && root.val >= max.val){
return false;
}
//这一句非常关键:
//1.规定了遍历方向,从root开始,检索root.left和root.right
//2.当检索root.left的时候,也就是检查它的左子树,那么对于这个节点而言和它的左子树而言,其上边的所有的值都不能够超过root.val,因此max设置为root
// 至于min的话,对于左子树而言,左子树的最小值是否有要求呢?首先它是root的左子树,root要保证它的值不会违反整棵树都是BST这个原则,所以限制root的(l,r)都会继承下去
//3.当检索root.right的时候,也就是检查它的右子树,那么对于这个节点而言和它的右子树而言,其上边的所有的值都不能够低于root.val,因此min设置为root
// 至于max同理,需要继承,因为要求整棵树都得是合理的BST
return isValidBST(root.left,min,root) && isValidBST(root.right,root,max);
}
}
3.3.2 在BST中查找一个数是否存在
- 给定二叉搜索树(BST)的根节点
root
和一个整数值val
,你需要在BST中找到节点值等于val
的节点,返回以该节点为根的子树,如果节点不存在,则会返回null
public TreeNode searchBST(TreeNode root, int val) {
if(root == null){
return root;
}
//BST的特性,左小右大
if(root.val == val){
return root;
}
if(root.val < val){
//如果要找的值是比root要大的,那么就搜索右子树,因为较大的值在右子树
return searchBST(root.right,val);
}
return searchBST(root.left,val);
}
- 根据上述代码以及思路可以归纳出一套基于BST的遍历框架
void BST(TreeNode root,int target){
if(root.val == target){
//找到目标,进行操作
}
if(root.val < target){
BST(root.right,target);
}
if(root.val > target){
BST(root.left,target);
}
}
3.3.3 在BST中插入一个数
-
给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,
新值和原始二叉搜索树中的任意节点值都不同
。 -
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
-
对数据结构的操作无非遍历与访问,遍历就是
找
,访问就是改
,具体到这个问题就是先找到插入的位置,然后进行插入的操作。那么如何来确定插入的位置呢? -
遍历BST,找到合适的位置:从root节点开始,按照左小右大的规则找位置,我们为了简化问题,就不做那种从已有节点中强行插入的操作了,我们要的最终效果是找到一个空的位置,按照规则将节点连接上就好了。
-
同时我们需要注意到的是,由于树节点的限制,它没有给到我们关于父节点的信息,那么连接这个事情交给谁做呢?
交给递归即可
public TreeNode insertIntoBST(TreeNode root, int val) {
if(root == null){
//遍历到合适的位置了,进行插入,注意将节点new出来后是交给递归来连接的
return new TreeNode(val);
}
//题干中说没有这种样例,但是如果有的话就直接返回就行了
if(root.val == val){
return root;
}
//val的值要大,`左小右大,那么我应该将这个节点连接到右子树上`
if(root.val < val){
root.right = insertIntoBST(root.right,val);
}
if(root.val > val){
root.left = insertIntoBST(root.left,val);
}
//完毕之后将链接好的树给返回回去就好了
return root;
}
3.3.4 在BST中删除一个数
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
-
首先找到需要删除的节点;
-
如果找到了,删除它。
-
套路是一样的,和插入的操作类似,先找到,然后再改,只是这个改会稍微有些不同
- [情况1] 要删除的节点恰好是树的叶子节点,那么它就可以被直接删除
- [情况2] 要删除的节点拥有一个节点,那么它就要让它的左子节点或者右子节点来接替自己的位置,把它搬上来
- [情况3] 这种情况比较复杂,这是因为要删除的节点既有左子节点又有右子节点,这时候为了不破坏BST的性质,我们的策略是找到左子树中最大的那个节点或者右子树中的最小的那个节点来接替自己,这一点比较难理解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-etST7mdS-1661225232323)(./image/BST-01.png)]
-
其实这个问题就是,到底要让哪个节点来接替自己?我们根据左小右大的规则,这时候这个节点的左子节点可以上来,右子节点可以上来,但是真的有这么简单吗?如果随意地让一个节点来接替,那么很有可能就会出现新接替的节点
镇不住
整棵BST的情况,我们希望能够找到一个节点
,这个节点比左子树的所有节点都要大,但是同时要比右子树的所有节点都要小,符合要求的节点就是左子树中最大的节点或者右子树中最大的节点 -
那么如何来找到到子树中的最小的节点或者最大的节点呢?根据BST的性质,左小右大,一直向左遍历直到叶子节点,那就肯定就是最小的,要找最大的同理
-
或许可能会问,诶这个右子树中的任意一个节点不是也比左子树的所有节点都要大吗,那我在右边瞎选一个就不行吗,注意,这里要求的是两个同时成立,万一你选出来的这个节点比右子树的一个节点要大咋办?
-
思路理清楚之后,来看看代码
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null){
return root;
}
if(root.val == key){//遍历查找得到了val为key的节点,进行删除
//情况1:这个节点没有子节点
if(root.left == null && root.right == null){
return null;
}
//情况2:这个节点只有一个子节点
if(root.left == null){//这里省略了root.right != null,因为前一个分支条件语句覆盖掉了
//这时候只需要把右子树给接替上来就好了
return root.right;
}
if(root.right == null){
return root.left;
}
//情况3,左右子树都不是空,这时候我们需要找到一个节点,这个节点可以是左子树最大的节点,可以是右子树最小的节点
TreeNode min = getMin(root.right);//这里找的是右子树的最小节点
//然后进行删除,这里根据BST的数据结构特点来进行操作,注意,无论是最大的节点还是最小的节点,我们发现它都是叶子节点,那么我们剩下就是要删除叶子节点嘛
//我们就是在编写删除操作,于是删除这个事情我们把它交给递归就好了
root.val = min.val;
root.right = deleteNode(root.right,root.val);
}
//否则的话就继续遍历,这里的话key比root的值要大,那么就要遍历值更大的右子树
else if(root.val < key){
root.right = deleteNode(root.right,key);
}else if(root.val > key){
root.left = deleteNode(root.left,key);
}
return root;
}
private TreeNode getMin(TreeNode root){
//向左遍历
while(root.left !=null){
root = root.left;
}
return root;
}
private TreeNode getMax(TreeNode root){
//向右遍历
while(root.right !=null){
root = root.right;
}
return root;
}
}
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null){
return root;
}
if(root.val == key){//遍历查找得到了val为key的节点,进行删除
//情况1:这个节点没有子节点
if(root.left == null && root.right == null){
return null;
}
//情况2:这个节点只有一个子节点
if(root.left == null){//这里省略了root.right != null,因为前一个分支条件语句覆盖掉了
//这时候只需要把右子树给接替上来就好了
return root.right;
}
if(root.right == null){
return root.left;
}
//情况3,左右子树都不是空,这时候我们需要找到一个节点,这个节点可以是左子树最大的节点,可以是右子树最小的节点
TreeNode max = getMax(root.left);//这里找的是左子树的最大节点
//然后进行删除,这里根据BST的数据结构特点来进行操作,注意,无论是最大的节点还是最小的节点,我们发现它都是叶子节点,那么我们剩下就是要删除叶子节点嘛
//我们就是在编写删除操作,于是删除这个事情我们把它交给递归就好了
root.val = max.val;
root.left = deleteNode(root.left,root.val);
}
//否则的话就继续遍历,这里的话key比root的值要大,那么就要遍历值更大的右子树
else if(root.val < key){
root.right = deleteNode(root.right,key);
}else if(root.val > key){
root.left = deleteNode(root.left,key);
}
return root;
}
private TreeNode getMin(TreeNode root){
//向左遍历
while(root.left !=null){
root = root.left;
}
return root;
}
private TreeNode getMax(TreeNode root){
//向左遍历
while(root.right !=null){
root = root.right;
}
return root;
}
3.4 完全二叉树的节点数
-
给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。
-
[普通二叉树的节点计算]
public int countNodes(TreeNode root){
if(root == null){
return 0;
}
return 1+countNodes(root.right)+countNodes(root.left);
}
- [满二叉树的节点计算]
public int countNodes(TreeNode root){
int h = 0;
//计算树的高度
while(root !=null){
root = root.left;
h++;
}
return (int)Math.pow(2,h-1);
}
满二叉树的节点个数计算方法: 2 h − 1 2^h-1 2h−1,其中h是树的高度
- 那么通过以上两者的代码,能否对本题有所启发呢?
- 我们知道完全二叉树与满二叉树的最大区别其实是在树的右端,如果我们对树进行遍历,假如说最左端的树的高度和最右端的树的高度是一样的,那么肯定就是满的二叉树,否则我们就把它当做普通二叉树来看待,按照普通二叉树的方法来遍历
public int countNodes(TreeNode root) {
TreeNode l = root;
TreeNode r = root;
int hl=0,hr=0;
while(l!=null){
l = l.left;
hl++;
}
while(r!=null){
r = r.right;
hr++;
}
if(hr == hl){
int h = hr;
return (int)Math.pow(2,hr)-1;
}
else {
return 1 + countNodes(root.left) + countNodes(root.right);
}
}
- 分析时间复杂度
- 首先While循环最坏情况下需要log(N)的时间,最后需要O(N)的时间向左向右递归,整体感觉是O(NlogN)
- 但是这两个递归只有一个会真的递归下去,另一个一定会触发
hl == hr
而立即返回而不会递归下去 一棵完全二叉树的两棵子树,至少有一棵会是满二叉树
- 综上所述,算法的递归深度就是树的高度是O(logN),每次递归所花费的时间就是while循环,需要O(logN),所以整体的时间复杂度就是O(logNlogN)
3.5 用框架序列化和反序列化二叉树
请实现两个函数,分别用来序列化和反序列化二叉树。
你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
提示:输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
}
}
// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));
- 所谓的序列化不过就是把结构化的数据给打平,其实就是在考察二叉树的遍历方式而已
3.5.1 前序遍历
- 遍历编码的代码比较简单,只要根据"根左右"的顺序来写就可以了
private static final String SEP =",";
private static final String NULL = "#";
private StringBuilder sb;
private void traverse(TreeNode root,StringBuilder stringBuilder){
if(root == null){
stringBuilder.append(NULL).append(SEP);
return;
}
stringBuilder.append(root.val).append(SEP);
traverse(root.left,stringBuilder);
traverse(root.right,stringBuilder);
}
- 遍历解码转换为树的比较困难,一步步分析来写
- 一般语境下,单单前序遍历结果是不能还原二叉树结构的,因为缺少空指针的信息,至少要得到前中后序遍历中的两种才能够还原二叉树,但是这里的node列表包含空指针的信息,所以只使用node列表就可以还原二叉树
- 反序列化过程也一样:先确定好根节点root,然后遵循前序遍历的规则,递归生成左右子树即可
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
private static final String SEP =",";
private static final String NULL = "#";
private StringBuilder sb;
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
sb = new StringBuilder();
serialize(root,sb);
return sb.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
LinkedList<String> lists = new LinkedList<>();
//1.将字符串转换为列表
String[] nodes = data.split(",");//此处得到的nodes列表就是二叉树的前序遍历结果
//2.如何通过二叉树的前序遍历结果来还原一棵二叉树
for (String node : nodes) {
lists.addLast(node);
}
TreeNode root = deserialize(lists);
return root;
}
private void serialize(TreeNode root,StringBuilder stringBuilder){
if(root == null){
stringBuilder.append(NULL).append(SEP);
return;
}
stringBuilder.append(root.val).append(SEP);
serialize(root.left,stringBuilder);
serialize(root.right,stringBuilder);
}
private TreeNode deserialize(LinkedList<String> nodes){
if(nodes.isEmpty()){
return null;
}
String first = nodes.removeFirst();
if(NULL.equals(first)){
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(first));
root.left = deserialize(nodes);
root.right = deserialize(nodes);
return root;
}
}
// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));
3.5.2 后序遍历
private static final String SEP =",";
private static final String NULL = "#";
private StringBuilder sb;
private void serialize(TreeNode root,StringBuilder sb){
if(root == null){
sb.append(NULL).append(SEP);
return;
}
serialize(root.left,sb);
serialize(root.right,sb);
sb.append(root.val).append(SEP);
}
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
sb = new StringBuilder();
serialize(root,sb);
return sb.toString();
}
private TreeNode deserialize(LinkedList<String> lists){
if(lists.isEmpty()){
return null;
}
//寻找root节点,然后跟着走,后序遍历序列中,根节点位于列表的最末端
String last = lists.removeLast();
//然后根据左右根的序列顺序特点反推
if(NULL.equals(last)){
return null;
}
//然后先把根节点给造出来
TreeNode node = new TreeNode(Integer.parseInt(last));
//左右根
node.right = deserialize(lists);
node.left = deserialize(lists);
return node;
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
String[] nodes = data.split(",");
LinkedList<String> list = new LinkedList<>();
for (String node : nodes) {
list.addLast(node);
}
TreeNode root = deserialize(list);
return root;
}
3.5.3 BFS
private static final String SEP =",";
private static final String NULL = "#";
private StringBuilder sb;
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
//层级遍历
if(root == null){
return "";
}
sb = new StringBuilder();
//初始化队列,root入队,开始BFS
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
TreeNode head = queue.poll();
if(head == null){
sb.append(NULL).append(SEP);
continue;
}
sb.append(head.val).append(SEP);
//检查队头元素,由于是需要记录空节点,所以我们需要将空节点给入队
queue.offer(head.left);
queue.offer(head.right);
}
return sb.toString();
}
/**
* root被夹在两颗子树的中间,也就是在nodes列表的中间,是否能够一下子确定下来哪个是根节点与列表的长度有关
* 当是偶数的时候,除于2的就行
* 否则的话还需要借助前序遍历和后续遍历中的任意一个序列的信息来完成
* 在这里我们选用一个BFS的方式来进行确定一棵二叉树
* @return
*/
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if(data.isEmpty()){
return null;
}
String[] nodes = data.split(",");
Queue<TreeNode> queue = new LinkedList<>();
TreeNode root = new TreeNode(Integer.parseInt(nodes[0]));
queue.offer(root);
for(int i = 1;i<nodes.length;){
TreeNode head = queue.poll();
String left = nodes[i++];
if(!NULL.equals(left)){
head.left = new TreeNode(Integer.parseInt(left));
queue.offer(head.left);
}else{
head.left = null;
}
String right = nodes[i++];
if(!NULL.equals(right)){
head.right = new TreeNode(Integer.parseInt(right));
queue.offer(head.right);
}else{
head.right =null;
}
}
return root;
}
3.6 Git原理:二叉树最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
- 首先我们明确一下,什么叫做LCA:LCA就是最近公共祖先,它是指两个节点通过最少次指针运算能够指向的那个公共节点
- 对于递归问题,我们需要确定三点
- 这个函数是干什么的?(函数定义)
- 这个函数参数中的变量是什么?(状态是什么)
- 得到函数到的递归结果,你应该干什么?(状态应该怎么转移?)
- 题目中说了输入的p和q一定存在以root为根的树中,但是在递归过程中,我们对root可能会做出拆分为左子树右子树这样的操作出来,所以我们可以分四种情况讨论
- [情况1] 如果p和q都在以root为根的树中,函数返回的是p和q的最近公共祖先节点
- [情况2] 如果p和q都不在以root为根的树中,函数返回null
- [情况3] 如果p和q只有一个存在于以root为根的树中,函数应该要返回那个节点
- 情况3解析:根据题意,要找的p和q是一定在以root为根的树里面的,那么什么时候会出现这种情况呢?也就是当前递归到的节点是其公共祖先的下一级子节点,也就是这时候递归函数并不知道自己已经经过了公共祖先节点,而是要靠递归边界来收敛这个条件,这时候我们直接返回存在的那个节点,把它返回上去
- 这时候拿到这个返回的节点,我们拿来干什么呢(状态转移)?我们知道这个节点是公共祖先的下一级子节点嘛,也就是说这个节点是它的上级的某个子节点,而且可以肯定的是:它位于这时候满足条件的最近公共祖先的左子树或右子树上,而且另一个节点位于另一半子树上
- 那么这样就说得通了,我们递归到发现这个节点,然后层层向上回退,然后检查,在某个祖先节点是否存在一种状态,在这种状态下,它所检查得到的p和q节点是不是都不是空的,如果都不是空的,是不是说明我们找到了这个最近公共祖先了?
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//1.假如说就是一根空树,那么都不用找到了
if(null == root){
return null;
}
//2.如果root本身就是p或者q,比如root就是p节点,如果q存在于以root为根的树中,显然root就是最近公共祖先
//就算q不存在以root为根的树中,按照情况3,我们也要返回那个存在的节点给它用
if(root == q || root == p){
return root;
}
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
//1.这时候从底部开始向上回溯,一直回溯到这里,发现这时候刚好p和q异侧不用讲,这时候root肯定就是最近公共祖先
if(left!=null && right!=null){
return root;
}
//2.假如说我遍历到的这个节点,两个都是空的
if(left == null && right == null){
return null;
}
//3.情况3
return left == null ?right : left;
}