AVL树

AVL树简介

AVL树的名字来源于它的发明作者G.M. Adelson-Velsky 和 E.M. Landis。AVL树是最先发明的自平衡二叉查找树(Self-Balancing Binary Search Tree,简称平衡二叉树)。一棵AVL树有如下必要条件:

  • 条件一:它必须是二叉查找树。
  • 条件二:每个节点的左子树和右子树的高度差至多为1。

AVL树

 

 

 图一中左边二叉树的节点45的左孩子46比45大,不满足二叉搜索树的条件,因此它也不是一棵平衡二叉树。 右边二叉树满足二叉搜索树的条件,同时它满足条件二,因此它是一棵平衡二叉树。

 AVL树

 

 

 左边二叉树的节点45左子树高度2,右子树高度0,左右子树高度差为2-0=2,不满足条件二; 右边二叉树的节点均满足左右子树高度差至多为1,同时它满足二叉搜索树的要求,因此它是一棵平衡二叉树。

AVL树的查找、插入、删除操作在平均和最坏的情况下都是O(logn),这得益于它时刻维护着二叉树的平衡。如果我们需要查找的集合本身没有顺序,在频繁查找的同时也经常的插入和删除,AVL树是不错的选择。不平衡的二叉查找树在查找时的效率是很低的,因此,AVL如何维护二叉树的平衡是我们的学习重点。

AVL树相关概念

平衡因子:将二叉树上节点的左子树高度减去右子树高度的值称为该节点的平衡因子BF(Balance Factor)。在图二右边的AVL树上:节点50的左子树高度为3,右子树高度为2,BF= 3-2 = 1;节点45的左子树高度为2,右子树高度为1,BF= 2-1 = 1;节点46的左子树高度为0,右子树高度为0,BF= 0-0 = 0;节点65的左子树高度为0,右子树高度为1,BF= 0-1 = -1。对于平衡二叉树,BF的取值范围为[-1,1]。如果发现某个节点的BF值不在此范围,则需要对树进行调整。

最小不平衡子树:距离插入节点最近的,且平衡因子的绝对值大于1的节点为根的子树。

AVL树

 

 

在图三中,左边二叉树的节点45的BF = 1,插入节点43后,节点45的BF = 2。节点45是距离插入点43最近的BF不在[-1,1]范围内的节点,因此以节点45为根的子树为最小不平衡子树。

 

AVL树的实现详解

1. 节点结构

struct AVLTreeNode {
  AVLTreeNode(T value, AVLTreeNode<T>*l, AVLTreeNode<T>*r) :key(value), lchild(l), rchild(r){}
  T key; 
  int height; 
  AVLTreeNode<T>* lchild; 
  AVLTreeNode<T>* rchild;
};

 

AVL的节点结构为AVLTreeNode,它包括:

  • key:节点的值;
  • height: 节点的高度,用于计算父节点的平衡因子;
  • lchild : 若节点有左子树,则lchild指向节点的左孩子,否则指向nullptr;
  • rchild : 若节点有右子树,则rchild指向节点的右孩子,否则指向nullptr。

在另外一些AVL实现的节点设计方案中,会把BF作为结点的一个属性存储起来,而在这里我们存储的是节点的高度,通过节点的高度我们也可以间接计算出节点的BF。例如节点A的左孩子的height = 2,右孩子的height = 1,那么节点A的平衡因子为2 - 1 = 1.

2. AVL树的抽象数据结构(ADT)

template<typename T>
class AVLTree
{
public:
  AVLTree(); //构造函数
  ~AVLTree(); //析构函数

  void preOrder(); //前序遍历AVL树
  void InOrder(); //中序遍历AVL树 
  void postOrder(); //后序遍历AVL树

  void print(); //打印AVL树
  void destory(); //销毁AVL树

  void insert(T key); //插入指定值的节点
  void remove(T key); //移除指定值的节点

