圖解:什麼是AVL樹?(刪除總結篇)

2021-02-19 帥地玩編程

來源:景禹

作者:景禹


上一篇文章討論了平衡二叉樹的插入操作,沒有看的可以去看一下 圖解:什麼是AVL樹?,有助於理解今天要講的平衡二叉樹的刪除操作。

平衡二叉樹的刪除操作與插入操作類似,先執行標準的BST刪除操作(可以參考文章 圖解:什麼是二叉排序樹? ),然後進行相應的平衡操作。而平衡操作最基本的兩個步驟就是左旋和右旋,如下圖所示:

平衡二叉樹的刪除操作

與平衡二叉樹的插入操作類似,我們以刪除一個結點

從結點 這裡一定注意和平衡二叉樹插入操作區分開來,y不再是從w回溯到z的路徑上z的孩子,x也不再是z的孫子這樣的描述,一定要注意奧!!! )。然後對以 x、y、z 可以的位置有四種情況,BST刪除操作之後的平衡操作也就處理以下四種情況:yz 的左孩子,xy 的左孩子 (Left Left ,LL );yz 的左孩子,xy 的右孩子 (Left Right ,LR );yz 的右孩子,xy 的右孩子 (Right Right ,RR );yz 的右孩子,xy 的左孩子 (Right Right ,RL );

這裡的四種情況與插入操作一樣,但需要注意的是,插入操作僅需要對以 z 為根的子樹進行平衡操作;而平衡二叉樹的刪除操作就不一樣,先對以 z 為根的子樹進行平衡操作,之後可能需要對 z 的祖先結點進行平衡操作,向上回溯直到根結點。

第一種情況:LL

第二種情況:LR

第三種情況:RR

第四種情況:RL

舉例說明示例一:

我們已刪除下圖中的結點 32 為例進行說明。

第一步:由於 32 結點為葉子結點,直接刪除,並保存刪除結點的父節點 17

第二步:從節點 17 向上回溯,找到第一個不平衡結點 44 ,並找到不平衡結點的左右孩子中深度最深的結點 78 (即 y );以及 y 的孩子結點當中深度最深的結點 50 (即 x )。 發現為 RL 的情況。

第三步:對結點 78 進行右旋操作

第四步:對結點 44 進行左旋操作

示例二

我們以刪除下圖中的結點 80 為例進行說明。

第一步,由於結點 80 為葉子結點,則直接刪除,並保存結點 80 的父結點 78

第二步:從結點 78 開始尋找第一個不平衡結點,發現就是結點 78 本身(即結點 z ),找到結點 78 深度最深的葉子結點 60 (即結點 y ),以及結點 y 的深度最深的葉結點 55 (即結點 x )。即 LL 的情況。

第三步:右旋結點 78

第四步:從旋轉後的返回的新的根結點 60 向上回溯(這裡就和平衡二叉樹的插入操作有別了奧,平衡二叉樹的插入操作僅對第一個不平衡結點的子樹進行平衡操作,而AVL的刪除需要不斷地回溯,直到根結點平衡為止 ),判斷是否還有不平衡結點,發現整棵樹的根結點 50 為第一個不平衡結點,找到對應的 y 結點 25x 結點 10 。同樣是 LL 的情況。

第五步:對 z 結點 50 進行右旋操作。

平衡二叉樹的優缺點分析
優點

平衡二叉樹的優點不言而喻,相對於二叉排序樹(BST)而言,平衡二叉樹避免了二叉排序樹可能出現的最極端情況(斜樹)問題,其平均查找的時間複雜度為

缺點

很遺憾,平衡二叉樹為了保持平衡,動態進行插入和刪除操作的代價也會增加。因此出現了後來的紅黑樹,過兩天景禹自會抽時間講解。

時間複雜度分析

左旋和右旋操作僅需要改變幾個指針,時間複雜度為

平衡二叉樹的刪除操作實現

關於左旋與右旋操作,以及平衡因子的計算與之前講的文章 圖解:什麼是AVL樹? 中的實現是一致的,我們直接看AVL刪除操作的實現代碼:

