本来不想再继续在掘金发文了的,但是想了一下自己前面已经把AVL的所有思想都已经吃透了也发了总结,只是思想层面的,没有做到落地。会让人误以为我是个只会嘴皮子的小丑,就还是发了出来。对指针,引用,对象没有深刻理解的小伙伴没有必要看这个,看了没用。真心想学数据结构就从我第一篇开始看,不懂的都可以联系我,我现在是脱产学习时间充足。我的注释全是我的心血,希望能帮到真心愿意走计算机编程这条路的小伙伴。虽然我也还是个没有入行仅仅自学了2个月的小白,但是我自认为在我目前的学习范围内应该还是有足够的水平教新人的。真有大佬愿意来打我脸的不吝赐教。作为一个新手AVL学习一周不到能彻底从设计理念吃透并且纯从0手撕AVL,应该比我强的也不是特别多吧。
package test;
public class MyAVLTest {
public static void main(String[] args) {
MyAVL<Integer, String> avl = new MyAVL<>();
// ==========================
// 【极端1】连续插入 → 强制触发所有旋转 LL, LR, RR, RL
// ==========================
System.out.println("===== 极端插入测试 =====");
avl.put(10, "A");
avl.put(20, "B");
avl.put(30, "C"); // RR
avl.put(40, "D");
avl.put(50, "E"); // RR
avl.put(25, "F"); // RL
avl.put(5, "G");
avl.put(3, "H"); // LL
avl.put(4, "I"); // LR
avl.put(35, "J");
avl.put(28, "K");
avl.put(26, "L");
System.out.println("插入完成,树结构:");
avl.display(avl.head);
// ==========================
// 【极端2】get 查询所有节点(包括不存在的)
// ==========================
System.out.println("\n===== 极端查询测试 =====");
System.out.println("get(10) = " + avl.get(10));
System.out.println("get(30) = " + avl.get(30));
System.out.println("get(99) = " + avl.get(99)); // 不存在
System.out.println("get(null) = " + avl.get(null)); // 空值
// ==========================
// 【极端3】replace 替换
// ==========================
System.out.println("\n===== 替换测试 =====");
avl.replace(10, "AAA");
avl.replace(50, "EEE");
System.out.println("替换后 get(10) = " + avl.get(10));
// ==========================
// 【极端4】暴力删除(所有边界)
// ==========================
System.out.println("\n===== 极端删除测试 =====");
avl.remove(3); // 叶子
avl.remove(4); // 叶子
avl.remove(25); // 中间节点
avl.remove(10); // 根节点
avl.remove(20); // 关键父节点
avl.remove(30); // 深层节点
avl.remove(99); // 删除不存在
avl.remove(null); // 删除null
System.out.println("删除完成,树结构:");
avl.display(avl.head);
// ==========================
// 【极端5】空树操作(绝对边界)
// ==========================
System.out.println("\n===== 空树测试 =====");
MyAVL<Integer, String> emptyTree = new MyAVL<>();
emptyTree.remove(1);
emptyTree.get(1);
emptyTree.replace(1, "x");
emptyTree.display(null);
// ==========================
// 【极端6】单节点树 增删查
// ==========================
System.out.println("\n===== 单节点测试 =====");
MyAVL<Integer, String> single = new MyAVL<>();
single.put(1, "one");
single.remove(1);
single.display(single.head);
// ==========================
// 【极端7】倒序插入(最容易失衡)
// ==========================
System.out.println("\n===== 倒序插入测试 =====");
MyAVL<Integer, String> reverse = new MyAVL<>();
reverse.put(50, "50");
reverse.put(40, "40");
reverse.put(30, "30");
reverse.put(20, "20");
reverse.put(10, "10");
reverse.display(reverse.head);
System.out.println("\n===== 所有极限测试完成 =====");
}
}
class MyAVL<K extends Comparable<K>,V>{
AVLNode<K,V> head;
int size;
public MyAVL(K key,V value) {
super();
this.head = new AVLNode(key,value);
this.size++;
}
public MyAVL() {
super();
}
public V get(K key) {
if(this.size == 0 || key == null) return null;
AVLNode<K,V> node = head;
while(node != null) {
if(node.key.equals(key)) {
return node.value;
}else if(node.key.compareTo(key) < 0) {
node = node.right;
}else {
node = node.left;
}
}
return null;
}
public boolean replace(K key, V value) {
if (key == null || value == null) return false;
AVLNode<K,V> node = head;
while (node != null) {
if (node.key.equals(key)) {
node.value = value;
return true;
}
node = node.key.compareTo(key) < 0 ? node.right : node.left;
}
return false;
}
//旋转后更新节点高度。
private void flush(AVLNode<K,V> child,AVLNode<K,V> parent,AVLNode<K,V> grand) {
grand.height = grand.getHeight(grand);
parent.height = parent.getHeight(parent);
child.height = child.getHeight(child);
}
//这个方法是回溯的分治思想抽的公共方法处理失衡。如果你没有写过单向链表或者双向链表对指针没有深刻的理解
//我的建议是没有必要看这个,你先自己去写单向链表或者双向链表先感受一下指针,对指针不理解的话就压根体会
//不到什么是指针,引用,对象。
private void reBlance(AVLNode<K,V> node,boolean contrl) {
if(node == null) return;
AVLNode<K,V> seek = this.head;
AVLNode<K,V> pre = null;
if(node.pre == null) return;
//循环这里一定要用父节点,不然你不好做判空拦截。用父节点循环就可以在循环外先拦截,再循环内拦截。
//因为失衡状态最低需要3个节点才能成立,所以必须对父节点跟爷爷节点进行判空,不然JVM必给你抛空指针异常。
//这里的失衡逻辑我是按下往上写的,所以写注释的时候就按下往上了,抱歉。
while(node.pre != null) {
if(node.pre.pre == null) break;
//逻辑判断时不需要判断父节点的左右,我开始也脱裤子放屁去加了,父节点的左右平衡因子就自动判断了。
//从右边逻辑写到左边才反应过来。
if(node == pre.left && pre.pre.getBalance(pre.pre) >= 2) {
if(pre.pre == this.head) {
//这里是先更新头节点,然后用seek指针去操作原头节点。这样就对更新的头节点不会产生任何影响
//而且操作起来更方便,指针更清晰,而且还处理了冗余代码问题,一举多得
this.head = pre;
this.head.pre = null;
}else {
//因为seek不是头节点,所以需要更新seek指向爷爷节点用于做通用指针连接。
seek = pre.pre;
//先用爷爷节点的父节点先把升级的父节点接住。因为是LL失衡都是在左边所以是left指针接
seek.pre.left = pre;
//这里是父节点直接升级为爷爷节点。
pre.pre = seek.pre;
}
//这里是公共指针逻辑
seek.pre = pre;
pre.right = seek;
//这里需要每次失衡调整以后都要更新节点高度,我开始都漏了,豆包提醒的。
//主要自己还是没太理解,我自认为的是高度既然是动态的,我指针更新好以后就不需要更新了。
//豆包解释的是因为height是成员变量,房子内部的东西更新了就需要同步更新。应该就是说
//指针还是指向原来的内存地址,但是我又是动态绑定的,还得再想想。
this.flush(seek,pre,node);
//这里是表示进来的是put方法是的失衡还是remove方法的失衡。参数直接在方法调用的时候内部直接传。
if(contrl == true) break;
}else if(node == pre.right && pre.pre.getBalance(pre.pre) >= 2) {
if(pre.pre == this.head) {
this.head = node;
this.head.pre = null;
}else {
seek = pre.pre;
seek.pre.left = node;
node.pre = seek.pre;
}
pre.pre = node;
node.left = pre;
seek.pre = node;
node.right = seek;
//这里也是一样需要手动置空,不然必定死循环。
seek.left = null;
this.flush(seek,pre,node);
if(contrl == true) break;
}else if(node == pre.left && pre.pre.getBalance(pre.pre) <= -2) {
if(pre.pre == this.head) {
this.head = node;
this.head.pre = null;
}else {
seek = pre.pre;
node.pre = seek.pre;
seek.pre.right = node;
}
//RL型失衡这里在对节点的指针连接的时候有一个问题需要指出,node节点不管是删除的还是添加的方法调用,
//由于必须遵守严格的AVL树的规则,所以这个节点天然就必定是高度为1的子节点。这个需要自己考虑到
//AVL规则,不然会写一个画蛇添足的右循环找右子节点。而且通过推论RL删除永远只可能发生在最底层。
//绝对不会是在中间层。本来开始不想写了的,结果一写就跟发现新大陆一样,五体投地的佩服。
seek.pre = node;
node.left = seek;
pre.pre = node;
node.right = pre;
//这里爷爷节点的右指针始终指向父节点需要手动置空,不然会形成环状,遍历的时候会死循环。
seek.right = null;
this.flush(seek,pre,node);
if(contrl == true) break;
}else if(node == pre.right && pre.pre.getBalance(pre.pre) <= -2) {
if(pre.pre == this.head) {
this.head = pre;
this.head.pre = null;
}else {
//非根
seek = pre.pre;
seek.pre.right = pre;
pre.pre = seek.pre;
}
//这里旋转我开始画面困惑了一下,因为自己没有彻底理解怎么旋转的,只好请教了豆包。爷爷节点降
//下来之前需要先把父节点的左孩子接住再去连接到父节点的左子节点。不然会丢父节点的左子树。或者
//按我开始想当然的想法直接接到父节点最左子节点到底树整体升高。指针的操作这里就不详细说了,需要
//自己对对象,指针,引用有清晰的认知才操作的了。不然基本就是乱指,丢子树。
seek.right = pre.left;
//这行是多余的,跟RL是相同的问题,LR也是一定没有左子节点的。跑测试代码才跑出来的问题
//考虑的还是不够周到。写RL的时候都已经知道了的问题,居然还犯真的蠢到家,不过豆包也没分析出来。
//她的教学还是太按教材来了。碰到我这种野路子她就懵逼了。
//pre.left.pre = seek;
pre.left = seek;
this.flush(seek,pre,node);
if(contrl == true) break;
}
pre = node.pre;
node = node.pre;
}
}
public boolean put(K key,V value) {
if(key == null || value == null) return false;
if(this.size == 0) {
this.head = new AVLNode<>(key,value);
this.size++;
return true;
}
AVLNode<K,V> node = head;
AVLNode<K,V> pre = null;
while(node != null) {
if(node.key.equals(key)) {
node.value = value;
return true;
}
pre = node;
if(node.key.compareTo(key) < 0) node = node.right;else node = node.left;
}
if(pre.key.compareTo(key) < 0) {
pre.right = new AVLNode<>(key,value);
pre.right.pre = pre;
}else {
pre.left = new AVLNode<>(key,value);
pre.left.pre = pre;
}
this.reBlance(node,true);
this.size++;
return true;
}
public boolean remove(K key) {
if(this.size == 0 || key == null) return false;
AVLNode<K,V> node = head;
AVLNode<K,V> pre = null;
while(node != null) {
if(node.key.equals(key)) break;
pre = node;
if(node.key.compareTo(key) < 0 ) node = node.right;else node = node.left;
}
if(node == null) return false;
if(node.right == null && node.left == null) {
if(pre == null) {
this.head = null;
}else{
if(pre.left == node) pre.left = null;
if(pre.right == node) pre.right = null;
}
}else if(node.left == null) {
if(pre == null) {
this.head = node.right;
this.head.pre = null;
}else if(pre.left == node){
pre.left = node.right;
node.right.pre = pre;
}else if(pre.right == node) {
pre.right = node.right;
node.right.pre = pre;
}
}else if(node.right == null) {
if(pre == null) {
this.head = node.left;
this.head.pre = null;
}else if(pre.left == node){
pre.left = node.left;
node.left.pre = pre;
}else if(pre.right == node) {
pre.right = node.left;
node.left.pre = pre;
}
}else {
AVLNode<K,V> child = node.right;
AVLNode<K,V> pChild = null;
while(child.left != null) {
pChild = child;
child = child.left;
}
node.key = child.key;
node.value = child.value;
if(pChild == null) {
if(child.right == null) {
node.right = null;
}else {
node.right = child.right;
child.right.pre = node;
}
}else {
if(child.right == null) {
pChild.left = null;
}else {
pChild.left = child.right;
child.right.pre = pChild;
}
}
child.left = child.right = child.pre = null;
}
this.reBlance(node, false);
this.size--;
return true;
}
public void display(AVLNode<K,V> node) {
if(node == null) return;
display(node.left);
System.out.println(node);
display(node.right);
}
}
class AVLNode<K extends Comparable<K>,V> {
AVLNode<K,V> pre;
AVLNode<K,V> left;
AVLNode<K,V> right;
int height;
K key;
V value;
public AVLNode(K key, V value) {
super();
this.key = key;
this.value = value;
this.height = this.getHeight(this);
}
public AVLNode() {
super();
}
//嘿嘿,我现在也能写出来这种一行代码了,爽啊。这种爽感真的太美妙了。
//敲黑板:这里我开始想的时候思维进了死胡同,一直想去用一个变量存储返回值,导致搞了挺久时间没想通,
//递归最核心的就需要自己首先懂得JVM栈内存的工作机制,最后进栈内存的执行的方法最先出去,也就是他们
//常说的压栈,弹栈。如果对JVM的工作原理都不懂就基本理解不了递归是怎么运行的,你可以把栈当成一个杯子
//往里面倒水的时候先出来的水是最后倒入的水。然后就是倒水进去杯子的时候是一次性倒进去的,然后一次性再
//倒出来。就相当于你的代码会一次性压N多个方法到栈内存,然后从最后压进去的方法的执行结果先返回,再把
//返回值传递给上一层,依次返回到最底层。最核心的是自己要找到方法执行到最底层的结束条件。
//第二个重点:当自己懂得如何写递归以后,一定要记住她是一次性给你把你的程序运行到最底层,相当于底层用
//容器把你一次性压进去的方法全部装起来,按后进先出的原理返回。这里会有一个物理层面的问题,JVM栈的最大容量
//存储JVM的栈的最大容量是1000,不能一次性压入的方法超过1000。不然必然栈溢出炸程序
//豆包说我写的性能不是最优,没太理解。说Java标准写法是存了height变量,每次调用速度O(1),我的是log(n).
//后续如果想到了再更新。
public int getHeight(AVLNode<K,V> node) {
if(node == null) return 0;
return Math.max(getHeight(node.left), getHeight(node.right)) + 1;
}
//每个节点的平衡因子
public int getBalance(AVLNode<K,V> node) {
if(node == null) return 0;
return getHeight(node.left) - getHeight(node.right);
}
@Override
public String toString() {
return this.key.toString()+","+this.value.toString()+","+this.height;
}
}
class MyNullPointerException extends RuntimeException{
public MyNullPointerException() {
super();
// TODO Auto-generated constructor stub
}
public MyNullPointerException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
}