【數據結構】樹、二叉樹、二叉查找樹、AVL樹、伸展樹、B樹與C++中的set和map

這是數據結構類重新復習筆記的第 三 篇,同專題的其他文章可以移步:http://www.lxweimin.com/nb/39256701

樹的實現

實現樹時,對于每一個節點,除了存儲該節點的數據以外,還需要存儲一些外鏈。

一個典型的存儲方式是:左孩子右兄弟法,即對于每一個節點,存儲節點的數據、指向其孩子中最左邊的孩子的指針、指向其緊鄰的右側的兄弟節點的指針。

struct TreeNode
{
    Object element;
    TreeNode * firstChild;
    TreeNode * nextSiling;
};

如下邊的這棵樹通過這種方式表現的結果是這樣的:

采用“左子右兄弟”表示樹

二叉樹

二叉樹(binary tree)是一棵每個節點都不能有多于兩個兒子的樹

二叉樹的特點

  • 二叉樹第i層上至多有 2i-1 個節點
  • 深度為k的二叉樹至多有 2k-1 個節點
  • 對于一棵非空二叉樹,如果其葉子節點數為n0,度為2的節點數為n2,則n0=n2+1
  • 具有n個節點的完全二叉樹的深度為 [log2n]+1 (向下取整數)
  • 如果對于n個節點的完全二叉樹,第 i>0 的節點,其父節點為 [(i-1)/2] (向下取整)

二叉樹的一個性質是平均二叉樹的深度要比結點個數N小得多,這個性質有時很重要。分析表明,這個平均深度為O(√N),而對于特殊類型的二叉樹,即二叉查找樹( binary search tree),其深度的平均值是O(logN)。當然極端的樹的深度也可以大到N-1。

二叉樹的實現

由于二叉樹的每個節點最多有兩個兒子,所以可以直接連接到它們。

struct BinaryNode
{
    Object element;
    BinaryNode * left;
    BinaryNode * red;
};

二叉查找樹

二叉查找樹(Binary Search Trees)是二叉樹,它的特點是:對于任何一個節點X,其左子樹中的所有節點的值都小于該節點X,其右子樹中的所有節點的值都大于該節點X。如下圖中的左邊就是一棵二叉查找是,而右側不是:

二叉查找樹

重要的方法與其實現

  • isEmpty:是否為空樹,這一點很重要,一般在進行樹的相關操作時都會先確定是否是一個空樹,只要指向根節點的指針為NULL,就表示是一個空樹
  • contains:是否包含某項,在確定樹非空后,查找是否包含某個項,將目標項與根節點進行比較開始,如果比該節點大,就從右子樹查找,如果比該節點小,就從左子樹查找,如果出現相等的則表示包含該項,如果一直不相等且無子樹可以繼續查找,則不包含此項
  • findMin:找到最小值,一直找左子樹,直到找到沒有左子樹的左子樹最左邊的節點就是最小值
  • findMax:找到最大值,一直找右子樹,直到找到沒有右子樹的右子樹最右邊的節點就是最大值
  • insert:插入某個值,從根節點開始比較,如果目標值比該節點大就插入其右子樹,否則插入左子樹,如果出現相等的情況說明有該元素不需要再插入,直到插入某個空節點為止
  • remove:刪除節點
    • 刪除葉子節點:直接刪除
    • 刪除有一個子節點的節點:將該子節點掛到被刪除的節點的父節點上,取代刪除節點的位置


      刪除只有一個子節點的子節點
    • 刪除有兩個子節點的節點:找到該節點右子樹中的最小值或者左子樹中的最大值,將其替換要刪除的元素,然后遞歸地刪除用來替換的最大/小值,為什么是遞歸地刪除呢?因為被拿出來替換被刪除值的那個最大/小值也會有子樹,所以刪除它的時候還要執行刪除remove操作。但是最多只進行一次單子節點的刪除工作,因為無論是右子樹中的最小值還是左子樹中的最大值,最多只有一個子樹


      刪除有兩個子節點的子節

平均情況分析

操作 平均時間復雜度
isEmpty O(1)
contains O(logN)
findMin/findMax O(logN)
insert O(logN)
remove O(1)