//返回刪除指定結點後的平衡二叉樹的根結點
struct Node* deleteNode(struct Node* root, int key) 

 // 步驟一: 標準的BST刪除操作

 if (root == NULL) 
  return root; 

 //如果要刪除的結點的key小於root->key
 //則表示該結點位於左子樹當中,遞歸遍歷左子樹
 if ( key < root->key ) 
  root->left = deleteNode(root->left, key); 

 //如果要刪除的結點的key大於root->key
 //則表示該結點位於右子樹當中,遞歸遍歷右子樹
 else if( key > root->key ) 
  root->right = deleteNode(root->right, key); 

 //找到刪除結點,進行刪除操作
 else
 { 
  // 被刪除結點只有一個孩子或者沒有孩子,
  if( (root->left == NULL) || (root->right == NULL) ) 
  { 
   struct Node *temp = root->left ? root->left : 
           root->right; 

   // temp為空,左右孩子均為空
   if (temp == NULL) 
   { 
    temp = root; 
    root = NULL; 
   } 
   else // 僅有一個孩子
    *root = *temp; //拷貝非空孩子

   free(temp); 
  } 
  else
  { 
   // 被刪除結點左右孩子都存在: 獲取該結點的直接後繼結點
   // 該結點右子樹中最小的結點
   struct Node* temp = minValueNode(root->right); 

   // 將直接後繼結點的值拷貝給刪除結點
   root->key = temp->key; 

   // 刪除其直接後繼結點
   root->right = deleteNode(root->right, temp->key); 
  } 
 } 

 // 如果樹中僅包含一個結點直接返回
 if (root == NULL) 
  return root; 

 //第二步: 更新當前結點的深度
 root->height = 1 + max(height(root->left), 
      height(root->right)); 

 // 第三步: 獲取刪除結點的平衡因子
 // 判斷該結點是否平衡
 int balance = getBalance(root); 

 // 如果結點為不平衡結點,分以下四種情況處理

 // LL情況
 if (balance > 1 && getBalance(root->left) >= 0) 
  return rightRotate(root); 

 // LR情況
 if (balance > 1 && getBalance(root->left) < 0) 
 { 
  root->left = leftRotate(root->left); 
  return rightRotate(root); 
 } 

 // RR情況 
 if (balance < -1 && getBalance(root->right) <= 0) 
  return leftRotate(root); 

 // RL情況
 if (balance < -1 && getBalance(root->right) > 0) 
 { 
  root->right = rightRotate(root->right); 
  return leftRotate(root); 
 } 

 return root; 

實戰應用題目描述

給定一個值 x ,返回一顆平衡二叉樹中比 x 大的結點個數

輸入輸出示例

輸入一個值 x  = 10 和下面的一顆平衡二叉樹:

輸出:4

解釋:平衡二叉樹中比結點10大有 11,13,14,16 ,共4個結點。

題目解析對於平衡二叉樹中的每一個結點維護一個 desc 欄位,用於保存每一個結點所包含的子孫結點的個數。比如示例中結點 10 的 desc 的值就等於 4,結點 10 的子孫結點包含 6、11、5、8 四個結點。計算大於給定結點的節點數目就可以通過遍歷平衡二叉樹獲得了,具體包含以下三種情況:x 比當前遍歷的結點的值大,我們則遍歷當前結點的右孩子。x 比當前遍歷的結點的值小,則大於指定結點的數目加上當前結點右孩子的 desc 加上 2 (加 2 是因為當前結點以及當前結點的右孩子都比指定的值 x 要大,當然是當前結點的右孩子存在的情況下)。具體操作是,判斷當前結點的右孩子是否存在,如果存在則給大於 x 的結點數目加上當前結點的右孩子的 desc 並加2,否則 給大於 x 的結點數目加 1 ;然後將當前結點更新為其左孩子。當 x 等於當前結點的值,判斷 x 的右孩子是否存在,如果存在則將大於 x 的結點數目加上 x 的右孩子 desc ,然後再加上右孩子本身(即 1);否則,右孩子不存在,則直接返回大於 x 的結點數目。

結點的定義中增加 desc 域:

