彻底搞懂AVL树

更新:
经过很多朋友的提醒, 本文的 insert()delete() 两个算法存在一些问题, 由于笔者最近略忙一直没有时间修改, 现在先给出一个两年前实现的源码链接, 关于那两步的算法请先参看源码!

做一次标题党,平常很少用这个“彻底搞懂”这几个字,最近在学AVL树,用这几个字希望自己是真的懂了。同时也希望本文可以帮助大家理解AVL树,以及它的一些基本操作。

什么是AVL树

AVL树,是一种平衡(balanced)的二叉搜索树(binary search tree, 简称为BST)。由两位科学家在1962年发表的论文《An algorithm for the organization of information》当中提出,作者是发明者G.M. Adelson-VelskyE.M. Landis(链接由维基百科提供)。它具有以下两个性质:

  • 任意一个结点的key,比它的左孩子key大,比它的右孩子key小;
  • 任意结点的孩子结点之间高度差距最大为1;

所以,在代码当中,AVL树的结构应该是这个样子的(本文的具体代码采用Java语法):

class AVLNode {
  AVLNode left;
  AVLNode right;
  int height;
  int key;
  ArrayList<Object> values;
}

基本操作(API)

对于一棵AVL树来说,它的应该对外部提供的接口(Application Interface, 简称API)有:

  • 往树中插入(insert)一个结点;
  • 删除(delete)树中已经存在的某个结点;
  • 搜索(search)树中某个结点;
  • 返回当前结点在中序遍历当中的后一个结点(successor);
  • 返回当前结点在中序遍历当中的前一个结点(predecessor);
  • 返回一个包含该AVL树当中的所有结点的有序集合(按照key的升序排列);

具体的接口文件如下所示:

// Assume the node of AVL tree is represented by AVLNode
interface AVLTree {
  /* return the inserted node */
  AVLNode insert(int key, Object value);
  /* return the deleted node, if no that node with the given key, return null */
  AVLNode delete(int key);
  /* return the node with the given key, if no that node that key, return null */
  AVLNode search(int key);
  /* return a list with all nodes in that tree in their key's increasing order */
  ArrayList<AVLNode> allNodes();
  /* return the next node of the given node when preforming inorder traversal */
  AVLNode next(AVLNode node);
  /* return the previous node of the given node when preforming inorder traversal */
  AVLNode prev(AVLNode node);
}

辅助操作

为了实现上述的基本操作,我们会需要一些辅助的操作。因为AVL树是一种平衡树,所以每次增加或者减少树中的元素都有可能使这棵树由平衡变得不平衡,所以我们需要一种机制来检测这棵树是否平衡,以及当它不平衡的时候,我们应该通过某些操作使它重新平衡(rebalanced)。

平衡检测

对于一棵树来说,它的高度(height)定义如下:

从根节点(root)开始到某一个叶子节点(leaf)的最长路径(path)上结点的个数

根据AVL树的定义,我们可以为所有的结点定义一个平衡因子(balanced factor)

某个结点的平衡因子等于该节点的左孩子的高度减去右孩子的高度

根据平衡树的定义,计算得到的平衡因为会出现两种情况:

  • 如果平衡因子是0, 1, -1 这三个数的话,可以认定该节点是符合平衡树的定义的;
  • 否则,该结点不平衡,需要重新平衡;

对于一个BST来说,每次插入的元素只可能放在叶子结点上。所以只能影响某个子树是否平衡,对其他子树不会有任何的影响。在这种情况下,我们只需要根据搜索的路径,从孩子往祖先找,如果有不平衡的结点就可以被找到。如果一直到根结点都没有发现不平衡结点,则可以认为这次的插入操作没有造成树的不平衡。

int getBalancedFactor(AVLNode node) {
  if (node != null) 
    return node.left.height - node.right.heigh;
  return -1;
}

重平衡

如果发现了某个不平衡的结点,那么就需要对该结点进行重平衡。实现重平衡的方法,是对该节点的子树进行旋转(rotation)