AVL樹

AVL(Adelson-Velskii and Landis)樹是帶有平衡條件(balance condition)的二叉查找樹,這個平衡條件很容易保持并且保證了樹的深度為O(logN)。

AVL樹要求每個節點的左子樹和右子樹的高度差最多為1。

AVL樹

AVL樹除了插入操作外,其他所有的操作都可以最多以O(logN)的時間執行

AVL樹的插入

AVL樹的插入比較復雜的點在于插入的值可能會破壞樹原本的平衡,所以在插入值后需要進行旋轉(rotation)

旋轉的情況可以根據插入后的樹的情況分為如下四種:

示意圖來源:https://blog.csdn.net/gabriel1026/article/details/6311339

插入后的情況 描述 旋轉方式
LL-左子樹左高 在左子樹根節點的左子樹上插入節點而破壞平衡 右旋轉
RR-右子樹右高 在右子樹根節點的右子樹上插入節點而破壞平衡 左旋轉
LR-左子樹右高 在左子樹根節點的右子樹上插入節點而破壞平衡 先左旋后右旋
RL-右子樹左高 在右子樹根節點的左子樹上插入節點而破壞平衡 先右旋后左旋
  • LL-左子樹左高的情況


    LL-左子樹左高
  • RR-右子樹右高的情況


    RR-右子樹右高
  • LR-左子樹右高的情況


    LR-左子樹右高
  • RL-右子樹左高的情況


    RL-右子樹左高

伸展樹

伸展樹與攤還時間

伸展樹(splay tree)保證從空樹開始任意連續M此對樹的操作最多花費O(MlogN)的時間,即這M次連續的操作中即使有些操作耗時長,但是也有一些耗時短的操作,使得這M個連續的操作花費的總時間最壞為O(MlogN)。

攤還(amortized):一般說來,當M次操作的序列總的最壞情形運行時間為O(M f(N))時,我們就說它的攤還(amortized)運行時間為O(f(N))。因此,一棵伸展樹每次操作的攤還代價是O(logN)。經過一系列的操作,有的操作可能花費時間多一些,有的可能要少一些,不存在不好的輸入隊列。

如果任意特定操作可以有最壞時間界O(N),而我們仍然要求一個O(logN)的攤還時間界,那么很清楚,只要有一個結點被訪問,它就必須被移動。否則,一旦我們發現一個深層的結點,就有可能不斷地對它進行訪問。如果這個結點不改變位置,而每次訪問又花費O(N),那么M次訪問將花費O(MN)的時間。這個思想和數據庫中將經常訪問到的數據前移以及操作系統中將經常訪問的數據放入cache高速緩存等思想相同。(在許多應用中,當一個結點被訪問時,它就很可能不久再被訪問。研究表明,這種情況的發生比人們預料的要頻繁得多。)

伸展(splaying)

伸展樹的基本思想是將訪問到的一個較深的節點通過旋轉的方式將其旋轉到根節點的位置,但是為了保證旋轉過程中不讓一些較淺的節點也被淪落到較深的位置,這里的伸展采用一定的策略:

  • 首先,一個要被旋轉到樹根處的深節點X,如果X的父節點就是根節點,那么只要旋轉X和樹根即可
  • 如果X的父節點不是根節點,將其父節點命名為P,同時X一定有祖父節點G,X、P、G三個節點存在兩種排列關系:
    • “之字形”(zig-zag):這種情況下執行一次AVL樹同理的雙旋轉


      “之字形”(zig-zag)
    • “一字形”(zig-zig):這種情況下直接將樹左右對調(類似于蹺蹺板)


      “一字形”(zig-zig)

一個例子

如下圖,想把 K1 節點調至樹根處

一個伸展樹的例子

首先由K1、K2、K3構成了一個“之字形”結構,使用雙旋轉方式得到如下的第一次調整

一個伸展樹的例子

然后由K1、K4、K5構成了一個“一字形”結構,采用蹺蹺板的方式做第二次調整

一個伸展樹的例子

樹的遍歷