  AVLTreeNode<T>* search_recurse(T key); //利用递归算法进行指定值的查找
  AVLTreeNode<T>* search_iterator(T key); //利用迭代算法进行指定值的查找
  T minimum(); //返回AVL中的最小值
  T maximum(); //返回AVL中的最大值

  int height(); //返回树的高度

private:
  AVLTreeNode<T>* root; //AVL树的根节点

private:
  void preOrder(AVLTreeNode<T>* pnode) const;
  void inOrder(AVLTreeNode<T>* pnode) const;
  void postOrder(AVLTreeNode<T>* pnode) const;

  void print(AVLTreeNode<T>* pnode,T key, int direction) const;
  void destory(AVLTreeNode<T>* & pnode);

  AVLTreeNode<T>* insert(AVLTreeNode<T>* &pnode, T key); 
  AVLTreeNode<T>* remove(AVLTreeNode<T>* & pnode, AVLTreeNode<T>* pdel); //删除AVL树中节点pdel,并返回被删除的节点

  AVLTreeNode<T>* minimum(AVLTreeNode<T>*pnode)const;
  AVLTreeNode<T>* maximum(AVLTreeNode<T>*pnode)const;

  AVLTreeNode<T>* search_recurse(AVLTreeNode<T>* pnode, T key) const;
  AVLTreeNode<T>* search_iterator(AVLTreeNode<T>* pnode, T key) const;

  AVLTreeNode<T>* leftRotation(AVLTreeNode<T>* pnode); //单旋:左旋操作
  AVLTreeNode<T>* rightRotation(AVLTreeNode<T>* pnode); //单旋:右旋操作
  AVLTreeNode<T>* leftRightRotation(AVLTreeNode<T>* pnode); //双旋:先左旋后右旋操作
  AVLTreeNode<T>* rightLeftRotation(AVLTreeNode<T>* pnode); //双旋:先右旋后左旋操作
};

 

这里我们定义了AVL树这个类型AVLTree。它包含了:

  • AVL树的根节点root,这是唯一的数据成员。
  • 操作的外部接口与内部实现接口。例如 preOrder()为提供给用户使用的接口,接口声明为public;而preOrder(AVLTreeNode* pnode)是类内部为了递归操作所使用的接口,接口声明为private。
  • 旋转操作(rotation)用来调整失去平衡的二叉树,四个内部接口针对四种失衡情况进行调整,后面详细解释。


3. AVL树的高度

如前所说,我们的节点结构中并不存储结点的BF,取而代之的是节点的高度。一个节点的BF可由其左右子树的高度计算出来。我们提供返回一个节点高度的操作:

/*返回一棵树的高度*/
template <typename T>
int AVLTree<T>::height(AVLTreeNode<T>* pnode)
{
  if (pnode != nullptr)
  {
  return pnode->height;
  }
  return 0; //如果是空树,高度为0
};

template <typename T>
int AVLTree<T>::height()
{
  return height(root);
};

 

4. AVL树失衡调整

节点的插入或删除都有可能导致AVL树失去平衡,因此,失衡调整是插入与删除操作的基础。 AVL树的失衡调整可以分为四种情况,我们逐一分析。 假设我们要为数组a[]={4,5,6,3,2,8,7,0,1}构建一棵AVL树。

情况一:左单旋转

首先插入{4,5,6},在插入元素6后出现不平衡的情况:

AVL树

 

 

当我们在右子树插入右孩子导致AVL失衡时,我们需要进行单左旋调整。旋转围绕最小失衡子树的根节点进行。 在删除新节点时也有可能会出现需要单左旋的情况。 左旋代码如下:

/*左旋转操作*/
/*pnode为最小失衡子树的根节点*/
/*返回旋转后的根节点*/
template<typename T>
AVLTreeNode<T>* AVLTree<T>::leftRotation(AVLTreeNode<T>* proot)
{
  AVLTreeNode<T>* prchild = proot->rchild;
  proot->rchild = prchild->lchild;
  prchild->lchild = proot;

  proot->height = max(height(proot->lchild),height(proot->rchild))+1; //更新节点的高度值
  prchild->height = max(height(prchild->lchild), height(prchild->rchild)) + 1; //更新节点的高度值

  return prchild; 
};

 

