重學數據結構與算法(09)--樹和二叉樹:分支關係與層次結構下如何有效實現增刪查?

1)樹是什麼

樹是由結點和邊組成的,不存在環的一種數據結構。通過下圖,我們就可以更直觀的認識樹的結構:
在這裏插入圖片描述
樹滿足遞歸定義的特性。也就是說,如果一個數據結構是樹結構,那麼剔除掉根結點後,得到的若干個子結構也是樹,通常稱作子樹。在一棵樹中,根據結點之間層次關係的不同,對結點的稱呼也有所不同。我們來看下面這棵樹,如下圖所示:
在這裏插入圖片描述

  • A 結點是 B 結點和 C 結點的上級,則 A 就是 B 和 C 的父結點,B 和 C 是 A 的子結點;
  • B 和 C 同時是 A 的“孩子”,則可以稱 B 和 C 互爲兄弟結點;
  • A 沒有父結點,則可以稱 A 爲根結點;
  • G、H、I、F 結點都沒有子結點,則稱 G、H、I、F 爲葉子結點。

當有了一棵樹之後,還需要用深度、層來描述這棵樹中結點的位置:結點的層次從根結點算起,根爲第一層,根的“孩子”爲第二層,根的“孩子”的“孩子”爲第三層,依此類推。樹中結點的最大層次數,就是這棵樹的樹深(稱爲深度,也稱爲高度);如下圖所示,就是一棵深度爲 4 的樹:
在這裏插入圖片描述

2)二叉樹是什麼

在樹的大家族中,有一種被高頻使用的特殊樹,它就是二叉樹;在二叉樹中,每個結點最多有兩個分支,即每個結點最多有兩個子結點,分別稱作左子結點和右子結點;在二叉樹中,有下面兩個特殊的類型,如下圖所示:

  • 滿二叉樹:定義爲除了葉子結點外,所有結點都有 2 個子結點;
    在這裏插入圖片描述
  • 完全二叉樹:定義爲除了最後一層以外,其他層的結點個數都達到最大,並且最後一層的葉子結點都靠左排列。
    在這裏插入圖片描述
    完全二叉樹看上去並不完全,但爲什麼這樣稱呼它呢?這其實和二叉樹的存儲有關係。
    存儲二叉樹有兩種辦法:一種是基於指針的鏈式存儲法,另一種是基於數組的順序存儲法
    鏈式存儲法:也就是像鏈表一樣,每個結點有三個字段,一個存儲數據,另外兩個分別存放指向左右子結點的指針,如下圖所示:
    在這裏插入圖片描述
    順序存儲法:就是按照規律把結點存放在數組裏,如下圖所示,爲了方便計算,我們會約定把根結點放在下標爲 1 的位置;隨後,B 結點存放在下標爲 2 的位置,C 結點存放在下標爲 3 的位置,依次類推。
    在這裏插入圖片描述
    根據這種存儲方法,我們可以發現如果結點 X 的下標爲 i,那麼 X 的左子結點總是存放在 2 * i 的位置,X 的右子結點總是存放在 2 * i + 1 的位置;之所以稱爲完全二叉樹,是從存儲空間利用效率的視角來看的對於一棵完全二叉樹而言,僅僅浪費了下標爲 0 的存儲位置;而如果是一棵非完全二叉樹,則會浪費大量的存儲空間
    我們來看如下圖所示的非完全二叉樹,它既需要保留出 5 和 6 的位置。同時,還需要保留 5 的兩個子結點 10 和 11 的位置,以及 6 的兩個子結點 12 和 13 的位置。這樣的二叉樹,沒有完全利用好數組的存儲空間。
    在這裏插入圖片描述

3)樹的基本操作

樹結構則是“一對多”的關係,即前面的父結點,跟下面若干個子結點產生了連接關係;
遍歷一棵樹,有非常經典的三種方法,分別是前序遍歷、中序遍歷、後序遍歷;這裏的序指的是父結點的遍歷順序,前序就是先遍歷父結點,中序就是中間遍歷父結點,後序就是最後遍歷父結點。不管哪種遍歷,都是通過遞歸調用完成的。如下圖所示:

  • 前序遍歷:對樹中的任意結點來說,先打印這個結點,然後前序遍歷它的左子樹,最後前序遍歷它的右子樹;
  • 中序遍歷:對樹中的任意結點來說,先中序遍歷它的左子樹,然後打印這個結點,最後中序遍歷它的右子樹;
  • 後序遍歷:對樹中的任意結點來說,先後序遍歷它的左子樹,然後後序遍歷它的右子樹,最後打印它本身。
    在這裏插入圖片描述
    代碼實現如下所示:
// 先序遍歷
public static void preOrderTraverse(Node node) {
    if (node == null)
        return;
    System.out.print(node.data + " ");
    preOrderTraverse(node.left);
    preOrderTraverse(node.right);
}
// 中序遍歷
public static void inOrderTraverse(Node node) {
    if (node == null)
        return;
    inOrderTraverse(node.left);
    System.out.print(node.data + " ");
    inOrderTraverse(node.right);
}
// 後序遍歷
public static void postOrderTraverse(Node node) {
    if (node == null)
        return;
    postOrderTraverse(node.left);
    postOrderTraverse(node.right);
    System.out.print(node.data + " ");
}