struct Node { 
    int key; 
    struct Node* left, *right; 
    int height; 
    int desc; 
}; 

我們以查找示例網絡中比結點 6 的結點數目為例講解,比結點 6 的結點數目用 count 表示且初始化為 0;

第一步:訪問根結點 13 ,發現結點 6 的值比 13 小,則 count 的值加上 13 的右孩子 15 的 desc=1 ,再加上結點 1315 本身, count = desc + 2 = 3 .

第二步:訪問結點 13 的左孩子 106 < 10 ,則 count 的值應加上 10 的右孩子 11 的 desc 的值,再加 2,其中結點 11 的 desc 的值為0,故 count = 3 + 2 = 5 .

第三步:訪問結點 10 的左孩子 6 ,發現與給定值相等,且結點 6 的右孩子存在,則 count 應加上結點 6 的右孩子 8 的 desc 以及結點 8 本身,即 count = 5 + 1 = 6 .

其實歸結到最本質,整個過程就是利用了二叉排序樹中,結點的右子樹的值大於結點,左子樹的值小於結點這樣的特性。

那麼該如何計算每一個結點的 desc 域呢?

插入:每當插入一個新的結點,則給新插入結點的所有父結點的 desc 加 1 。當然相應的旋轉操作也需要進行處理,稍後用圖進行說明。刪除操作:當刪除一個結點,則將刪除結點的所有祖先結點的 desc 減 1 。同樣不論左旋還是右旋都需要進行處理。

還是以之前的左旋和右旋圖說明 desc 值的相應變化:

左旋的情況下:

int val = (T2 != NULL) ? T2->desc : -1; 
x->desc = x->desc - (y->desc + 1) + (val + 1); 
y->desc = y->desc - (val + 1) + (x->desc + 1); 

x 的 desc 的值將等於其原來的值減去其原來的右孩子結點 y 的 desc ,再加上左旋之後其右孩子

右旋的情況下:

int val = (T2 != NULL) ? T2->desc : -1; 
y->desc = y->desc - (x->desc + 1) + (val + 1); 
x->desc = x->desc - (val + 1) + (y->desc + 1); 

與左旋類似,當 y 的值變為其之前的 desc j減去 x 的 desc+1 ,再加上 x 的 desc 則變為其原來的 desc 的值減去 val+1 ,然後再加上旋轉後的 y->desc + 1 。

有了上面的基礎,我們可以一起先來看一下這道題目的左旋和右旋操作。

左右旋操作代碼: 本質上與之前講過的平衡二叉樹插入和刪除操作涉及的左旋與右旋一樣,只是增加了上面的 desc 域的處理操作 (需要複習的就再看一遍代碼,不需要的直接跳過)。

struct Node* rightRotate(struct Node* y) 

 struct Node* x = y->left; 
 struct Node* T2 = x->right; 

 //旋轉操作,對著圖看
 x->right = y; 
 y->left = T2; 

 // 高度更新
 y->height = max(height(y->left), height(y->right)) + 1; 
 x->height = max(height(x->left), height(x->right)) + 1; 

 // 更新desc 
 int val = (T2 != NULL) ? T2->desc : -1; 
 y->desc = y->desc - (x->desc + 1) + (val + 1); 
 x->desc = x->desc - (val + 1) + (y->desc + 1); 

 return x; 



struct Node* leftRotate(struct Node* x) 

 struct Node* y = x->right; 
 struct Node* T2 = y->left; 

 //左旋 
 y->left = x; 
 x->right = T2; 

 //更新高度
 x->height = max(height(x->left), height(x->right)) + 1; 
 y->height = max(height(y->left), height(y->right)) + 1; 

 //更新 desc 
 int val = (T2 != NULL) ? T2->desc : -1; 
 x->desc = x->desc - (y->desc + 1) + (val + 1); 
 y->desc = y->desc - (val + 1) + (x->desc + 1); 

 return y; 

獲取結點 N 的平衡因子(複習):

int getBalance(struct Node* N) 

 if (N == NULL) 
  return 0; 
 return height(N->left) - height(N->right); 

平衡二叉樹結點的插入操作(增加了對desc的處理,其他和之前講的插入操作的實現一致):