结合例子进行分析:

① 参数proot为最小失衡子树的根节点,在图四中为节点4;

② 若节点5有左子树,则该左子树成为节点4的右子树;

③ 节点4成为节点5的左子树;

④ 最后更新节点的高度值。

情况二:右单旋转

我们继续插入元素{3,2},此时二叉树为:

AVL树

 

 

插入3、2后出现了不平衡的情况。此时的插入情况是“在左子树上插入左孩子导致AVL树失衡”,我们需要进行单右旋调整。 单右旋代码为:

/*右旋转操作*/
/*pnode为最小失衡子树的根节点*/
/*返回旋转后的根节点*/
template <typename T>
AVLTreeNode<T>* AVLTree<T>::rightRotation(AVLTreeNode<T>*proot)
{
  AVLTreeNode<T>* plchild = proot->lchild;
  proot->lchild = plchild->rchild;
  plchild->rchild = proot;

  proot->height = max(height(proot->lchild), height(proot->rchild)) + 1; //更新节点的高度值
  plchild->height = max(height(plchild->lchild), height(plchild->rchild)) + 1; //更新节点的高度值

  return plchild;
};

 

结合例子进行分析:

① 参数proot为最小失衡子树的根节点,在图四中为节点4;

② 若节点3有右子树,则该右子树成为节点4的左子树;

③ 节点4成为节点3的左子树;

④ 调整节点的高度值。

情况三:先左旋后右旋

需要进行两次旋转的原因是第一次旋转后,AVL树仍旧处于不平衡的状态,第二次旋转再次进行调整。 我们继续插入元素{8,7}

 AVL树

 

 

 这种情况,总结起来就是“在右子树上插入左孩子导致AVL树失衡",此时我们需要进行先右旋后左旋的调整。 调整的代码为:

/*先右旋再左旋*/
/*参数proot为最小失衡子树的根节点*/
/*返回旋转后的根节点*/
template<typename T>
AVLTreeNode<T>* AVLTree<T>::rightLeftRotation(AVLTreeNode<T>* proot)
{
  proot->rchild = rightRotation(proot->rchild);
  return leftRotation(proot);
};

 

结合例子进行分析:

① 首先对最小不平衡子树的根节点(也就是节点6)的右孩子(也就是8)进行右旋操作;

② 再对节点6进行一次左旋操作;

情况四:先左旋后右旋

根据对称性原理,当我们“在左子树上插入右孩子导致AVL树失衡",此时我们需要进行先左旋后右旋的调整。如果你不理解接着看图。 我们接着插入节点{0,1}

 AVL树

 

 

调整的代码:

/*先左后右做旋转*/
/*参数proot为最小失衡子树的根节点*/
/*返回旋转后的根节点*/
template <typename T>
AVLTreeNode<T>* AVLTree<T>::leftRightRotation(AVLTreeNode<T>* proot)
{
  proot->lchild= leftRotation(proot->lchild);
  return rightRotation(proot);
};

 

结合例子进行分析:

① 首先对最小不平衡子树的根节点(也就是节点2)的左孩子(也就是0)进行左旋操作;

② 再对节点2进行一次右旋操作。

总结:四种失衡调整

类型 使用情形
单左旋 在左子树插入左孩子节点,使得平衡因子绝对值由1增至2
单右旋 在右子树插入右孩子节点,使得平衡因子绝对值由1增至2
先左旋后右旋 在左子树插入右孩子节点,使得平衡因子绝对值由1增至2
先右旋后左旋 在右子树插入左孩子节点,使得平衡因子绝对值由1增至2

 

 

 

 

 


 

5. 插入新节点

其实上面已经展示了一个完整的插入过程,结合例子很好理解下面这段代码。

