package test; /*
-
总结,这一版是错的,也不能说是错的,只是不是最优解,因为用深度当高度处理起来非常麻烦。
-
但是我依然收获满满,希望对看到的人有所帮助。这一版的AVL开始思考的方向就错了,导致我后续
-
代码想做兜底策略也永远做不到,极限添加失衡以后我拿不到确定的全局失衡的节点,更别提平衡因子了。
-
在我经过跟豆包的几个小时讨论以后他最后还是点醒了我,节点的height一定是由他的子节点的高度
-
来决定的,而不是我最开始想的节点深度。节点的平衡因子就应该是她的左孩子高度减去右孩子高度。
-
因为每个节点的高度都是由子节点的高度决定的越上层的节点的高度就越高这样才能拿到每个节点正确的平衡因子。
-
下面的每行代码基本都有我写时的心路历程。 */
public class MyAVLTest { public static void main(String[] args) { } } 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(); } //这个方法是直接copy的原来的TreeMap,也没重写,估计应该很多地方可以精简的。以后写代码应该会比这个好一些, //现在就不去纠结了。 public boolean put(K key,V value) { if(key == null || value == null) return false; if(this.size == 0) { this.head = new AVLNode<>(key,value); 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) { node = new AVLNode<>(key,value); pre.right = node; node.pre = pre; node.pre.height = node.height++; }else { node = new AVLNode<>(key,value); pre.left = node; node.pre = pre; node.pre.height = node.height++; } //赋值以后不管this.head指针怎么改变。对seek都没有影响。我前面对这行代码的理解还是没有足够深刻 //因为在我联系到删除的方法是假删除以后我就彻底理解了这行代码是什么意思了。这行代码代表的是指向了 //this.head在堆内存这个指针本身。如果我对this.head重新赋值以后仅仅是改变了head的指针方向,并没有 //改变它在堆内存的this.head这个指针本身(我觉得叫数据也可以)。 AVLNode<K,V> seek = this.head; //这个方法的思路我是在晚上睡觉失眠的时候脑子自己要画图跑代码想通的。主要是想通了平衡因子的问题,这个是 //AVL自平衡树的核心,想不通这里代码基本无从下笔。草图很容易画,但是真要落地代码了不理解平衡因子画再多的 //草图也没有意义。前面的四大容器总结的心得不太好,因为都是我写完了半个月才后补的,没有当时写的时候对思路的 //理解足够深刻。 if(pre == pre.pre.left && node == pre.left && this.getBalance(node, pre.pre.right) > 1) { if(pre.pre == head) { //这3行代码我想了半个多小时才想明白要引入一个引用来接住这个节点本身,再去操作指针。 //这种对指针的感受只有自己写过才能体会到指针到底有多恐怖。我刚开始写的时候居然写出来了 //this.head.pre = this.head.left;this.head.left = null这种代码虽然当时马上反应过来了。 //想想都觉得头皮发麻。而且我当时脑子里的代码还有直接this.head = pre;本来就已经链全部断了还重新 //赋值,简直搞笑。然后一定要画图,画图,画图,让画图成为自己的本能。没有画图的习惯很难学好编程。 //重点注意:这里有个大坑,豆包都没分析出来,我都是在想删除方法的时候才反应过来的。失衡以后节点高度 //需要全部重新更新。因为平衡因子只在new的时候会更新一次,没有重新赋值就依然是原来的。 //豆包编程版本跟专业版的分析都是稀烂的,被我暴打。至少证明我在指针跟动态赋值这块比豆包更强。 //节点高度只有在new的时候会根据头节点被动态赋值,头结点的height指针是指向了方法区内存的常量池 //基本数据类型“1”,而剩余整颗树的height指针都并没有直接指向任何数据。只有在被调用的瞬间才会 //指向方法区的常量池基本数据类型。而在非头节点的失衡状态指针重新连接好以后节点的height才重新动态赋值 seek.pre = pre; pre.right = seek; this.head = pre; pre.height = 1; seek.left = pre.pre = null; }else { //一定要先把这个失衡的爷爷节点用一个引用先接住,不然你在操作指针的时候所有的链都会断开 //导致指针乱飞。这个你不画图压根体会不到指针的操作的恐怖之处。这里有重复代码可以提,刚写的 //时候不熟悉就没提取,写完了就不想改了,后面的逻辑就直接提取了。 seek = pre.pre; pre.pre = pre.pre.pre; pre.pre.pre.left = pre; seek.pre = pre; pre.right = seek; seek.left = null; } }else if(pre == pre.pre.left && node == pre.right && this.getBalance(node, pre.pre.right) > 1) { //这个玩意你不画图如果能把指针脑补出来并把代码完美跑通,我相信你一定是天才。 //写这个的时候我写着写着就发现有重复逻辑。只需要先把孙子节点先去接住爷爷节点的父节点再去写 //这样操作指针的时候逻辑更加清晰。 //我这是改了两版后的最终版,才让我彻底捋清楚了指针的逻辑。自己学习的时候一定要画图画图画图。 //我相信再牛的工程师开始也一定是依靠画图来学习的。不然这个指针不把人绕晕,以我的认知我绝对不信。 if(pre.pre == head) { //这行代码要联系起AVLNode<K,V> seek = this.head;这个一起看才能真正悟透什么是引用,引用的本质。 this.head = node; //这里也要重新赋值,因为this.head重新赋值node时只是改了head的指针方向,对node内部的height //并没有更新,头节点的height一定要更新了,后续节点的height才能拿到正确的字面值。 this.head.height = 1; node.pre = null; }else { seek = pre.pre; seek.pre.left = node; node.pre = seek.pre; } //我的策略是先在把孙子节点升级的事先安排好以后再进行下一步的操作。这样指针逻辑操作起来就 //非常顺了,不然就是一团乱麻。因为我写代码都不会去运行验证,我都是给豆包帮我验证豆包居然还 //说我的代码里面有指针错了。让我懵逼了一会,我重新再捋了一遍以后狠狠的打脸了她, //然后她说 AVL 树的 LR 旋转指针就是数据结构界的绕口令天花板! node.left = pre; node.right = seek; pre.pre = node; seek.pre = node; //这里我踩了坑,本来父节点的父指针已经转到子节点的左边了,我最后还去手动置空了。 seek.left = null; //后面这两个逻辑我不想补了,我现在意识到了学编程不仅仅是脑力活,更需要铁打的身体。我在写这些代码的时候 //颈部跟脊椎连接处时不时就隐隐作痛,真的难顶。至少在现在的我的认知下编程百分之60-70是纯体力活,而需要的 //脑力活应该只有百分之30-40. }else if(pre == pre.pre.right && node == pre.left && this.getBalance(node, pre.pre.right) > 1) { if(pre.pre == head) { this.head = node; }else { } }else if(pre == pre.pre.right && node == pre.right && this.getBalance(node, pre.pre.right) > 1) { } this.size++; return true; } //这个方法是直接copy的原来的TreeMap,也没重写,估计应该很多地方可以精简的。以后写代码应该会比这个好一些,现在就不去纠结了。 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.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; } if(pre.left == node && this.getBalance(node.left,node.right) > 0) { if(pre == head) { } } this.size--; return true } //这个删除的平衡因子是我在画图时发现用put的平衡因子判断的话压根判断不了所有的删除以后的树的状态 //然后问豆包才知道应该重新定义平衡因子应该由当前节点的左右孩子的高度来判断AVL树的状态。不然我都提笔不了 //知道了需要专门定义删除的平衡因子方法以后再去做删除就只需要画图就能把删除后失衡的逻辑闭环了。 //然后我还跟豆包讨论了应该是先删除还是先判断节点在删除以后失衡状态再重现连接节点还是应该先删的问题。 //我发现了我定义的平衡因子本身就是动态存在的,只有在调用的时候才会真正意义获得字面值,所以必须是先删 //再判断树的状态是否失衡。在没有删的情况下压根不存在判断的情况,更加不能拿到新的平衡因子,因为没删之前 //用的是没删的平衡因子数据,删了是拿到删除以后的平衡因子。我才彻底想通了应该怎么写代码。 private int getBalance(AVLNode<K,V> node1,AVLNode<K,V> node2) { if(node1 == null) return 0 - node2.height; if(node2 == null) return node1.height; else return node1.height - node2.height; } } //这个平衡因子我刚开始定义的时候方向都错了。我定义到了数据结构的类中。准备开始写旋转的时候就发现自己错的离谱 //而且我刚开始模拟失衡的时候还出现了暴力赋值的想法直接在旋转逻辑里面对孩子赋值2,爷爷赋值0的。后面晚上 //睡觉的时候失眠,脑子自己要画图跑代码才想通应该动态添加。这样的好处非常多最直接的是能知道这个节点是在哪层。 //和别的节点的距离差是多少。 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 = 1; } public AVLNode() { super(); } @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 } }