b
补充一点:二分搜索为什么时间复杂度为O(logn)?
二分搜素:顾名思义可知它是一半一半的搜素排查
那么每一次排除一半 那么假设有n个
那么我们循环就需要多少次才可以把n除尽呢?
上述的问题也等价于2的多少次方等于n呢?
很明显 log2n次(注意这里2是下标)
大O法可知:循环的时间复杂度为O(logn)
由于这两种情况下都各有缺点
因此我们引入了二叉搜索树
可以把搜索 添加 删除的时间复杂度 最坏的情况都维护在O(logn)处
二叉搜索树的节点大小规则:
举个例子:
假如说我们添加一个节点为12那么开始放
12比8大 那么放在相当于8的右子树
又比10大那么找到14
比14小 转换到左边
比13还小 那么插到13的左子树处
再添加一个15
与上面同理:它是加到14的右子树上
因此我们得出一个结论
节点添加到第几层与它的添加顺序是无关的
因此二叉树搜索树中无索引
给你一个二叉搜索树:
OK接下来我们就要代码实现对二叉搜索树的一些用处:
当我们在add增加一个节点的时候
我们需要一层一层往下去找
因此我们需要模拟节点 要模拟出那个线
因此我们需要搞一个内部类
private static class Node<E>{
E elements;
//左子节点
Node<E> left;
//右子节点
Node<E> right;
//左右节点的父节点
Node<E> parent;
//提供构造方法
public Node(E elements,Node<E> parent){
//由于可能一个相对父节点无左右节点
//它是一个叶子节点 那么这里只构造父节点与元素即可
this.elements=elements;
this.parent=parent;
}
}
这里的构造方法:
构造方法的方法名必须与类名相同
构造方法没有返回类型,也不能定义为void,在方法名前面不声明方法类型。
构造方法的主要作用是完成对象的初始化工作,它能够把定义对象时的参数传给对象的域。
OK
实现add
我们要顺着根节点7往下找合适的位置进行添加
思路:
当我们要添加12的时候
先与根节点进行比较 发现比根节点要大 那么放到根节点的右边
再往下找 发现比9还大
那么放到9的右边 发现比11还大 那么放到右子树位置
这时候注意了啊
发现11的右子树处是空的
那么就可以add了
这个父节点是随着往下递推是不断的变化的
我们最后可以通过父节点的左右子节点进行 赋值node即可完成add
假设我们添加一个节点为9
发现有重复 那么我们可以直接返回 也可以覆盖旧的值
建议覆盖旧的值
其实无论是覆盖旧的值还是直接返回 都是取决于我们的个人需求
当一个节点是带有名字的 我们想要add 那么肯定是为了把它加上去
那么即是节点值相等 我们也要进行覆盖才可以实现添加这一方法操作
add的代码如下:
public void add(E elements) {
elementsNotnullcheck(elements);
if (root == null) {
root = creatNode(elements, root.parent);
size++;
//添加节点之后的操作
addafter(root);
return;
}
//根节点不为空
Node<E> node = root;
Node<E> parent = root;
int cmp = 0;
while (node != null) {
cmp = compare(elements, node.elements);
//每一次的父节点就是node 在它向左下或右下走之前的节点
parent = node;
if (cmp > 0) {
node = node.right;
} else if (cmp < 0) {
node = node.left;
} else {
return;
}
}
Node<E> newNode =creatNode(elements,node);
if (cmp > 0) {
parent.right = newNode;
} else {
parent.left = newNode;
}
//添加节点之后的操作
addafter(newNode);
}
/**
*返回值大于0 e1>e2
*等于0 e1==e2 小于0 e1<e2
*/
private int compare(E e1,E e2){
if(comparator!=null){
return comparator.compare(e1,e2);
}
//当比较器为空的时候 先强制进行转换为可比较性的
// 那么就默认调用Person里面重写实现的比较逻辑compareTo()即可
return ((Comparable<E>)e1).compareTo(e2);
}
}
private void elementsNotnullcheck(E elements){
if(elements==null){
throw new IllegalArgumentException("NO");
}
}
/**
*这里这个方法是为了实现创建出一个通用的节点
* 因为AVL BST以后等等的树的节点的属性是不一样的
* AVL中需要创建出的节点带有height的属性 然而BST不需要
*以后需要时重写即可
*/
protected Node<E> creatNode(E elements,Node<E> parent){
return new Node<E>(elements,parent);
}
protected static class Node<E>{
E elements;
Node<E> left;
Node<E> right;
Node<E> parent;
public Node(E elements,Node<E> parent){
this.elements=elements;
this.parent=parent;
}
}
二叉树的遍历:
1.前序遍历
前序遍历就是先访问根节点
递归
基本顺序:
先访问根节点7 访问之后访问左子树节点4
递进行 把节点4当作根节点访问相对的左节点2
递进行 把节点2当作根节点 访问相对的左子节点1
访问完之后 开始归
从最底部节点2的右子节点3往上
归 5
归 9
归 8 11 10 12
然后遍历完毕。
public void preorderTraversal(){
//先把根节点拿出来传递
preorderTraversal(root);
}
//这里第一次接收根节点
public void preorderTraversal(Node<E> node){
if(node==null){
return;
}
System.out.println(node.elements);//直接打印根的
//遍历左边
preorderTraversal(node.left);
//遍历右边
preorderTraversal(node.right);
}
2.中序遍历
遍历是对任何的树都是存在的
但是如果遍历出来的得出的是升序或者是降序
那么就是二叉搜索树专有的
因为二叉搜索树的节点特性:
节点是大于左节点
小于右节点的
二叉搜索树的节点大小规则:
二叉搜索树的遍历顺序要么升序要么降序
public void inorderTraversal(){
//把根节点先传给node
inorderTraversal(root);
}
private void inorderTraversal(Node<E> node){
if(node==null){
return;
}
//先搞左子
inorderTraversal(node.left);
//中间的直接打印即可
System.out.println(node.elements);
//再递归右子
inorderTraversal(node.right);
}
3.后序遍历
其实后序遍历就是最后再访问根节点
这与前序遍历的正好相反
这就是前后遍历的唯一不同之处
public void postorderTraversal(){
preorderTraversal(root);
}
private void postorderTraversal(Node<E> node){
if(node==null){
return;
}
postorderTraversal(node.left);
postorderTraversal(node.right);
System.out.println(node.elements);
}
4.层序遍历
一开始我们的队中只有根节点
因此我们一开始入队的头节点7就是根节点
接着先把左子节点4入队 再把右子节点9入队
那么当根节点7访问完之后
我们接着访问先入队的4
那么接着4访问完之后
再先把2入队 然后把5入队
此时队列中有3个节点
之后再访问9
当访问完9之后
那么左节点8先入队 然后11入队
那么此时有4个节点在队中
我们在开始访问 2
当2访问完之后
又让1 3 入队
之后再访问5 8 这俩下面没有节点
那么访问到11
当11访问完时
10 12 入队
按顺序访问最后的四个节点 1 3 10 12
层序遍历
一定要会!
//层序遍历
public void levelOrderTranversal(){
if(root==null){
return;
}
Queue<Node<E>> queue=new LinkedList<>();
//先把根节点入队
queue.offer(root);
//只要队列不为空 那么就一直取出它的头节点
while (!queue.isEmpty()){
//让头节点出队
Node<E> node=queue.poll();//poll()方法的返回值为队列的头节点
//拿出来之后我们直接拿出来访问
System.out.println(node.elements);
//判断左右节点是否为空
if(node.left!=null){
//左节点不为空那么就入队
queue.offer(node.left);
}
if(node.right!=null){
queue.offer(node.right);
}
}
}
但是我们也要学习设计模式:
慢慢来只能
当我们拿出来一个节点的元素时 我们上面进行的操作时进行打印
但是我们不一定非要进行打印
所以我们可以设计一个方法来让它们自己决定自己的操作模式
实现类BinarySearchTree
对所有遍历进行实现设计模式
/**
* 优化前序 中序 后序遍历
*/
//前序遍历 根 左 右
public void preorderTraversal(Visitor<E> visitor){
inner(root,visitor);
}
private void preorderTraversal(Node<E> node,Visitor<E> visitor){
if(node==null||visitor==null){
return;
}
visitor.visit(node.elements);
preorderTraversal(node.left,visitor);
preorderTraversal(node.right,visitor);
}
//中序遍历 左 根 右
public void inner(Visitor<E> visitor){
inner(root,visitor);
}
private void inner(Node<E> node,Visitor<E> visitor){
if(node==null||visitor==null){
return;
}
inner(node.left,visitor);
visitor.visit(node.elements);
inner(node.right,visitor);
}
//后序遍历 左 右 根
public void Last(Visitor<E> visitor){
Last(root,visitor);
}
private void Last(Node<E> node,Visitor<E> visitor){
if(node==null||visitor==null){
return;
}
Last(node.left,visitor);
Last(node.right,visitor);
visitor.visit(node.elements);
}
/**
*优化层序遍历
*/
//从外面传入一个visitor给这个 之后visitor实现接口Visitor
//然后通过接口把元素传过去
public void leveOrder2(Visitor<E> visitor){
if(root!=null||visitor==null){
return;
}
Queue<Node<E>> queue=new LinkedList<>();
queue.offer(root);//先把根节点入队
while (!queue.isEmpty()){
Node<E> node=queue.poll();//出队 poll()方法的返回值为队列的头节点
visitor.visit(node.elements);
//我们不一定非要打印 直接调用外部方法
if(node.left!=null){
queue.offer(node.left);//左入队
}
if(node.right!=null){
queue.offer(node.right);//右入队
}
}
}
public static interface Visitor<E>{
void visit(E element);
}
并且在测试类TestDemo中我们也要new Visitor这个接口并且使用匿名类重写
/**
* 使用匿名内部类 重写visit方法
*/
bst.leveOrder2(new BinarySearchTree.Visitor<Integer>() {
@Override
public void visit(Integer element) {
System.out.println("leo"+element+"leo");
}
});
再次优化遍历方式:
/**
* 再一次进行优化前序 中序 后序遍历
*/
//前序遍历 根 左 右
public void preorderTraversal(Visitor<E> visitor){
inner(root,visitor);
}
private void preorderTraversal(Node<E> node,Visitor<E> visitor){
if(node==null||visitor.stop){
return;
}
visitor.stop=visitor.visit(node.elements);
preorderTraversal(node.left,visitor);
preorderTraversal(node.right,visitor);
}
//中序遍历 左 根 右
public void inner(Visitor<E> visitor){
inner(root,visitor);
}
private void inner(Node<E> node,Visitor<E> visitor){
if(node==null||visitor.stop){
return;
}
inner(node.left,visitor);
if(visitor.stop){
return;
}
visitor.stop=visitor.visit(node.elements);
inner(node.right,visitor);
}
//后序遍历 左 右 根
public void Last(Visitor<E> visitor){
Last(root,visitor);
}
private void Last(Node<E> node,Visitor<E> visitor){
if(node==null||visitor.stop){
return;
}
Last(node.left,visitor);
Last(node.right,visitor);
if(visitor.stop){
return;
}
visitor.stop=visitor.visit(node.elements);
}
/**
*优化层序遍历
*/
//从外面传入一个visitor给这个 之后visitor实现接口Visitor
//然后通过接口把元素传过去
public void leveOrder2(Visitor<E> visitor){
if(root!=null||visitor==null){
return;
}
Queue<Node<E>> queue=new LinkedList<>();
queue.offer(root);//先把根节点入队
while (!queue.isEmpty()){
Node<E> node=queue.poll();//出队 poll()方法的返回值为队列的头节点
if(visitor.visit(node.elements)){
return;//当方法visit返回值为true时 直接退出程序
}
//我们不一定非要打印 直接调用外部方法
if(node.left!=null){
queue.offer(node.left);//左入队
}
if(node.right!=null){
queue.offer(node.right);//右入队
}
}
}
/**
* 这一次优化把接口改为抽象类 因为抽象类可以定义成员变量
*如果返回true表示停止 false表示继续
*/
public static abstract class Visitor<E>{
boolean stop;
//接口中不可以定义成员变量 因此改为抽象类
abstract boolean visit(E element);
}
Test测试类代码
bst.leveOrder2(new BinarySearchTree.Visitor<Integer>() {
@Override
public boolean visit(Integer element) {
System.out.println("leo"+element+"leo");
return element==9?true:false;//表示当遍历到元素9的时候 我们就停止
}
});
bst.preorderTraversal(new BinarySearchTree.Visitor<Integer>() {
@Override
public boolean visit(Integer element) {
System.out.println("messi"+element+"messi");
return element==4?true:false;//表示当遍历到元素4的时候 我们就停止
}
});
}
}
练习:
计算二叉树的高度:
1.用递归的方法
二叉树的高度就等于它根节点的高度
那么根节点的高度往下递归就等于它子节点中最大的高度再加一即可
一直递到最下面
之后再归到最上层就可求出
/**
* 计算二叉树的高度
*递归运算
*/
public int height(){
return height(root);
}
private int height(Node<E> node){
if(node==null){
return 0;
}
return 1+Math.max(height(node.left),height(node.right));
//相对根节点的高度等于左右子树最大的高度再加1
}
2.用迭代
有队列入队的规律我们可知:
当每一层访问完之后 下一层的元素个数就等于在队中的个数
举个例子:
当7被访问完之后 4 和9才会入队
然后先访问4 访问完4之后 2 和5入队
再访问9 访问完9之后 8 和11入队
此时第二层访问完毕 此时队中有4个节点
那么第三层有4个节点
那么可知:
当每一层访问完之后 下一层的元素个数就等于此时在队中的个数
public int height(){
if(root==null){
return 0;
}
int height=0;//记录高度 每访问完一层就+1
int levelSize=1;//存着每一层的元素个数
//一开始就把根节点入队 默认第一层为一个
Queue<Node<E>> queue=new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()){
//每一次削掉一个节点
Node<E> node=queue.poll();
//每一次减一
levelSize--;
if(node.right!=null){
queue.offer(node.right);
}
if (node.left!=null){
queue.offer(node.left);
}
//当为0时 证明一层已经访问完了 接着访问下一层
if(levelSize==0){
//前面也分析了
// 当访问完一层之后我们队中的元素个数就等于下一层即将访问的元素个数
levelSize=queue.size();
height++;
}
}
return height;
}
练习2:
判断一颗树是否为完全二叉树
用层序遍历来写
做这个题的前提是你得清楚完全二叉树
何为完全二叉树?
节点从上到下 从左到右依次排列的二叉树
当最后一行排满的时候我们就称之为满二叉树
首先我们说几种不是完全二叉树的情况:
1.一个节点 左为空 右不为空
2.在一层中 靠左边的无子节点 然而相对靠右的有子节点
分析一下图中第三种的情况:
node.left!=null&&node.right==null 或者node.left==null&&node.right==null
那么这个两种情况的node都是度为1的节点 那么这种度为1的节点后面的节点肯定都是叶子节点
何为叶子节点?
无子节点的节点。。。。
何为度?
到叶子节点的唯一路径长为1
路径长即是节点的个数
代码实现如下:
首先在内部类中搞出两个方法
写出代码判断是否是完全二叉树
总共也就四种情况:
一个节点的左右子节点 这里称之为左右了
左为空 右不为空 false
左为空 右为空 判断这个节点之后的节点是否为叶子节点 若是 返回true 反之 返回false
左不为空 右为空 判断这个节点之后的节点是否为叶子节点 若是 返回true 反之 返回false
左不为空 右不为空 按顺序入队
核心代码如图:
但是这种设计模式存在bug
hasTwochildren()我们知道这个方法的设计:
当一个节点左右节点都不为空时 才返回true
if(node.hasTwoChildren()){
queue.offer(node.left);
queue.offer(node.right);
当我们上面这一段代码 因此只有当左右都不为空时左右节点才会入队
然而存在一种情况 如图:当把4和9节点入队之后
访问完4之后 我们发现只有2因此会出现一个bug
2不会入队
那么其实这个二叉树真正有效存在的部分仅仅是7 4 9
1 和2都没进去
如何修复?
我们修复else那一块
到else的有两种情况
左为空 右为空
左不为空 右为空
此时我们应该让左不空的这个节点入队
有两种改正方法:
/**
* 改正1:上面那种判断是否为完全二叉树的写法
* 上面这种写法存在bug
* 当node.left!=null时 它没有进行入队操作而是向后遍历 如果node.left.left!=null时 则会出现bug
* 解决方案:
* 只要最开始的时候不管是左 还是右不为空 先直接入队
* 再判断剩余情况
* 总之:
* 要用层序遍历去检验是否为完全二叉树:
* 应当先把左右入队的情况架构写清楚:两个if语句
*/
public boolean isCompeleteMax(){
if(root==null){
return false;
}
Queue<Node<E>> queue=new LinkedList<>();
queue.offer(root);
boolean leaf=false;
while (!queue.isEmpty()){
Node<E> node=queue.poll();
if(leaf&&!node.isLeaf(node)){
return true;
}
if(node.left!=null){
queue.offer(node.left);
} else if(node.right!=null) {
//node.left==null&&node.right!=null
return false;
}
if(node.right!=null){
queue.offer(node.right);
} else {
//node.right==null
// 有两种情况:node.right==null&&node.left==null||node.right==null&&node.left!=null
leaf=true;
}
}
return true;
}
/**
* 判断是否为完全二叉树的
* 改正2:这种方法没有上一种方法优美 这种方法存在较多的重复判断
* 可以基于错误的那一种直接修改
* 解决方案:在最后一种情况进行判断左是否为空
* 若不为空 那么进行入队操作
*/
public boolean isCompleteMAX(){
if(root==null){
return false;
}
Queue<Node<E>> queue=new LinkedList<>();
queue.offer(root);
boolean leaf=false;
while (!queue.isEmpty()){
Node<E> node=queue.poll();
if(leaf&&!node.isLeaf(node)){
return false;
}
if(node.isLeaf(node)){
queue.offer(node.left);
queue.offer(node.right);
} else if(node.left==null&&node.right!=null) {
return false;
} else {
if(node.left!=null){
queue.offer(node.left);//在最后一种情况进行判断左是否为空 若不为空 那么进行入队操作
}
leaf=true;
}
}
return true;
}
练习三:
翻转二叉树:
就是将所有节点的左右子树都交换
代码标准:力扣
/**
* 翻转二叉树
* 1.前序遍历
*/
public Node<E> invertTree(Node<E> node){
if(node==null){
return null;//传进来的可能是空
}
//交换左右
Node<E> tmp=node.left;
node.left=node.right;
node.right=tmp;
//递归左 右
//我们前序遍历的原则就是先交换头节点
// 左右子节点的交换是不用计较顺序的
// 因此先递归谁都没关系
invertTree(node.left);
//这里的node就是每一次递归调用传进来的新的节点
invertTree(node.right);
return node;
}
/**
* 翻转二叉树
* 2.后序遍历
*/
public Node<E> invertTree2(Node<E> node){
if(node==null){
return null;//传进来的可能是空
}
//递归左 右
//我们前序遍历的原则就是先交换头节点
// 左右子节点的交换是不用计较顺序的
// 因此先递归谁都没关系
invertTree(node.left);
//这里的node就是每一次递归调用传进来的新的节点
invertTree(node.right);
//交换左右
Node<E> tmp=node.left;
node.left=node.right;
node.right=tmp;
return node;
}
/**
* 翻转二叉树
* 3.中序遍历
*/
public Node<E> invertTree3(Node<E> node){
if(node==null){
return null;//传进来的可能是空
}
invertTree(node.left);
//这里的node就是每一次递归调用传进来的新的节点
//交换左右
Node<E> tmp=node.left;
node.left=node.right;
node.right=tmp;
//注意这种情况 左右已经交换了
// 因此这里左就是右
invertTree(node.left);
return node;
}
/**
* 4.层序遍历
*/
public Node<E> invertTree4(Node<E> node){
if(node==null){//传进来的可能为空
return null;
}
Queue<Node<E>> queue=new LinkedList<>();
queue.offer(node);//第一次传进来的node就是根节点 不要多想
while (!queue.isEmpty()){
node=queue.poll();
Node<E> tmp=node.left;
node.left=node.right;
node.right=tmp;
if(node.left!=null){
queue.offer(node.left);
}
if(node.right!=null){
queue.offer(node.right);
}
}
return node;
}
练习:
通过前序遍历与中序遍历 推出原来二叉树的结构
重构二叉树:
给出一组二叉树节点
以下是分别进行前序遍历和中序遍历得出的结果
何为前序遍历? 根节点先遍历 然后左 然后右
中序? 先左 再根节点 再右
分析图:当我们知道前序遍历的结果之后可以推出4是根节点 那么中序遍历中 根节点放到中间
因此推出123 为左子树 5 6为右子树
害 这个分清之后 又是递归了该
如果从中序遍历开始推:由前序遍历可知:4为第一个主根
看中序:分为 left区 根 right区
之后我们通过前序遍历可知
2为left区的根节点
6为right区的根节点
通过递归知识可知:left区和right区都进行了中序遍历
那么都是左 中 右 区的中序遍历顺序
那么推出1为2的左子 3为2的右子
同理 6在5的右边
那么得出5为6的右子
何为真二叉树?
要么度为2 要么度为0
但是根据前序遍历和后序遍历
除非它是真二叉树
否则结果不唯一确定
因为遍历之后可能存在度为1的时候 也就是说左右子树有可能只是存在其中一个
这时候我们无法分出到底是左还是右
前驱节点:
不仅适用于二叉搜索树 适用于任何二叉树
如:8的前驱节点为7
先把节点都进行中序遍历 然后找应找节点的前一个
由于这是二叉搜索树
大小规律:左子树点<根< 右子树点
我们在找前驱节点时
可以先找到根节点的左子树节点4
然后一直往右子树右边的那个方向去找
一直找到当发现右边为null 那么7则是所找前驱节点
举个例子:
13的前驱节点
先往左找到10 然后一直向右直到右为空才停止往右
那么13的前驱节点为12
但是这种方法只适用于二叉搜索树
OK
上面找前驱的情况都是节点的左子树节点是存在的
如果不存在呢?
假如说找9的前驱节点:
总结一句话:找它最近的父节点并且这个父节点是比它小的父节点
那么我们可知9的前驱节点就是往上找
一直找
当我们找到它是某一个父节点的右子树中
二叉搜索树中 右>根
最后一种情况
假设把8的左半部分都删除
它既没有左节点又没有父节点
那么它没有前驱节点
总结:
你要看一个节点的前驱节点
第一步 先看它的左子树处的节点
如果不为空 一直向右找 直到右为空
如果为空 才执行第二步
第二步:
找与它最近的父节点且该父节点是比它要小的节点
换句话说
就是找到一个父节点 该父节点的右子树就是它
那么它的前驱就是这个父节点
举个例子:
我们要找节点1的前驱
先执行第一步:看节点1的左子树
发现没有
那么执行第二步 一直往上找
看是否可以找到一个父节点
这个父节点的右子树处是否是它
发现直到找到8时还是没有 它一直父节点的左子树
再往上发现 父节点为空此时停止 确定1没有前驱
找前驱代码如下:
/**
* 找前驱节点
*/
private Node<E> predecessor(Node<E> node){
if(node==null){
return null;
}
if(node.left!=null){
node=node.left;
while (node.right!=null){
node=node.right;
}
return node;
}
//在这里就是node.left==null
while (node.parent!=null&&node==node.parent.left){
node=node.parent;
}
//退出循环有两种情况
// 1.node.parent.right==node
// 2.node.parent==null
return node.parent;
}
后继节点:
找后边第一个比它大的节点
总结:与找前驱节点正好相反
第一步:先找它的右子树处的节点
如果右子树处节点不为空
那么一直向左找
直到找到左为空时 那就算找到了
如果右子树为空时 执行第二步
第二步:先上找离他最近的父节点
并且该父节点的左子树节点就是它
那么该父节点就是它的后继
举个例子:2的后继就是3
6的后继:
右子树处节点为空 那么往上找父节点 5的左不是6
继续往上 发现7的左是5
那么7就是6的后继
代码如下:
/**
* 找后继节点
*/
public Node<E> Lastfind(Node<E> node){
if(node==null){
return null;
}
if(node.right!=null) {
node = node.right;
while (node.left != null) {
node = node.left;
}
return node;
}
//能到这说明node.right==null 因为如果执行上面条件一定会返回的 因为有return
while (node==node.parent.right&&node.parent!=null){
node=node.parent;
}
//到这里
// 1.node==node.parent.left
// 2.node.parent==null
return node.parent;
}
总而言之:
前驱节点是中序遍历之后的前一个节点
后继节点是后一个节点
删除节点:
1.删除叶子节点
分为三种情况:
2.删除度为1的节点
先设一下 设node为我们要删除的节点
我们用child代表node的子节点
那么假如我们要去掉4时
child.parent=node.parent
除了节点 我们还要搞一下属性node.parent.left=child
OK
假设我们要删除9
child.parent=node.parent
node.parent.right=child
如果要删的是根节点
root=child
child.parent=null
3.删除度为2的节点
假设要删除节点5
先用5的前驱或后继节点覆盖5
这里用前驱节点4覆盖了要被删的度为2的节点5
覆盖之后
再把原来的4给删除
总结一下:
删除度为2的节点 不能直接删 我们先用度为1的前驱或后继节点覆盖这个节点
覆盖完之后
我们再把这个前驱或后继给删除
所以删除度为2的节点
真正被删除的不是这个节点 而是这个节点的前驱或后继
但是度为2的节点的前驱或后继的度只可能是1或0
代码如下:
/**
* 通过下面的方法找到对应的节点之后
* 我们进行删除操作
* 设计模式:
* 我们删除的时候 总共要删除三类节点:
* 度为0 1 2的节点
* 我们在删除度为2的节点时 不可以直接删除而是先拿前驱或后继节点的元素值去覆盖它
* 然后在把用来的前驱或后继节点给删除
* 这被删除的节点只可能是度为0或1的节点 这与前两类的删除模式相重复
* 因此:
* 我们先删除度为2的节点
* 之后再处理后继或前驱节点时 也相当于处理度为1或0的节点了
*/
private void remove(Node<E> node) {
if (node == null) {
return;
}
//先删除度为2的节点
if (node.left!=null&&node.right!=null) {
Node<E> f = predecessor(node);//找到前驱节点
node.elements = f.elements;//值覆盖
node = f;
//由于我们后面不只是为了度为2这一种情况而准备的
//有可能传进来的node直接是度为1或0 那么我们不可以直接用f去处理
//因此我们用node指向f所指向的节点
}
Node<E> replacement = node.left != null ? node.left : node.right;
if (replacement != null) { //度为1
replacement.parent = node.parent;
if (node.parent == null) {//度为1且为根节点
root = replacement;
} else if (node == node.parent.left) {//一定要用node这条线的判断指向 自己想想
//replacement.parent = node.parent;
node.parent.left = replacement;
} else if (node == node.parent.right) {
//replacement.parent = node.parent;
node.parent.right = replacement;
removeafter(node);
}
//到这已经代表replacement==null也就是说那个为了删除度为2节点去覆盖的是叶子节点
} else if (node.parent == null) {
root = null;//度为0 且为根节点
removeafter(node);
} else {//度为0 不是根节点
if (node.parent.left == node) {
node.parent.left = null;
} else {//node.parent.right==node
node.parent.right =null;
}
removeafter(node);
}
}
然后下面是里面所用到找前驱的方法
怕太长 所以分两块吧
public Node<E> prefind(Node<E> node) {
if (node == null) {
return null;
}
Node<E> p = node.left;
//如果一开始要找的节点node左节点不为空
if(p!=null) {
while (p.right != null) {
p = p.right;
}
//循环退出 找到了
return p;
}
//如果一开始要找的节点node左节点为空
//从父节点 父节点的父节点找前驱节点
while(node.parent!=null&&node==node.parent.left){
node=node.parent;
}
//循环退出的条件:
// 1.node.parent==null
// 2.node==node.parent.right
// 但是两者的返回值都是node.parent
return node.parent;
}
二叉搜索树是继承于二叉树的
因此我们根据继承的关系来对代码进行重构