对于AVL的插入和删除,主要利用的就是上篇文章所述的四种旋转操作,根据插入后不同的结构选用不同的方式复原平衡。
再次声明一下,http://www.cnblogs.com/QG-whz/p/5167238.html这篇文章讲解的比较好,我也是从链接处的大神那学习的,这里只用来复习。
首先对于插入操作,有以下几个步骤:
步骤1:根据二叉树的性质:大的向右找,小的向左找。逐个比较要插入的值和节点的值的大小关系,找到应该插入的位置。
步骤2:在插入的位置新申请一个节点,并返回该节点。
步骤3:判断是否出现不平衡的情况,通过旋转操作恢复平衡。
其次明确插入函数的实现方式:利用递归,假设当前结点为pnode,要插入的值为key。
步骤1:比较权值
如果key > pnode->key。那么执行insert(pnode->rightChild);
如果key < pnode->key。那么执行insert(pnode->leftChild);
步骤2:处理不平衡
如果在右子树中插入,则可能会需要左旋操作或者先右旋后左旋操作。
如果在左子树种插入,则可能会需要右旋操作或者先左旋后右旋操作。
然后应该明确插入操作的参数和其返回值:
既然使用了递归,很明显参数应该是当前节点pnode和要插入的值key。理解为在以节点pnode为根节点的子树中插入新节点,该节点的权值为key。
返回值是当前子树的根节点。
所以插入操作的大体结构应该长这个样子:
template<class T>
AVLTreeNode<T>* AVLTree<T>::insert(AVLTreeNode<T>* &pnode, const T& key)
{
if(pnode == NULL)
{
pnode = new AVLTreeNode<T>(key);
}
else
{
if(key > pnode->key)
{
pnode->rightChild = insert(pnode->rightChild, key);
if(height(pnode->rightChild) - height(pnode->leftChild) == 2)
{
if(key > pnode->rightChild->key)
pnode = leftRotation(pnode);
else if(key < pnode->rightChild->key)
pnode = rightLeftRotation(pnode);
}
}
else if(key < pnode->key)
{
pnode->leftChild = insert(pnode->leftChild, key);
if(height(pnode->leftChild) - height(pnode->rightChild) == 2)
{
if(key < pnode->leftChild->key)
pnode = rightRotation(pnode);
else if(key > pnode->leftChild->key)
pnode = leftRightRotation(pnode);
}
}
}
return pnode;
}
接下来是删除操作,删除操作和插入都是利用了递归的思想。
首先思路如下:
步骤1:根据二叉树的定义,找到要删除的节点。该节点的key值和参数key相等。
步骤2:比较待删除的节点的左右子树的高度。
如果左子树的高度大,则将左子树中key值最大的节点移动到待删除的节点处。
如果右子树的高度大,则将右子树种key值最小的节点移动到待删除的节点处。
其次明确删除操作的实现方式:利用递归,假设当前结点为pnode,要删除的节点权值应为key。
步骤1:比较权值。
如果key > pnode->key,那么执行remove(pnode->rightChild, key);
如果key < pnode->key,那么执行remove(pnode->leftChild, key);
如果key == pnode->key,那么删除该节点。
删除操作
步骤1:
如果height(pnode->leftChild) > height(pnode->rightChild),那么寻找其左子树中key值最大的那个节点plnode。
如果height(pnode->leftChild) < height(pnode->rightChild),那么寻找其右子树中key值最小的那个节点prnode。
步骤2:
若在左子树中查找,则令pnode->key = plnode->key,随后利用递归在pnode的左子树中删除plnode节点,即remove(pnode->leftChild, plnode->key)。
若在右子树中查找,则令pnode->key = prnode->key,随后利用递归在pnode的右子树中删除prnode节点,即remove(pnode->rightChild, prnode->key)。
然后应该明确删除操作的参数和其返回值。
参数应为当前结点pnode和要删除的节点的值key。理解为在以节点pnode为根节点的子树中删除权值为key的节点。
返回值为当前子树的根节点pnode。
所以删除操作大体上长这个样子:
template<class T>
AVLTreeNode<T>* AVLTree<T>::remove(AVLTreeNode<T>* &pnode, const T& key)
{
if(pnode != NULL)
{
if(pnode->key == key)
{
if(pnode->leftChild != NULL && pnode->rightChild != NULL)
{
if(height(pnode->leftChild) > height(pnode->rightChild))
{
AVLTreeNode<T>* plnode = maximun(pnode->leftChild);
pnode->key = plnode->key;
pnode->leftChild = remove(pnode->leftChild, plnode->key);
}
else
{
AVLTreeNode<T>* prnode = minimun(pnode->rightChild);
pnode->key = prnode->key;
pnode->rightChild = remove(pnode->rightChild, prnode->key);
}
}
else
{
AVLTreeNode<T>* temp = pnode;
if(pnode->leftChild != NULL)
pnode = pnode->leftChild;
else if(pnode->rightChild != NULL)
pnode = pnode->rightChild;
delete temp;
return pnode;
}
}
else if(key > pnode->key)
{
pnode->rightChild = remove(pnode->rightChild, key);
if(height(pnode->leftChild) - height(pnode->rightChild) == 2)
{
if(height(pnode->leftChild->leftChild) > height(pnode->leftChild->rightChild))
pnode = rightRotation(pnode);
else
pnode = leftRightRotation(pnode);
}
}
else
{
pnode->leftChild = remove(pnode->leftChild, key);
if(height(pnode->rightChild) - height(pnode->leftChild) == 2)
{
if(height(pnode->rightChild->rightChild) > height(pnode->rightChild->leftChild))
pnode = leftRotation(pnode);
else
pnode = rightLeftRotation(pnode);
}
}
return pnode;
}
return NULL;
}
在没有找到要删除的节点的时候,需要利用递归再次调用删除函数。
又因为以待删除的那个节点为根节点的子树中,替换掉的节点是高度大的一方,所以这个子树的平衡不会被破坏。
但是因为确实少了一个节点,高度很有可能会改变,所以其父节点的平衡很有可能就被破坏了,需要重新改变树的结构。
不过在判断需要什么样的旋转操作时与插入操作有点不同,它是比较当前节点的左节点的左节点和当前节点的左节点的右节点的高度差,或者是当前节点的右节点的左节点和当前节点的右节点的右节点的高度差。具体的实现可以参考上述代码。
下面说一下为什么插入操作和删除操作利用的是递归而不是迭代。
考虑一下,在插入或删除操作后需要进行左旋或者右旋来重塑树的结构,具体向哪个方向旋转是根据它在父节点的左边还是右边。同时在插入删除后,改变树的结构是从插入位置开始,或者从删除位置开始逐步向上进行重塑的。即应该先考虑当前父节点开始的子树结构是否需要重塑,完成后再考虑以父节点的父节点开始的子树结构是否需要重塑。
简单来说,就是从发生改变的那个节点到整个树的根节点的这条路上经过的所有节点都需要判断一下是否需要重塑。
所以如果利用迭代的话,那么每一个节点都需要记录,而且每一个节点是其父节点的左节点还是其右节点也需要记录。
但是如果利用递归的话,在递归的调用空间中,我们默认就记录下了这些节点,也记录下了每一个节点在其父节点的左边还是右边,所以利用递归实现起来是比较方便的。