旋转有两种情况,如下图所示:

  • 一种称为左旋转(关于X结点的左旋转);
  • 一种称为右旋转(关于Y结点的右旋转);
旋转示意图

在上图的基础上,用代码实现这个旋转很简单,代码如下:

AVLNode rightRotation(AVLNode y) {
  AVLNode x = y.left;
  AVLNode T2 = x.right;
  x.right = y;
  y.left = T2;
  y.height = Math.max(y.left.height, y.right.height);
  x.height = Math.max(x.left.height, x.right.height);
  return x;
}

AVLNode leftRotation(AVLNode x) {
  AVLNode y = x.rigth;
  AVLNode T2 = y.left;
  x.right = T2;
  y.left = x;
  x.height = Math.max(x.left.height, x.right.height);
  y.height = Math.max(y.left.height, y.right.height);
  return y
}

上述,是原理性的操作。在真实的情况下,我们会遇到四种可能出现的情况 (注意下图当中的x,y, 不可以直接和上图的对应) :

四种可能情况意图

图中的x, y, z 所代表的含义如下:

  • z, 代表从插入元素的位置开始,逆插入元素时的访问结点的顺序,从孩子向祖先方向开始检测平衡因子时发现的第一个不平衡结点;
  • y,代表插入元素时的访问结点路径上,访问z结点之后访问的结点
  • x,代表插入元素时的访问结点路径上,访问y结点之后访问的结点

这四种情况,都可以通过一次或者两次的旋转,来使得不平衡的结点变平衡。其中,case1, case4可以通过一次旋转(singly rotation)重新平衡,case2, case3可以通过两次旋转(doubly rotation)重新平衡。

单次旋转(singly rotatioin)

单次旋转得到重新平衡的子树的示意图如下所示,需要注意的是分清对应的结点。

单次旋转示意图

在有了前面的辅助方法的情况下,我们可以写如下的针对case1case4的旋转代码:

AVLNode rebalanced(AVLNode node, int insertedKey) {
  balancedFactor = getBalancedFactor(node);
  if (balancedFactor > 1 && insertedKey < node.key)
    return rightRotation(node);     //case 1
  if (balancedFactor < -1 && insertedKey > node.key)
    return leftRotation(node);      //case 4
  
  // ......
  // miss other 2 possibilities
  
  // if do not need rebalanced (all conditions cannot satisfied)
  // then return the current node
  return node;
}

双次旋转(doubly rotatioin)

双次旋转顾名思义,就是要进行两次旋转来使子树重新平衡,流程如下图示:

  • case2情况下,需要先对y结点进行一次左旋转,然后再对z结点进行一次右旋转;
  • case3情况下,需要先对y结点进行一次右旋转,然后再对z结点进行一次左旋转;
双次旋转示意图

接着上面的代码来写:

AVLNode rebalanced(AVLNode node, int insertedKey) {
  balancedFactor = getBalancedFactor(node);
  if (balancedFactor > 1 && insertedKey < node.key)
    return rightRotation(node);     //case 1
  if (balancedFactor < -1 && insertedKey > node.key)
    return leftRotation(node);      //case 4
  if (balancedFactor > 1 && insertedKey > node.left.key) {
    // case 2
    node.left = leftRotation(node.left);
    return rightRotation(node);
  }
  if (balancedFactor < -1 && insertedKey < node.left.key) {
    // case 3
    node.right = rightRotation(node.right);
    return leftRotation(node);
  }
  // if do not need rebalanced (all conditions cannot satisfied)
  // then return the current node
  return node;
}

搜索子树中的特殊元素

根据BST树的性质,我们可以在不搜索整棵树的前提下,查找某些特殊的元素,比如最小key的元素以及最大key的元素。这两个操作,可以O(logn)的时间内完成。

搜索子树中最大元素

在一棵子树当中,因为右孩子的key始终大于当前结点key,所以拥有最大key的元素必定位于其最深层的右孩子结点处。