struct Node* insert(struct Node* node, int key) 

 /* 標準的BST的插入操作 */
 if (node == NULL) 
  return (newNode(key)); 

 if (key < node->key) { 
  node->left = insert(node->left, key); 
  node->desc++; //插入結點的左右祖先結點的desc++
 } 

 else if (key > node->key) { 
  node->right = insert(node->right, key); 
  node->desc++; 
 } 

 else // 二叉排序樹中不允許插入相同的值
  return node; 

 /* 2. 更新祖先結點的高度 */
 node->height = 1 + max(height(node->left), 
      height(node->right)); 

 /* 3. 獲取祖先結點的平衡因子,判斷是否平衡*/
 int balance = getBalance(node); 

 // 結點不平衡,分一下四種情況處理

 // LL
 if (balance > 1 && key < node->left->key) 
  return rightRotate(node); 

 // RR 
 if (balance < -1 && key > node->right->key) 
  return leftRotate(node); 

 // LR
 if (balance > 1 && key > node->left->key) { 
  node->left = leftRotate(node->left); 
  return rightRotate(node); 
 } 

 // RL
 if (balance < -1 && key < node->right->key) { 
  node->right = rightRotate(node->right); 
  return leftRotate(node); 
 } 

 /*返回插入結點之後,樹的根結點*/
 return node; 

至於刪除操作的修改代碼,我就不在這裡放了,需要的可以對上面的平衡二叉樹刪除操作的代碼修改一下即可。我們主要看一下統計大於給定值 x 的結點個數的代碼。

統計大於結點 x 的結點數目

int CountGreater(struct Node* root, int x) 

 int res = 0; 

 // 查找結點 x, 同時更新 res的值
 while (root != NULL) { 

  //保存當前結點的右孩子的desc
  //不為空則保存root->right-desc
  //否則保存 -1
  int desc = (root->right != NULL) ? 
    root->right->desc : -1; 

  //如果root的值大於x,則說明 x 位於左子樹當中
  //res = res + 當且結點右孩子的desc + 2
  if (root->key > x) { 
   res = res + desc + 1 + 1; 
   root = root->left; 
  } //當root的值小於 x,則說明 x 位於右子樹當中,繼續查找
  else if (root->key < x) {
   root = root->right; 
  }
  else { //當相等時,res = res + x的右孩子的desc + 1.
   res = res + desc + 1; 
   break; 
  } 
 } 
 return res; 

