学习数据结构 AVL树

一曲冷凌霜 提交于 2021-01-31 09:06:54

前一篇学习了二叉搜索树,本篇试图学习 AVL 树。可惜的是,Mehta 教授的《数据结构基础》一书中没有给出
删除算法,我还没研究清楚,只好先不写(学)删除部分了。

修改后的 TestTree.java 文件在这里: http://vdisk.weibo.com/s/3fAzF

以下是 insert 部分的代码,注释我已经写得挺清楚的了:

public void insert(K key, V value) {
    this.checkValid();  // 检查现在树的有效性。
    internal_insert(key, value);  // 实际执行插入。
    this.checkValid();  // 插入之后再检查一次。
  }

  /** 内部实现 insert, 返回插入或更新的节点对象。 */
  private boolean internal_insert(K key, V value) {
    if (this.root == null) {  
      // 特定情况,简单处理。
      this.root = new TreeNode<K,V>(key, value);
      return true;
    }
    
    // 第一步:查找 key 的插入位置。
    boolean found = false;
    TreeNode<K, V> a = root,  // 离插入点最近的 bf=+-1 的节点,也可能为 root(其 bf 可能为 0)。
      f = null,        // a 的父节点,可能为 null。
      p = root,        // p 用于根据 key 访问节点树。
      q = null,        // q 是 p 的父节点,可能为 null。
      y = null;        // y 是新插入的节点,或原来已经在树中的节点。
    while (p != null && !found) {  // p 遍历从 root -> key 节点的整个路径。
      if (p.bf != 0) {
        a = p;    // 记录 a 为最接近的那个 bf=+-1 的节点。
        f = q;
      }
      int comp = key.compareTo(p.key);
      if (comp < 0) {
        q = p;    // 如果 key < p.key 则查找左子树,q = p.parent。
        p = p.left;
      }
      else if (comp > 0) {
        q = p;    // 如果 key > p.key 则查找右子树。
        p = p.right;
      }
      else {      // key == p.key,则 y 已经在树中了。
        y = p;
        found = true;
      }
    } // end of while: 查找插入位置。
    if (found) {
      y.setValue(value);
      return false;
    }
    
    // 第二步:插入新节点并重新平衡树。现在 key 不在树中,并应作为 pp 的某个
    //   子节点插入到树中。
    assert(q != null);    // 一定非空,因至少有 root 节点。
    assert(key.compareTo(q.key) != 0);  // key != pp.key
    y = new TreeNode<K, V>(key, value);  // 要插入的新节点。
    if (key.compareTo(q.key) < 0)
      q.left = y;  // key < pp.key,作为左子树插入到 pp 中。
    else
      q.right = y;  // key > pp.key,作为右子树插入到 pp 中。
    
    // 调整从节点 a->pp 路径上的节点的平衡因子 bf(balance factor)
    
    int d;    // d = +-1 表示要调整的 bf 的大小。+1 意味着插入到了左子树,-1 意味着插入到了右子树。
    TreeNode<K, V> b,  // a 的子节点,可能是 Al|Ar。
             c;  // b 的子节点,可能是 Bl|Br。
    if (key.compareTo(a.key) > 0) {  // (k > a.key)
      b = p = a.right;  // 插入到了 a 的右边,则设置 b,p 是 a 的右子树。
      d = -1;        // d=-1 意味着插入到了右子树。
    } else {
      b = p = a.left;    // 插入到了 a 的左边,则设置 b,p 是 a 的左子树。
      d = +1;        // d=+1 意味着插入到了左子树。
    }
    
    // 遍历从 p=a.child -> y 的节点路径,这些节点的 bf=0,根据插入节点的方向调整 bf 值。
    while (p != y) {
      assert(p.bf == 0);
      if (key.compareTo(p.key) > 0) {
        p.bf = -1;    // 被插入到了右边,则 bf 右倾斜 -1
        p = p.right;
      } else {
        p.bf = +1;    // 被插入到了左边,则 bf 左倾斜 +1
        p = p.left;
      }
    } // end of while (p != y);
    
    // a 节点原来 a.bf=+-1,如果更改了则还平衡吗?
    if (is_balance(a.bf + d)) {    // is_balance 为 Math.abs(a.bf+d) <= 1
      a.bf += d;
      return true;
    }
    
    // 新的 a.bf = 2 或者 -2
    assert((a.bf + d) == 2 || (a.bf + d) == -2);
    if (d == 1) {      // 插入到左子树,则 a.bf = 2 (必左倾斜)
      assert(a.bf + d == 2);
      if (b.bf == 1) {  // b 是 a 的子节点,则插入到了 B.left 方向。
        // 执行一次 LL 旋转。A.right 不变; A.left=原B.right; B.left 不变; B.right=A
        // 旋转之后,A.bf=0,因 h(Al)==h(Ar);B.bf=0,因h(Bl)=h(Br)=h(A)
        a.left = b.right;
        b.right = a;
        a.bf = 0; b.bf = 0;
      } else {      // 插入到了 B.right 右子树,则 b.bf = -1
        assert(b.bf == -1);
        // 执行一次 LR 旋转(相当于 RR+LL 旋转)。参见书上的 图10.13。
        c = b.right;
        b.right = c.left;
        a.left = c.right;
        c.left = b; c.right = a;
        // 根据原来的 c.bf 值(可能值是 -1,0,1) 分别计算新的 a,b,c 的 bf 值。
        if (c.bf == 1) {      // h(Cl)=h, h(Cr)=h-1,则 h(new_B)=0, h(new_A)=-1
          b.bf = 0; a.bf = -1;
        } else if (c.bf == -1) {  // h(Cl)=h-1, h(Cr)=h,则 h(new_B)=1, h(new_A)=0
          b.bf = 1; a.bf = 0;
        } else if (c.bf == 0) {    // h(Cl)==h(Cr)==h,则 h(new_B)=0, h(new_A)=0
          b.bf = 0; a.bf = 0;
        }
        c.bf = 0; b = c;  // b 现在指向新的根(替换了原来的 a)
      } // end of if
    } else {
      assert(d == -1); assert(a.bf + d == -2);
      // 插入到了右子树,则 a.bf = -2(必右倾斜)
      if (b.bf == -1) {  // 插入到了 B 的右子树,则执行 RR 旋转。
        a.right = b.left;
        b.left = a;
        a.bf = 0; b.bf = 0;    // 旋转之后,h(Al)=h(new_ar=old_Bl)=h, h(new_Bl=A)=h(Br)=h+1
      } else {      // 要执行 RL 旋转。
        assert(b.bf == 1);
        c = b.left;    // 书上没有图了,需要自己画图。
        a.right = c.left;
        b.left = c.right;
        c.left = a; c.right = b;
        // 根据 c.bf 值(可能为 -1,0,1),分别计算新的 a,b,c 的 bf 值。
        if (c.bf == 1) {
          a.bf = 0; b.bf = -1;  // bf(A)=h(Al)-h(Cl)=h-h=0; bf(B)=h(Cr)-h(Br)=(h-1)-h=-1
        } else if (c.bf == -1) {
          a.bf = 1; b.bf = 0;    // bf(A)=h(Al)-h(Cl)=h-(h-1)=1; bf(B)=h(Cr)-h(Br)=h-h=0
        } else if (c.bf == 0) {
          a.bf = 0; b.bf = 0;    // bf(A)=h-h=0; bf(B)=h-h=0
        }
        c.bf = 0; b = c;  // 同 LR 中。
      }
    }
    
    // f 是 a 的父节点,如果 a 是 root 则 f == null。经旋转之后 b(或c) 现在替代了 a 。
    if (f == null) this.root = b;
    else if (a == f.left) f.left = b;
    else if (a == f.right) f.right = b;
    
    return true;
  }

为方便补上 RL 旋转部分,画图如下:

AVL RL 旋转示意图

比较费事的地方是根据 bf(C)=-1,0,1 三种情况分别要计算旋转后的 bf(A), bf(B) 的值。用图可以方便地
标记旋转后的 CL, CR 的高度(一般为 h 或 h-1),然后计算出 bf(A), bf(B) 的值。 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!