AVLNode maxOfSubtree(AVLNode node) {
  while (node.right != null) node = node.right;
  return node;
}

搜索子树中最小元素

在一棵子树当中,因为左孩子的key始终小于当前结点key,所以拥有最小key的元素必定位于其最深层的左孩子结点处。

AVLNode minOfSubtree(AVLNode node) {
  while (node.left != null) node = node.left;
  return node;
}

基本操作的具体实现

有了上面的辅助操作,我们可以考虑如何具体的实现前面提到的几种基本操作了。对于一棵树的操作,遍历它的结点可以采用递推或者递归的方法来进行,本文尽量采用递归的方法使代码看起来更加的优雅。

插入元素(Insert)

在一棵AVL树中插入一个元素的逻辑应该是这样子的:

  1. 标准的BST插入元素操作,找到该元素应该被放置的叶子结点,将该元素连接上去;
  2. 检查这次操作是否破坏了树的平衡,若是,通过旋转维护平衡特性;
// assume root point to the root of the tree
AVLNode insert(AVLNode node, int key, Object value) {
  // if the tree is empty
  if (node == null) {
    root = new AVLNode(key, value);
    return root;
  }
  if (key > node.key)
    return insert(node.right, key, value);
  else if (key < node.key)
    return insert(node.left, key, value);
  else {
    // if key is already in the tree, here I will update the value
    node.value = value;
    return node;
   }
  // update the height of the node in insertion path
  node.height = 1 + Math.max(node.left.height, node.right.height);
  return rebalanced(node, key);
}

删除元素(Delete)

在一棵树中,删除某个元素,逻辑应该是这样子的:

  1. 搜索给定的key,确定其是否在树中;
  2. 如果不在树中,返回null;如果在树中,执行标准的BST删除操作,并返回该删除的结点;
  3. 检查被删除结点的所有祖先结点是否平衡,如果不平衡,则执行重平衡操作;

标准的BST删除操作,必须维持BST的性质,应该是这样子的:

  1. 找到要删除的结点;
  2. 找到中序遍历下,该节点的下一个结点,把这个结点移到要删除的结点的位置;
  3. 返回被删除的结点;

寻找中序遍历下,某个结点的下一个结点又会出现以下几种情况:

  • 该结点没有或只有一个孩子:
    • 若没有孩子,直接移除这个结点;
    • 若有且仅有一个孩子,用该孩子顶替将要被删除结点的位置;
  • 该结点有两个孩子:
    • 寻找该节点的右子树的最小元素 (即该结点右子树最深层的左孩子);
AVLNode delete(AVLNode node, int key) {
  if (root == null) return null;
  if (key > node.key)
    return delete(node.right, key);
  else if (key < node.key)
    return delete(node.left, key);
  
  // if the target key is found
  if ((node.left == null) || (node.right == null)) {
    // if there is only a child or no child
    if ((node.left == null) && (node.right == null)) {
      node = null;
    }else if ((node.left != null) && (node.right == null)) {
      node = node.left;
    }else if ((node.left == null) && (node.right != null)) {
      node = node.right;
    }
  } else {
    // if there is two child
    AVLNode temp = minOfSubTree(node.right);
    // copy the key and values to the current node
    node.key = temp.key;
    node.values = temp.values;
    node.right = delete(node.right, temp.key);
  }
  
  // if node has no child just remove itself
  if (node == null) return null;
  // otherwise, update the height after deletion because we just 
  // replace the target node with another node, so the height of 
  // the node's children will never change
  node.height = 1 + Math.max(node.left, node.right);
  // rebalanced
  return rebalanced(node, key);
}

搜索(search)

在AVL树中搜索和在BST中的搜索是完全一样的,根据左孩子key小于根结点key,右结点key大于根结点key的性质:

AVLNode search(AVLNode node, int key) {
  if (node != null) {
    if (key == node.key) return node;
    if (key > node.key) return search(node.right, key);
    if (key < node.key) return search(node.left, key);
  }
  return null;
}