相關焦點

  • 一文讀懂 AVL 樹
    所謂的平衡之意,就是樹中任意一個結點下左右兩個子樹的高度差不超過 1。(本文對於樹的高度約定為:空結點高度是 0,葉子結點高度是 1。)那 AVL 樹和普通的二叉查找樹有何區別呢?如圖,如果我們插入的是一組有序上升或下降的數據,則一棵普通的二叉查找樹必然會退化成一個單鍊表,其查找效率就降為 O(n)。
  • 圖解:什麼是AVL樹?(上篇)
    平衡二叉樹基礎篇什麼是平衡二叉樹?AVL樹或者是一顆空樹,或者是具有下列性質的二叉排序樹:它的左子樹和右子樹都是平衡二叉樹,且左子樹和右子樹的深度之差的絕對值不超過 1 。什麼是平衡因子?什麼是左旋?什麼又是右旋?
  • 什麼是平衡二叉樹(AVL)
    前言Wiki:在計算機科學中,AVL樹是最早被發明的自平衡二叉查找樹。在AVL樹中,任一節點對應的兩棵子樹的最大高度差為1,因此它也被稱為高度平衡樹。查找、插入和刪除在平均和最壞情況下的時間複雜度都是O(logn)。增加和刪除元素的操作則可能需要藉由一次或多次樹旋轉,以實現樹的重新平衡。
  • 趣味算法圖解,文科生都看懂了
    Fekete、Sebastian Morr 和 Sebastian Stiller 共同推出的圖解算法系列。 它們最初是為 Sándor 在德國不倫瑞克工業大學開設的算法和數據結構講座而設計的,作者希望它們能夠有更廣的用途,因此在網上發布了這個項目,希望能夠幫助到教師、學生和有好奇心的人們。
  • 平衡二叉樹 AVL樹結構詳解 [Java實現]
    二叉樹什麼是二叉樹?二叉樹數據結構,顧名思義,只有兩個叉,在數據結構中,操作性能要遠高於線性結構,有O(height)的索引性能。與線性結構有相同的空間複雜度,特性如下: 每個節點最多只有兩個兒子節點左兒子小,右兒子大 (大小按照我們默認的比較規則,本例用int來比較)線行找7與二叉數找7線性找7二叉樹找7 okay,我想大家聰明人已經看出來了,二叉樹搜索用了2次,而線性結構卻用了5次。
  • 圖解:數據結構中的各種「樹」,你都心中有數嗎?
    今天就帶大家一起學習下,數據結構中的各種「樹」,這也是面試中經常考查的內容,手撕二叉樹是常規套路,對候選人也很有區分度,學完這篇文章,相信大家都會心中有「樹」了。從樹說起什麼是樹?現實中的樹大家都見過,在數據結構中也有樹,此樹非彼樹,不過數據結構的樹和現實中的樹在形態上確實有點相像。
  • 圖解:數據結構中的6種「樹」,你心中有數嗎?
    今天就帶大家一起學習下,數據結構中的各種「樹」,這也是面試中經常考察的內容,手撕二叉樹是常規套路,對候選人也很有區分度,學完這篇文章,相信大家都會心中有「樹」了。下面幾個基礎概念必須了解,否則你當你刷LeetCode樹相關題目時候,或者面試官向你描述問題時,你會連題目都看不懂事什麼意思。先來上一個圖解,下面的術語和概念對照著看,更容易理解。
  • Python數據結構——AVL樹的實現
    既然,我們已經證明,保持AVL樹的平衡將會使性能得到很大的提升,那我們看看如何在程序中向樹插入一個新的鍵值。因為所有的新鍵是作為葉節點插入樹的,而新葉子的平衡因子為零,所以我們對新插入的節點不作調整。如果父節點的平衡因子不為零, 算法通過父節點遞歸調用updateBalance方法繼續遞歸到樹的根。當對一棵樹進行再平衡是必要的,我們該怎麼做呢?高效的再平衡是使AVL樹能夠很好地執行而不犧牲性能的關鍵。為了讓AVL樹恢復平衡,我們會在樹上執行一個或多個「旋轉」(rotation)。為了了解什麼是旋轉,讓我們看一個很簡單的例子。思考一下圖3的左邊的樹。
  • Python數據結構――AVL樹的實現
    既然,我們已經證明,保持 AVL 樹的平衡將會使性能得到很大的提升,那我們看看如何在程序中向樹插入一個新的鍵值。因為所有的新鍵是作為葉節點插入樹的,而新葉子的平衡因子為零,所以我們對新插入的節點不作調整。
  • 圖解:數據結構中的6種「樹」,檸檬問你心中有數嗎?
    今天就帶大家一起學習下,數據結構中的各種「樹」,這也是面試中經常考察的內容,手撕二叉樹是常規套路,對候選人也很有區分度,學完這篇文章,相信大家都會心中有「樹」了。從樹說起什麼是樹?現實中的樹大家都見過,在數據結構中也有樹,此樹非彼樹,不過數據結構的樹和現實中的樹在形態上確實有點相像。
  • 圖解:計算機數據結構中的 6 種「樹」,你心中有數了嗎?
    今天就帶大家一起學習下,數據結構中的各種「樹」,這也是面試中經常考察的內容,手撕二叉樹是常規套路,對候選人也很有區分度,學完這篇文章,相信大家都會心中有「樹」了。從樹說起什麼是樹?現實中的樹大家都見過,在數據結構中也有樹,此樹非彼樹,不過數據結構的樹和現實中的樹在形態上確實有點相像。
  • 決策樹學習筆記(三):CART算法,決策樹總結
    作者:xiaoyu介紹:一個半路轉行的數據挖掘工程師 推薦導讀:本篇為樹模型系列第三篇▍前情回顧前兩篇介紹了決策樹主要的三個步驟,以及ID3和C4.5算法:本篇將繼續介紹決策的第三種算法:CART算法,它可以說是學習決策樹的核心了
  • 很簡單的小聖誕樹摺紙教程圖解-兒童摺紙系列
    很簡單的小聖誕樹摺紙教程圖解-兒童摺紙系列這是一款比較簡單較小的聖誕樹,可以貼在牆上或玻璃上,可以用來裝飾,經濟又美觀,可以用有顏色的紙進行。簡單聖誕樹的效果圖10.這個時候既得如圖10,這個時候來製作樹的層次。將上面的三角向後摺疊一次,即得到一個山痕,然後再向回摺疊一次,即得到一個谷痕,請按照圖中所示摺痕進行操作,然後完成第一的層次的製作。
  • 漫畫:唐玄奘教你橫掃 AVL 樹面試題無敵手!
    作者 | 帥地責編 | 胡巍巍二叉查找樹1、若它的左子樹不為空,則左子樹上所有的節點值都小於它的根節點值。2、若它的右子樹不為空,則右子樹上所有的節點值均大於它的根節點值。3、它的左右子樹也分別可以充當為二叉查找樹。
  • 谷歌和FB到底有什麼區別?看看我的總結【個人總結篇】
    一定要搞清楚,我們在通過兩個平臺推廣自己的業務和產品時一定一定搞清楚這個平臺本身時用來幹什麼的?如果你把這個弄混淆了 ,你再怎麼推廣達不到你想要的效果 我在上面大概的總結了這兩個平臺的各個的優勢,那我們看看劣勢,這個劣勢也是相對的,並不是絕對的,看你怎麼利用。
  • 《異界鎖鏈》疏導交通高級篇操作步驟圖解
    18183首頁 推箱子 《異界鎖鏈》疏導交通高級篇操作步驟圖解 《異界鎖鏈》疏導交通高級篇操作步驟圖解 來源:網絡
  • 這篇文章告訴你
    提出的問題什麼情況下創建索引,什麼時候不需要索引?索引的種類有哪些?什麼是索引索引就是幫助資料庫管理系統高效獲取數據的數據結構,就好比一本書的目錄,它可以幫我們快速進行特定值的定位與查找,從而加快數據查詢的效率。
  • 面相算命圖解:面相耳朵
    ※ 耳朵上有窩的人,耳不聰,聽不清別人講什麼。    ※ 從前面能看見耳朵的人,多貧窮耳朵,是與大腦連貫的,並與腎經相通。古法分為天輪地輪人輪。五行上,屬於金木兩星,即左耳金星,右耳木星。宮位上是採聽宮,又為聰明學堂。百歲流年法,行運是1歲—14歲。面相算命圖解今將面相耳朵秘訣公布如下;
  • 什麼是紅黑樹?程式設計師面試必問!
    —— 學紅黑樹有感。終於,在學習了幾天的紅黑樹相關的知識後,我想把我所學所想和所感分享給大家。紅黑樹是一種比較難的數據結構,要完全搞懂非常耗時耗力,紅黑樹怎麼自平衡?什麼時候需要左旋或右旋?插入和刪除破壞了樹的平衡後怎麼處理?等等一連串的問題在學習前困擾著我。如果你在學習過程中也會存在我的疑問,那麼本文對你會有幫助,本文幫助你全面、徹底地理解紅黑樹,面試官放馬過來!
  • 漫畫:什麼是紅黑樹?
    什麼情況下會破壞紅黑樹的規則,什麼情況下不會破壞規則呢?我們舉兩個簡單的慄子:1.向原紅黑樹插入值為14的新節點:關於紅黑樹自平衡的調整,插入和刪除節點的時候都涉及到很多種Case,由於篇幅原因無法展開來一一列舉,有興趣的朋友可以參考維基百科,裡面講的非常清晰。2.漫畫中紅黑樹調整過程的示例是一種比較複雜的情形,沒太看明白的小夥伴也不必鑽牛角尖,關鍵要懂得紅黑樹自平衡調整的主體思想。