二叉樹遍歷過程中,每個結點都被訪問了一次,其時間複雜度是 O(n)。接着,在找到位置後,執行增加和刪除數據的操作時,我們只需要通過指針建立連接關係就可以了;對於沒有任何特殊性質的二叉樹而言,拋開遍歷的時間複雜度以外,真正執行增加和刪除操作的時間複雜度是 O(1);樹數據的查找操作和鏈表一樣,都需要遍歷每一個數據去判斷,所以時間複雜度是 O(n)。
我們上面講到二叉樹的增刪查操作很普通,時間複雜度與鏈表並沒有太多差別。但當二叉樹具備一些特性的時候,則可以利用這些特性實現時間複雜度的降低。接下來詳細介紹二叉查找樹的特性。

3.1)二叉查找樹的特性

二叉查找樹(也稱作二叉搜索樹)具備以下幾個的特性:

  • 在二叉查找樹中的任意一個結點,其左子樹中的每個結點的值,都要小於這個結點的值;
  • 在二叉查找樹中的任意一個結點,其右子樹中每個結點的值,都要大於這個結點的值;
  • 在二叉查找樹中,會盡可能規避兩個結點數值相等的情況;
  • 對二叉查找樹進行中序遍歷,就可以輸出一個從小到大的有序數據隊列。如下圖所示,中序遍歷的結果就是
    10、13、15、16、20、21、22、26。
    在這裏插入圖片描述

3.2)二叉查找樹的查找操作

在利用二叉查找樹執行查找操作時,我們可以進行以下判斷:

  • 首先判斷根結點是否等於要查找的數據,如果是就返回;
  • 如果根結點大於要查找的數據,就在左子樹中遞歸執行查找動作,直到葉子結點;
  • 如果根結點小於要查找的數據,就在右子樹中遞歸執行查找動作,直到葉子結點;

這樣的“二分查找”所消耗的時間複雜度就可以降低爲 O(logn)。

3.3)二叉查找樹的插入操作

在二叉查找樹執行插入操作也很簡單。從根結點開始,如果要插入的數據比根結點的數據大,且根結點的右子結點不爲空,則在根結點的右子樹中繼續嘗試執行插入操作。直到找到爲空的子結點執行插入動作。
如下圖所示,如果要插入數據 X 的值爲 14,則需要判斷 X 與根結點的大小關係:

  • 由於 14 小於 16,則聚焦在其左子樹,繼續判斷 X 與 13 的關係;
  • 由於 14 大於 13,則聚焦在其右子樹,繼續判斷 X 與15 的關係;
  • 由於 14 小於 15,則聚焦在其左子樹;
  • 因爲此時左子樹爲空,則直接通過指針建立 15 結點的左指針指向結點 X 的關係,就完成了插入動作。
    在這裏插入圖片描述
    二叉查找樹插入數據的時間複雜度是 O(logn)。但這並不意味着它比普通二叉樹要複雜。原因在於這裏的時間複雜度更多是消耗在了遍歷數據去找到查找位置上,真正執行插入動作的時間複雜度仍然是 O(1)。
    二叉查找樹的刪除操作會比較複雜,這是因爲刪除完某個結點後的樹,仍然要滿足二叉查找樹的性質。我們分爲下面三種情況討論。
    1)情況一,如果要刪除的結點是某個葉子結點,則直接刪除,將其父結點指針指向 null 即可
    在這裏插入圖片描述
    2)情況二,如果要刪除的結點只有一個子結點,只需要將其父結點指向的子結點的指針換成其子結點的指針即可;
    在這裏插入圖片描述
    3)情況三,如果要刪除的結點有兩個子結點,則有兩種可行的操作方式:
  • 第一種,找到這個結點的左子樹中最大的結點,替換要刪除的結點。
    在這裏插入圖片描述
    第二種,找到這個結點的右子樹中最小的結點,替換要刪除的結點。
    在這裏插入圖片描述

4)樹的案例

題目:輸入一個字符串,判斷它在已有的字符串集合中是否出現過?(假設集合中沒有某個字符串與另一個字符串擁有共同前綴且完全包含的特殊情況,例如 deep 和 dee。)如,已有字符串集合包含 6 個字符串分別爲,cat, car, city, dog,door, deep。輸入 cat,輸出 true;輸入 home,輸出 false。
分析:對字符串建立一個的樹結構,如下圖所示,它將字符串集合的前綴進行合併,每個根結點到葉子結點的鏈條就是一個字符串。
在這裏插入圖片描述
這個樹結構也稱作 Trie 樹,或字典樹。它具有三個特點:
1)根結點不包含字符;
2)除根結點外每一個結點都只包含一個字符;
3)從根結點到某一葉子結點,路徑上經過的字符連接起來,即爲集合中的某個字符串。

這個問題的解法可以拆解爲以下兩個步驟:
1)根據候選字符串集合,建立字典樹。這需要使用數據插入的動作;
2)對於一個輸入字符串,判斷它能否在這個樹結構中走到葉子結點。如果能,則出現過。
在這裏插入圖片描述

5)總結

對於查找操作,如果是普通二叉樹,則查找的時間複雜度和遍歷一樣,都是 O(n);
如果是二叉查找樹,則可以在 O(logn) 的時間複雜度內完成查找動作;
樹結構在存在“一對多”的數據關係中,可被高頻使用,這也是它區別於鏈表系列數據結構的關鍵點。

——————————————————————————————————————————————
關注公衆號,回覆 【算法】,獲取高清算法書!
在這裏插入圖片描述
內容來源《拉勾教育–重學數據結構與算法》
原文如下:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章