/*插入操作*/
/*递归地进行插入*/
/*返回插入后的根节点*/
template <typename T>
AVLTreeNode<T>* AVLTree<T>::insert(AVLTreeNode<T>* &pnode, T key)
{
  if (pnode == nullptr) //寻找到插入的位置
  {
    pnode = new AVLTreeNode<T>(key, nullptr, nullptr);
  }
  else if (key > pnode->key) //插入值比当前结点值大,插入到当前结点的右子树上
  {
    pnode->rchild = insert(pnode->rchild, key);
    if (height(pnode->rchild) - height(pnode->lchild) == 2) //插入后出现失衡
    {
      if (key > pnode->rchild->key) //情况一:插入右子树的右节点,进行左旋
        pnode=leftRotation(pnode);
      else if (key < pnode->rchild->key) //情况三:插入右子树的左节点,进行先右再左旋转
        pnode=rightLeftRotation(pnode);
    }
  }
  else if (key < pnode->key) //插入值比当前节点值小,插入到当前结点的左子树上
  {
    pnode->lchild = insert(pnode->lchild, key);
    if (height(pnode->lchild) - height(pnode->rchild) == 2) //如果插入导致失衡
    {
      if (key < pnode->lchild->key) //情况二:插入到左子树的左孩子节点上,进行右旋
        pnode = rightRotation(pnode);
      else if (key>pnode->lchild->key)
        pnode = leftRightRotation(pnode);//情况四:插入到左子树的右孩子节点上,进行先左后右旋转
    }
  }
  pnode->height = max(height(pnode->lchild), height(pnode->rchild)) + 1;
  return pnode;
};

 


6.删除节点

删除节点也可能导致AVL树的失衡,实际上删除节点和插入节点是一种互逆的操作:

  • 删除右子树的节点导致AVL树失衡时,相当于在左子树插入节点导致AVL树失衡,即情况情况二或情况四。
  • 删除左子树的节点导致AVL树失衡时,相当于在右子树插入节点导致AVL树失衡,即情况情况一或情况三。

另外,AVL树也是一棵二叉排序树,因此在删除节点时也要维护二叉排序树的性质。删除的代码实现:

/*删除指定元素*/
template<typename T>
AVLTreeNode<T>* AVLTree<T>::remove(AVLTreeNode<T>* & pnode, T key)
{
  if (pnode != nullptr)
  {
    if (key == pnode->key) //找到删除的节点
    {
      //因AVL也是二叉排序树,删除节点要维护其二叉排序树的条件
      if (pnode->lchild != nullptr&&pnode->rchild != nullptr) //若左右都不为空
      {
        // 左子树比右子树高,在左子树上选择节点进行替换
        if (height(pnode->lchild) > height(pnode->rchild))
        {
          //使用左子树最大节点来代替被删节点,而删除该最大节点
          AVLTreeNode<T>* ppre = maximum(pnode->lchild); //左子树最大节点
          pnode->key = ppre->key; //将最大节点的值覆盖当前结点
          pnode->lchild = remove(pnode->lchild, ppre->key); //递归地删除最大节点
        }
        else //在右子树上选择节点进行替换
        {
          //使用最小节点来代替被删节点,而删除该最小节点
          AVLTreeNode<T>* psuc = minimum(pnode->rchild); //右子树的最小节点
          pnode->key = psuc->key; //将最小节点值覆盖当前结点
          pnode->rchild = remove(pnode->rchild, psuc->key); //递归地删除最小节点
        }
      }
      else
      {
        AVLTreeNode<T> * ptemp = pnode;
        if (pnode->lchild != nullptr)
          pnode = pnode->lchild;
        else if (pnode->rchild != nullptr)
          pnode = pnode->rchild;
        delete ptemp;
        return nullptr;
      }
    }
    else if (key > pnode->key)//要删除的节点比当前节点大,则在右子树进行删除
    {
      pnode->rchild = remove(pnode->rchild, key);
      //删除右子树节点导致不平衡:相当于情况二或情况四
      if (height(pnode->lchild) - height(pnode->rchild) == 2) 
      {
        //相当于在左子树上插入右节点造成的失衡(情况四)
        if (height(pnode->lchild->rchild)>height(pnode->lchild->lchild))
          pnode = leftRightRotation(pnode);
        else//相当于在左子树上插入左节点造成的失衡(情况二)
          pnode = rightRotation(pnode); 
      }
    }
    else if (key < pnode->key)//要删除的节点比当前节点小,则在左子树进行删除
    {
      pnode->lchild= remove(pnode->lchild, key);
      //删除左子树节点导致不平衡:相当于情况三或情况一
      if (height(pnode->rchild) - height(pnode->lchild) == 2)
      {
        //相当于在右子树上插入左节点造成的失衡(情况三)
        if (height(pnode->rchild->lchild)>height(pnode->rchild->rchild))
          pnode = rightLeftRotation(pnode);
        else//相当于在右子树上插入右节点造成的失衡(情况一)
          pnode = leftRotation(pnode); 
      }
    }
    return pnode;
  }
  return nullptr;
};

 