樹的遍歷分為三種

  • 先根遍歷:每棵樹按照 根-左子樹-右子樹 順序遍歷
  • 中根遍歷:每棵樹按照 左子樹-根-右子樹 順序遍歷
  • 后根遍歷:每棵樹按照 左子樹-右子樹-根 順序遍歷

B樹

B樹的思想很簡單,如果我們使用二叉樹,樹的平均深度為logN,如果我們是一個M叉樹(每個節點最多有M個子樹),則樹的平均深度為logMN,顯然這樣會使得樹的深度降低。

M階B樹的規范

  • 數據項存儲在樹葉上
  • 非葉結點存儲直到 M-1 個鍵,以指示搜索的方向;鍵 i 代表子樹 i+1 中的最小的鍵
  • 樹的根或者是一片樹葉,或者其兒子數在2和M之間
  • 除根外,所有非樹葉結點的兒子數在 [M/2](向上取整)和M之間
  • 所有的樹葉都在相同的深度上并有 [L/2](向上取整)和L之間個數據項

一個5階B樹的例子

一個5階的B樹,所有的非葉子節點的兒子都在3和5之間,從而有2到4個鍵,根可能只有兩個的兒子,L=5,因此每個樹葉有3到5個數據項,要求節點一半滿,保證B樹不致退化成簡單的二叉樹

一個5階B樹的例子

向B樹中插入 57 數據項:按照鍵索引到插入數據的位置,插入數據項

一個5階B樹的例子

再插入 55 數據項,導致該葉子節點數據超出5,所以需要分裂其父節點成為兩個葉子

一個5階B樹的例子

再插入 40 數據項,引起樹葉被分裂成兩片然后又造成父結點的分裂

一個5階B樹的例子

從B樹中刪除 99 數據項導致葉子節點的數據少于3從而合并葉子,而父節點也少于3從而再一次合并

一個5階B樹的例子

標準庫(SLT)中的set和map

set

set是一個排序后的容器,不允許重復。set特有的操作是高效的插入、刪除和執行基本查找。set也允許使用iterator來遍歷。

set的方法

  • vectorlist相同的方法
    • iterator begin():返回容器開始的迭代器
    • iterator end():返回容器結尾處的迭代器
    • int size() const:返回容器內的元素個數
    • bool empty():如果容器沒有元素,返回 true,否則返回 false
  • 特有的插入操作,set使用insert進行插入操作,由于非重復性,導致插入有可能會失敗,insert操作返回一個iterator指明插入新項的位置或者失敗時已有的項的位置.。pair是一個類模板,并且提供兩個成員 first 和 second 用來訪問返回值的兩項成員
    • pair<iterator, bool> insert( const Object & x); :插入Object x
    • pair<iterator, bool> insert( iterator hint, const Object & x);:在指定索引hint處插入Object x,比單參數的插入快得多,通常為O(1)
  • erase刪除操作
    • int erase( const Object & x); :刪除x,如果找到的話,返回刪除元素的個數,顯然只能返回0或者1
    • iterator erase( iterator itr);:刪除有iterator指定的位置的對象
    • iterator erase( iterator start, iterator end);:刪除由兩個iterator指定的位置對象中間包含的所有元素,包含前不包含后
  • find查找操作
    • iterator find( const Object & x) const;:查找x返回其位置

map

map用來存儲排序后的由鍵和值組成的項的集合,鍵必須唯一,但是多個鍵可以同時對應一個值,即值不需要唯一,鍵保持邏輯排序后的順序

map的方法

map的方法和set很像,但是其返回值是一個鍵-值對: pair<KeyType, ValueType>,map支持 beginendsizeenmtyinsertfinderasefind

  • insert操作必須提供 pair<KeyType, ValueType> 對象
  • find僅需要一個鍵,但是返回值的iterator還是指向一個 pair<KeyType, ValueType> 對象,并可以用 first 訪問返回的鍵,使用 second 訪問返回的值

map還重載了數組索引的操作符 []

ValueType & operator[] ( const KeyType & key );

如果map中存在key就返回只想相應值的引用,如果不存在key就在map中插入一個默認的值,然后返回指向這個插入的默認值的引用


轉載請注明出處,本文永久更新鏈接:https://blogs.littlegenius.xin/2019/08/19/【數據結構】三樹/

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容