前一个元素(successor)

这里所说的前一个元素,以及下文所说的后一个元素是指AVL树在中序遍历的情况下的前一个元素和后一个元素。因为在BST当中,中序遍历可以将所有的元素按照key升序 (如果不允许重复的元素出现)排列。

首先,需要知道什么是中序遍历:

先访问结点的左孩子,然后访问该结点,最后访问该结点的右孩子。

中序遍历,可以用如下的伪码 (递归的方式)表示:

inOrderTraversal(Node node)
  if (node.left != null) 
    inOrderTraversal(node.left)
      visit(node)
  if (node.right != null)
    inOrderTraversal(node.right)

在AVL树中,寻找符合上述要求的前一个结点,可能会遇到以下两种情况:

  • 当前结点有左子树,查找左子树的最大元素 (即当前结点的左子树的最深层右孩子)
  • 当前结点没有左子树:向上查找祖先结点,直到找到第一个比当前结点的key小的结点,返回之;若查到root都没有,则证明当前结点没有前一个元素,返回null;
AVLNode prev(AVLNode root, int key) {
  AVLNode curNode = root;
  if (curNode == null) return null;
  Stack<AVLNode> visited = new Stack<>();
  while (curNode != null) {
    visited.push(curNode);
    if (key > curNode.key) {
      curNode = curNode.right;
      continue;
    } else if (key < curNode.key) {
      curNode = curNode.left;
      continue;
    }
    // if the target key is found, check whether
    // it has left subtree or not
    if (curNode.left != null) 
      return maxOfSubtree(curNode.left);
    else {
      // pop out the current node itself
      visited.pop();
      while (visited.peek().key > key) {
        // if the top element's key is greater than
        // the given key, keep check its parent
        visited.pop();
        if (visited.isEmpty())
          // if reach to the root
          return null;
      }
      return visited.pop();
    }
  }
  // if reach here means the given key
  // do not exit in the tree
  return null;
}

后一个元素(predecessor)

同上理,在AVL树中,寻找符合上述要求的后一个结点,可能会遇到以下两种情况:

  • 当前结点有右子树,查找右子树的最小元素 (即当前结点的右子树的最深层左孩子)
  • 当前结点没有右子树:向上查找祖先结点,直到找到第一个比当前结点的key大的结点,返回之;若查到root都没有,则证明当前结点没有后一个元素,返回null;
AVLNode nextKey(AVLNode root, int key) {
  AVLNode curNode = root;
  Stack<MyAVLNode> visited = new Stack<>();
  while (curNode != null) {
    visited.push(curNode);
    if (key > curNode.key) {
      curNode = curNode.right;
      continue;
    }
    if (key < curNode.key) {
      curNode = curNode.left;
      continue;
    }
    if (curNode.right != null) {
      return minOfSubTree(curNode.right);
    } else {
      // if the node do not has right subtree
      visited.pop();
      while (visited.peek().key < key) {
        // if the top element is less than the given key
        // pop them out, search for the ancestor
        visited.pop();
        if (visited.isEmpty())
          // reach the root
          return null;
      }
      // find the first ancestor whose key is greater than the given key
      return visited.pop();
    }
  }
  return null;
}

获取树中所有结点的有序集合

因为要求返回包含该AVL树当中的所有结点按照key的升序排列的有序集合,很显然可以通过中序遍历整棵树得到,上文已经给出了中序遍历的伪码,只需要实现它即可:

ArrayList<AVLNode> allNodes() {
  ArrayList<AVLNode> list = new ArrayList<>();
  inOrderTraversal(this.root, list);
  return list;
}

void inOrderTraversal(Node node, ArrayList<AVLNode> list) {
  if (node.left != null) inOrderTraversal(node.left);
  list.add(node);
  if (node.right != null)inOrderTraversal(node.right);
}

以上就是AVL树的内容,测试文件整理一下再上传。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容