資料結構-----AVL樹的插入刪除操作
對於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;
}
在沒有找到要刪除的節點的時候,需要利用遞迴再次呼叫刪除函式。
又因為以待刪除的那個節點為根節點的子樹中,替換掉的節點是高度大的一方,所以這個子樹的平衡不會被破壞。
但是因為確實少了一個節點,高度很有可能會改變,所以其父節點的平衡很有可能就被破壞了,需要重新改變樹的結構。
不過在判斷需要什麼樣的旋轉操作時與插入操作有點不同,它是比較當前節點的左節點的左節點和當前節點的左節點的右節點的高度差,或者是當前節點的右節點的左節點和當前節點的右節點的右節點的高度差。具體的實現可以參考上述程式碼。
下面說一下為什麼插入操作和刪除操作利用的是遞迴而不是迭代。
考慮一下,在插入或刪除操作後需要進行左旋或者右旋來重塑樹的結構,具體向哪個方向旋轉是根據它在父節點的左邊還是右邊。同時在插入刪除後,改變樹的結構是從插入位置開始,或者從刪除位置開始逐步向上進行重塑的。即應該先考慮當前父節點開始的子樹結構是否需要重塑,完成後再考慮以父節點的父節點開始的子樹結構是否需要重塑。
簡單來說,就是從發生改變的那個節點到整個樹的根節點的這條路上經過的所有節點都需要判斷一下是否需要重塑。
所以如果利用迭代的話,那麼每一個節點都需要記錄,而且每一個節點是其父節點的左節點還是其右節點也需要記錄。
但是如果利用遞迴的話,在遞迴的呼叫空間中,我們預設就記錄下了這些節點,也記錄下了每一個節點在其父節點的左邊還是右邊,所以利用遞迴實現起來是比較方便的。