一、搜索二叉树
搜索二叉树一定要说明以什么标准来排序
经典的搜索二叉树,树上没有重复的用来排序的key值
如果有重复节点的需求,可以在一个节点内部增加数据项
二、搜索二叉树查询key(查询某个key存在还是不存在)
1)如果当前节点的value==key,返回true
2)如果当前节点的value<key,当前节点向右移动
3)如果当前节点的value>key,当前节点向左移动
4)如果当前节点变成null,返回false
三、搜索二叉树插入新的key
和查询过程一样,但当前节点滑到空的时候,就插入在这里
四、搜索二叉树删除key
0)先找到key所在的节点
1)如果该节点没有左孩子、没有右孩子,直接删除即可
2)如果该节点有左孩子、没有右孩子,直接用左孩子顶替该节点
3)如果该节点没有左孩子、有右孩子,直接用右孩子顶替该节点
4)如果该节点有左孩子、有右孩子,用该节点后继节点顶替该节点
后继节点 也就是节点的右子树的最左节点 就是该节点的下个节点 称后继节点 因为搜索二叉树中序遍历就是有序的升序排列 下个节点自然就是找右子树的最左节点
搜索二叉树特别不讲究
1)基础的搜索二叉树,添加、删除时候不照顾平衡性
2)数据状况很差时,性能就很差
给搜索二叉树引入两个动作:左旋、右旋
五、AVL树、SB树、红黑树的共性
1)都是搜索二叉树
2)插入、删除、查询(一切查询)搜索二叉树怎么做,这些结构都这么做
3)使用调整的基本动作都只有左旋、右旋
4)插入、删除时,从最底层被影响到的节点开始,对往上路径的节点做平衡性检查
5)因为只对一条向上路径的每个节点做O(1)的检查和调整,所以可以做到O(logN)
六、AVL树、SB树、红黑树的不同
1)平衡性的约束不同
AVL树最严格、SB树稍宽松、红黑树最宽松
2)插入、删除和搜索二叉树一样,但是额外,做各自的平衡性调整。各自的平衡性调整所使用的动作都是左旋或者右旋
七、AVL树
平衡二叉搜索树, 左右树高度差小于2,即为平衡 搜索树表示有序,才能搜索,左中右 中序遍历是升序
1)最严格的平衡性,任何节点左树高度和右树高度差不超过1
2)往上沿途检查每个节点时,都去检查四种违规情况:LL、RR、LR、RL
3)不同情况虽然看起来复杂,但是核心点是:
LL(做一次右旋)、RR(做一次左旋)
LR和RL(利用旋转让底层那个上到顶部)
LL 就是说cur的左节点的左节点 路线是较高的一个子树导致的高度差大于1 cur进行右旋就可以修复平衡性 LR 就是cur的左节点的右节点 路线是较高的子树 将其左节点的右节点弹到cur 也就是对左节点进行左旋,左节点的右节点位置就来到左节点,然后再对cur进行右旋,就会使得前面来到左节点的左右子节点来到cur 完成平衡 RR 对cur 进行左旋 RL 对cur.r 进行右旋,使得cur.r.l来到 cur.r位置 然后对cur进行左旋,使得cur.r的节点来到cur 完成平衡性 如果同时存在LL与LR 也就是当前cur.l的左右子树高度相同 都可以影响平衡性 都比cur.r的高度差大于1 那么就对cur进行 右旋即可 如果同时存在RR与RL 就对cur进行 左旋即可
代码演示:
package class35;
import java.util.Comparator;
/**
* AVL树 平衡二叉搜索树, 左右树高度差小于2,即为平衡 搜索树表示有序,才能搜索,左中右 中序遍历是升序
* 1)最严格的平衡性,任何节点左树高度和右树高度差不超过1
* <p>
* 2)往上沿途检查每个节点时,都去检查四种违规情况:LL、RR、LR、RL
* <p>
* 3)不同情况虽然看起来复杂,但是核心点是:
* LL(做一次右旋)、RR(做一次左旋)
* LR和RL(利用旋转让底层那个上到顶部)
*/
public class AVLTreeMap1 {
//定义AVL节点类, K类型表示树的 key 支持比较 继承比较类 用于后续的大小比较找对应的节点位置
public static class AVLNode<K extends Comparable<K>, V> {
public K k; //AVL树节点的 key值
public V v; //AVL树节点的 value值
public AVLNode<K, V> l; //当前节点的左子节点
public AVLNode<K, V> r; //当前节点的右子节点
public int h; //AVL树该节点作为根节点的子树的高度 作为后续平衡性调整的关键因子
public AVLNode(K key, V value) { //构造器 初始化节点属性 键值
k = key;
v = value;
h = 1; //高度为当前节点本身 1
}
}
//定义AVL树类 AVL树类实现的有序表结构 有序表的K支持比较大小
public static class AVLTreeMap<K extends Comparable<K>, V> {
public AVLNode<K, V> root; //AVL树的根节点,只需一个根节点,就能得到整个树
public int size; //AVL树中有多少个Key,表示有多少个节点
public AVLTreeMap() {
root = null; //初始化 根节点为空
size = 0; //初始化 树节点树0
}
//右旋操作:当前cur右旋,则cur会来到右子节点位置,
//并且如果cur.l.r非空,就需要将这个子树指向 cur.l 也就是来到右子节点的左子节点 因为是基于平衡且搜索树,要保证顺序正确,
//cur.l来到cur位置 那么其cur.l.r 是比cur.l大的 因为是升序 右节点是较大的 所以就把这个cur.l.r 给到 cur的左节点 他是比cur小的 所以放左
//cur.l会来到cur的位置 最后返回刷新后的cur节点
public AVLNode<K, V> rightRotate(AVLNode<K, V> cur) {
AVLNode<K, V> left = cur.l; //右旋则需要将左子节点提到当前cur位置
cur.l = left.r; //将左节点left的右树 赋值给cur节点的左树
left.r = cur; //左节点left的右树 赋值给cur节点 这样就完成右旋 左子节点来到顶部原cur位置,cur来到其右子节点
//完成右旋,只是完成了第一步,因为右旋后会更换节点位置 也就是cur 与 cur.left两个节点 需要刷新
//他们的高度, 也就是返回各自节点的左右子树的较大值然后+1本身节点 就是新高度
//注意需要先刷新cur 因为是底部的节点, 再去刷新left的节点
cur.h = Math.max((cur.l != null ? cur.l.h : 0), (cur.r != null ? cur.r.h : 0)) + 1;
left.h = Math.max((left.l != null ? left.l.h : 0), (left.r != null ? left.r.h : 0)) + 1;
//最后需要返回当前新的cur节点 也就是cur的左节点
return left;
}
//左旋操作:同理 换个方向
public AVLNode<K, V> leftRotate(AVLNode<K, V> cur) {
AVLNode<K, V> right = cur.r; //左旋则需要将右子节点提到当前cur位置
cur.r = right.l; //将右节点right的左树 赋值给cur节点的右树
right.l = cur; //右节点right的左树 赋值给cur节点 这样就完成左旋 右子节点来到顶部原cur的位置 而cur就来到其左子节点位置
//完成左旋,只是完成了第一步,因为左旋后会更换节点位置 也就是cur 与 cur.right两个节点 需要刷新
//他们的高度, 也就是返回各自节点的左右子树的较大值然后+1本身节点 就是新高度
//注意需要先刷新cur 因为是底部的节点, 再去刷新left的节点
cur.h = Math.max((cur.l != null ? cur.l.h : 0), (cur.r != null ? cur.r.h : 0)) + 1;
right.h = Math.max((right.l != null ? right.l.h : 0), (right.r != null ? right.r.h : 0)) + 1;
//最后需要返回当前新的cur节点 也就是cur的左节点
return right;
}
//AVL树节点调整平衡 当添加节点或者删除节点 有可能涉及节点位置调换那么就有可能存在平衡被打破
//也就是当左右树高度差大于1时 就需要就分情况去进行左右旋调整平衡
//存在破坏平衡的情况可能是 LL LR RR RL 四种情况
//比如LL 就是说cur的左节点的左节点 路线是较高的一个子树导致的高度差大于1 cur进行右旋就可以修复平衡性
// LR 就是cur的左节点的右节点 路线是较高的子树 将其左节点的右节点弹到cur 也就是对左节点进行左旋,左节点的右节点位置就来到左节点,然后再对cur进行右旋,就会使得前面来到左节点的左右子节点来到cur 完成平衡
// RR 对cur 进行左旋
// RL 对cur.r 进行右旋,使得cur.r.l来到 cur.r位置 然后对cur进行左旋,使得cur.r的节点来到cur 完成平衡性
//如果同时存在LL与LR 也就是当前cur.l的左右子树高度相同 都可以影响平衡性 都比cur.r的高度差大于1 那么就对cur进行 右旋即可
//如果同时存在RR与RL 就对cur进行 左旋即可
//最后返回调整平衡性后的当前节点
public AVLNode<K, V> maintain(AVLNode<K, V> cur) {
if (cur == null) return null; //如果当前节点是空节点 直接返回空
int leftHeight = cur.l != null ? cur.l.h : 0; //定义出当前cur节点的左右子树的高度
int rightHeight = cur.r != null ? cur.r.h : 0;
if (Math.abs(leftHeight - rightHeight) > 1) { //当前左右子树如果高度差大于1 就表示不平衡,需要调整位置使得平衡
//分析情况看看左右子树那边高,
if (leftHeight > rightHeight) {
//L左子树高 那么就再获取左子树L的 左右子树高度
int leftLeftHeight = cur.l != null && cur.l.l != null ? cur.l.l.h : 0;
int leftRightHeight = cur.l != null && cur.l.r != null ? cur.l.r.h : 0;
if (leftLeftHeight >= leftRightHeight) {
//如果cur.l.l 大于等于 cur.l.r 那么我们就对当前cur节点进行依次 右旋 就可以使得整个树平衡
cur = rightRotate(cur);
} else {
//否则 cur.l.l 小于 cur.l.r 就需要对cur.l进行左旋,使得cur.l.r上移来到cur.l的位置 而cur.l就会来到cur.l.l
//这里注意需要用cur.l重新接收左旋后的新节点 函数返回的节点
cur.l = leftRotate(cur.l);
//接着再将cur节点 右旋,使得cur.l上移来到cur的位置
//然后注意需要用cur重新接收右旋后的新节点
//最终就是把cur.l.r上移到了cur位置 完成平衡调整
cur = rightRotate(cur);
}
} else {
//如果cur.r高于cur.l 那么就需要对cur的右子树进行同理的操作
int rightLeftHeight = cur.r != null && cur.r.l != null ? cur.r.l.h : 0;
int rightRightHeight = cur.r != null && cur.r.r != null ? cur.r.r.h : 0;
if (rightRightHeight >= rightLeftHeight) {
cur = leftRotate(cur); // 右节点cur.r下: 右子树高度大于等于左子树 对cur节点进行一次左旋
} else {
cur.r = rightRotate(cur.r); //右节点cur.r下: 右子树高度小于左子树 先对cur.r进行右旋 使得cur.r.l上移来到cur.r 同时要刷新cur.r 接收函数返回的当前节点
cur = leftRotate(cur); //当前节点cur: 进行左移 使得 cur.r的节点继续上移来到cur 同时要刷新cur节点 完成最终的平衡调整
}
}
}
return cur; //最后返回调整平衡后的当前节点
}
//如果插入key 那么就要找要插入的位置 如果AVL树存在 key值的节点 那么就直接返回这个节点 后续就直接修改v值
//如果不存在 那么就根据搜索顺序 往左右下移 直到空 返回的就是key的父节点
public AVLNode<K,V> findLastIndex(K key){
//定义两个辅助变量指向root 进行遍历
AVLNode<K,V> pre = root;
AVLNode<K,V> cur = root;
//开始遍历cur 节点 ,
while(cur != null){
//让pre指向cur
pre = cur;
//如果key值等于当前cur的key 那么就是找到最后经过的节点 不用再继续下去了 直接退出
if(key.compareTo(cur.k) == 0){
break;
} else if(key.compareTo(cur.k) < 0){
//如果key是比当前cur小的 那么就去cur的左子树找,因为左子树值小 区间就在左子树
cur = cur.l;
}else{
//如果key比当前cur大,那么就需要继续到cur的右子树找,找到最靠近key的节点
cur =cur.r;
}
}
return pre; //最后返回辅助变量指向的就是最终的结果
}
//获取大于等于指定的key的最靠近的节点 比如key=8 AVL树节点有1,3,6,7,13,10 那么就返回10 最接近8 且大于等于8 如果存在8 就返回8
public AVLNode<K,V> findLastNoSmallIndex(K key){
AVLNode<K,V> ans = null;
AVLNode<K,V> cur = root;
while(cur != null){
if(key.compareTo(cur.k) == 0){
//key 等于当前cur的key ans指向当前节点cur 退出
ans = cur;
break;
}else if(key.compareTo(cur.k) < 0){
//如果key 小于cur的key 说明cur是在目标范围内的,ans赋值当前节点 接着下层到做子树看看有没有更小更接近key的节点
ans = cur;
cur = cur.l;
}else{
//如果key大于cur 说明cur 不在范围内,需要往右子树 扩大到 大于等于key值
cur = cur.r;
}
}
return ans; //最后返回ans 节点
}
//获取小于等于指定的key的最靠近的节点 比如key=8 AVL树节点有1,3,6,7,13,10 那么就返回7 最接近8 且小于等于8 如果存在8 就返回8
public AVLNode<K,V> findLastNoBigIndex(K key){
AVLNode<K,V> ans = null;
AVLNode<K,V> cur = root;
while(cur != null){
if(key.compareTo(cur.k) == 0){
ans = root;
break;
} else if(key.compareTo(cur.k) > 0){ //key 大于 cur.k 说明是在范围内 赋值ans 并且接着往右子树扩大看有没有更接近的节点
ans = cur;
cur = cur.r;
} else{
cur = cur.l;
}
}
return ans;
}
//添加一个K,V值,传入的root节点从整个AVL树的根节点开始往下遍历
//因为添加的过程可能会需要调整平衡性,并且会不断往最低个下层 找到位置添加 然后再返回新调整好的节点返回上层 就需要递归
//为了方便 我们递归函数给一个 返回当前节点的返回值
public AVLNode<K,V> add(AVLNode<K,V> cur, K key, V value){
if(cur == null){
//如果是一个空的树,那么添加的节点 就直接返回一个新节点就可以了
return new AVLNode<>(key,value);
} else{
//如果非空,那么就判断大小 如果key 比cur的大 那么就需要cur.r 去递归右子树 否则就是递归左子树 并且要返回接收刷新节点左右子树
//这里没有等于的情况,因为会在 调用添加节点的 put方法去提前做相等的判断
if(key.compareTo(cur.k) > 0){
cur.r = add(cur.r,key,value);
}else{
cur.l = add(cur.l,key,value);
}
//然后需要刷新当前cur节点的高度
cur.h = Math.max(cur.l != null ? cur.l.h : 0 , cur.r != null ? cur.r.h : 0) + 1;
//然后再调整cur的平衡性
return maintain(cur);
}
}
//在cur的AVL树上,删掉key的对应节点 , 返回树的新头节点,因为可能会涉及平衡调整 ,头节点需要返回
public AVLNode<K,V> delete(AVLNode<K,V> cur, K key){
if(key.compareTo(cur.k) > 0){
//key 大于 cur 那么就递归 cur右子树 并且返回新的右子树头节点
cur.r = delete(cur.r, key);
} else if(key.compareTo(cur.k) < 0){
//key 小于 cur 递归cur左子树
cur.l = delete(cur.l, key);
} else{ //相等值,那么就表示要删的节点找到了
if(cur.l == null && cur.r == null){
cur = null; //如果没有左右子节点 直接删除 cur赋值空
} else if(cur.l != null && cur.r == null){ //左非空 右空 直接将左子节点上移
cur = cur.l;
} else if(cur.r != null && cur.l == null){ //右非空 左空 右子节点上移
cur = cur.r;
} else {
//左右子节点非空,那么删除cur节点 就需要找后继节点 替换cur 也就是cur的右子树的最左子节点,这个节点就是接着比cur大的最接近的下个节点
//思路就是 先在cur.r子树中 找到最左子节点 然后把该节点 调用delete 把这个cur.r子树的 最左子节点删除
//然后再把节点指向重新指向 把最左子节点提上来,他的左右子树 就是cur的左右子树
AVLNode<K,V> next = cur.r;
while(next.l != null){
next = next.l; //下沉到cur右子树的最左节点 直到为空停 next就是最左子节点
}
cur.r = delete(cur.r,next.k); //调用delete方法 把cur.r的子树的 最左节点next删除 并返回新头节点给到cur.r
next.l = cur.l; //删除子树的最左节点 next已经保存了最左节点 提到被删除cur节点位置 将cur的左右子树 赋值给next节点
next.r = cur.r;
cur = next; //最后cur节点再指向新的节点
}
}
//来到这里 就已经把cur的值删除 并且挂好了对应的新节点 接着判断 cur的高度 并且 要向上刷新是否需要调整平衡性 调用函数返回
if(cur != null){//注意可能删除key之后 就是空树 所以要判断非空 再刷新高度
cur.h = Math.max(cur.l != null ? cur.l.h : 0, cur.r != null ? cur.r.h : 0) + 1;
}
return maintain(cur);
}
//返回整个树的节点个数
public int size(){
return size;
}
//返回是否树中存在key的对应节点
public boolean containsKey(K key){
if(key == null) return false; //空值 直接返回false
AVLNode<K,V> node = findLastIndex(key); //通过调用对应函数 找到key对应需要插入的位置 如果存在key 返回就是对应节点 否则就是返回key的父节点 插入左或者右节点
return node != null && key.compareTo(node.k) == 0; //如果节点非空 并且值节点 返回true 否则就是false 空树的情况下node就会为空
}
//给外部调用的 添加节点方法,添加到整个树
public void put(K key,V value){
if(key == null) return; //如果key为空 返回退出返回
AVLNode<K,V> node = findLastIndex(key); //调用函数 找到key值对应的节点位置
if(node !=null && key.compareTo(node.k) == 0){
node.v = value; //如果节点非空 key值就等于node 那么就是直接覆盖当前node的value值
}else {
//如果不相等 那么就是新节点 size先加1 然后调用add函数 在root整个树种加入 key,value 最后在把新头节点 返回给root 因为可能会涉及平衡性调整位置
size++;
root = add(root, key, value);
}
}
//给外部调用 移除节点的方法
public void remove(K key){
if(key == null) return; //空值 直接退出返回
if(containsKey(key)){ //删除前先判断 是否树种存在key 存在那么就size-- 然后调用delete函数 对整个树root进行删除key 最后返回新的头节点返回给root
size--;
root = delete(root, key);
}
}
//给外部调用 获取节点key对应的value
public V get(K key){
if(key == null) return null; //如果空 就直接返回Null
AVLNode<K,V> node = findLastIndex(key); //调用函数找到对应key节点weiz
if(node != null && key.compareTo(node.k)==0){
//节点非空且是等于key 说明找到了节点 返回对应的v
return node.v;
}
return null; //如果没有找到直接返回Null
}
//外部调用 获取树的第一个值 树是有序的 按中序遍历 左中右是升序 所以第一个节点就是 整个树的最左节点就是最小的节点
public K firstKey(){
if(root == null) return null;
AVLNode<K,V> node = root; //获取root节点
while (node.l != null){
node = node.l; //节点一直往左子节点下沉直到空
}
return node.k; //找到最左节点 返回K
}
//外部调用 获取树的最后一个值 树是有序的 按中序遍历 左中右是升序 所以最后一个节点就是 整个树的最右节点就是最大的节点
public K lastKey(){
if(root == null) return null;
AVLNode<K,V> node = root; //获取root节点
while (node.r != null){
node = node.r; //节点一直往右子节点下沉直到空
}
return node.k; //找到最右节点 返回K
}
//外部调用 获取对应key 小于等于key的最大的一个节点 也就是最接近Key的节点
//比如key=8 AVL树节点有1,3,6,7,13,10 那么就返回7 最接近8 且小于等于8 如果存在8 就返回8
public K floorKey(K key){
if(key == null) return null;
AVLNode<K,V> node = findLastNoBigIndex(key); //调用函数 找到小于等于key 最接近key的树中的节点返回
return node == null ? null : node.k;
}
//外部调用 获取对应key 大于等于key的最小的一个节点 也就是最接近Key的节点
//比如key=8 AVL树节点有1,3,6,7,13,10 那么就返回10 最接近8 且大于等于8 如果存在8 就返回8
public K ceilingKey(K key){
if(key == null) return null;
AVLNode<K,V> node = findLastNoSmallIndex(key); //调用函数 找到大于等于key 最接近key的树中的节点返回
return node == null ? null : node.k;
}
}
}