删除节点时,如果节点同时拥有左子树和右子树,则在高度教低的子树上选择最大(或最小)元素进行替换,这样能保证替换后不会再出现失衡的现象。至此,AVL树较为复杂的部分都已经分析完毕。剩下的其他操作是普通的二叉排序树共通的操作。为了完整性,我们简单说一下代码。

 

7.查找元素

二叉树是一种递归的定义,因此,二叉树的许多操作都可以通过递归简单地实现,例如遍历二叉树、查找指定元素、销毁二叉树等。 基于二叉排序树的特殊性质, 元素查找操作也能够使用非递归算法简单地实现,我们提供递归与非递归两种版本的元素查找算法。

/*递归查找指定元素*/
template <typename T>
AVLTreeNode<T>* AVLTree<T>::search_recurse(T key)
{
  return search_recurse(root,key);
};
template <typename T>
AVLTreeNode<T>* AVLTree<T>::search_recurse(AVLTreeNode<T>* pnode, T key) const
{
  if (pnode != nullptr)
  {
    if (key == pnode->key)
      return pnode;
    if (key > pnode->key)
      return search_recurse(pnode->rchild,key);
    else
      return search_recurse(pnode->lchild,key);
  }
  return nullptr;
};

/*非递归查找指定元素*/
template <typename T>
AVLTreeNode<T>* AVLTree<T>::search_iterator(T key)
{
  return search_iterator(root, key);
};
template <typename T>
AVLTreeNode<T>* AVLTree<T>::search_iterator(AVLTreeNode<T>* pnode, T key) const
{
  while (pnode != nullptr)
  {
    if (pnode->key == key)
      return pnode;
    else if (key > pnode->key)
      pnode = pnode->rchild;
    else
      pnode = pnode->lchild;
  }
  return nullptr;
};

 


8.遍历二叉树

二叉树的遍历,如果区分左右孩的顺序,共有三种遍历方式:

  • 先序遍历,或称前序遍历
  • 中序遍历,对二叉排序树来说,中序遍历刚好输出一个非递减的序列(假设我们对元素的访问操作是”输出“)
  • 后序遍历

遍历操作可以对二叉树的节点进行访问,这个访问是个抽象的词语。访问可以是输出节点值,或者是销毁这个节点或其他允许对节点进行的操作。我们就如图的AVL树介绍遍历算法

AVL树

 

 

8.1 先序遍历

/*先序遍历*/
template<typename T>
void AVLTree<T>::preOrder(AVLTreeNode<T>* pnode) const
{
  if (pnode != nullptr)
  {
    cout << pnode->key << endl;
    inOrder(pnode->lchild);
    inOrder(pnode->rchild);
  }
};
template<typename T>
void AVLTree<T>::preOrder()
{
  preOrder(root);
};

 

若二叉树为空,则空操作返回,否则先访问根节点,然后前序遍历左子树,再前序遍历右子树。(简记为:VLR) 

前序遍历树a:5 3 2 4 7 6 8 

前序遍历树b:5 3 2 4 1 0 7 6 8

8.2 中序遍历

/*中序遍历*/
template<typename T>
void AVLTree<T>::inOrder(AVLTreeNode<T>* pnode) const
{
  if (pnode != nullptr)
  {
    inOrder(pnode->lchild);
    cout << pnode->key << endl;
    inOrder(pnode->rchild);
  }
};

template<typename T>
void AVLTree<T>::InOrder()
{
  inOrder(root);
};

 

若二叉树为空,则空操作返回,否则从根节点开始,中序遍历根节点的左子树,然后访问根节点,最后中序遍历右子树。(简记为:LVR) 

 中序遍历树a:2 3 4 5 6 7 8 

 中序遍历树b:0 1 2 3 4 5 6 7 8

8.3 后序遍历

/*后序遍历*/
template<typename T>
void AVLTree<T>::postOrder(AVLTreeNode<T>* pnode) const
{
  if (pnode != nullptr)
  {
    inOrder(pnode->lchild);
    inOrder(pnode->rchild);
    cout << pnode->key << endl;
  }
}
template<typename T>
void AVLTree<T>::postOrder()
{
  postOrder(root);
};

 

若树为空,则返回空操作,否则从左到右先叶子后节点的方式遍历访问左右子树,左右子树都访问结束,才访问根节点。(简称LRV) 

后序遍历树a:2 4 3 6 8 7 5 

后序遍历树b:0 2 1 4 3 6 8 7 5

 

9. AVL树的销毁

采用后序遍历AVL树来销毁二叉树。即先销毁根节点的左子树,然后销毁根节点的右子树,最后才销毁根节点。

/*销毁AVL树*/
template<typename T>
void AVLTree<T>::destory(AVLTreeNode<T>* & pnode)
{
  if (pnode != nullptr)
  {
    destory(pnode->lchild); //递归销毁左子树
    destory(pnode->rchild); //递归销毁右子树
    delete pnode; //销毁根节点
    pnode = nullptr; 
  }
};
template<typename T>
void AVLTree<T>::destory()
{
  destory(root);
}

 

 

10.求最大最小值

二叉排序树的最小值位于最左节点,最大值位于其最右节点。

/*返回树中最大节点值*/
template <typename T>
AVLTreeNode<T>* AVLTree<T>::maximum(AVLTreeNode<T>* pnode)const
{
  if (pnode != nullptr)
  {
    while (pnode->rchild != nullptr)
      pnode = pnode->rchild;
    return pnode;
  }
  return nullptr;
};

template<typename T>
T AVLTree<T>::maximum()
{
  AVLTreeNode<T>* presult = maximum(root);
  if (presult != nullptr)
    return presult->key;
};

/*返回树中最小节点值*/
template <typename T>
AVLTreeNode<T>* AVLTree<T>::minimum(AVLTreeNode<T>* pnode)const
{
  if (pnode != nullptr)
  {
    while (pnode->lchild != nullptr)
      pnode = pnode->lchild;
    return pnode;
  }
  return nullptr;
};

template<typename T>
T AVLTree<T>::minimum()
{
  AVLTreeNode<T>* presult = minimum(root);
  if (presult != nullptr)
    return presult->key;
};

 

 

11. 测试

测试代码为:

int _tmain(int argc, _TCHAR* argv[])
{
  AVLTree<int> a;
  for (int i = 0; i < 10; i++)
    a.insert(i);
  cout << "树高:" << a.height() << endl;

  cout << "中序遍历:" << endl;
  a.InOrder();

  cout << "删除元素10"<<endl;
  a.remove(10);

  AVLTreeNode<int>* b = a.search_iterator(10);

  if (b != nullptr)
    cout << b->key;
  else
    cout << "无此元素" << endl;

  getchar();

  return 0;
}

 

运行结果:

树高:4
中序遍历:
0
1
2
3
4
5
6
7
8
9
删除元素10
无此元素


 

上一篇:c++ 类实现 AVL树容器(包含迭代器)


下一篇:Unix/Linux系统中僵尸进程是如何产生的?有什么危害?如何避免?