【Java數據結構】樹及其應用
Java中的樹
一、樹基礎結構
1.樹定義
1.1.一些花裏胡哨的名詞
·結點
·結點的度、樹的度、結點的深度、結點的高度、樹的高(深)度
·孩子、雙親(父)、兄弟、祖先、子孫、堂兄弟
1.2.存儲方式
1)順序存儲
2)鏈式存儲
2.二叉樹
2.1.性質
·每個結點有0~2個孩子結點
·非空二叉樹葉子結點數=雙分支結點數+1
·二叉樹第n層最多有 2^(n-1)個結點
·高度爲n的二叉樹最多有2^n-1個結點
·二叉樹的結點要麼是葉子結點,要麼它有兩個子結點則稱其爲滿二叉樹
2.2.遍歷方式
·前序遍歷:根>左>右
·中序遍歷:左>根>右
·後序遍歷:左>右>根
PS:只有根是存值的,遍歷目的就是遍歷根的值,但需要決策是先看值還是先往深走
二、樹的進階與Java
1.完全二叉樹
1.1.定義
若設二叉樹的深度爲k,除第 k 層外,其它各層 (1~k-1) 的結點數都達到最大個數,第k 層所有的結點都連續集中在最左邊,這就是完全二叉樹。
1.2.性質
從上到下、從左到右依次編號,則編號具有規律:
1.3.優先級隊列中的堆排序
小頂堆:每個結點的值都小於或等於其左右孩子結點的值
2.二叉查找樹與平衡二叉樹(小陀螺)
2.1.定義
1)二叉查找樹
左子樹上所有節點的值均小於根節點的值,而右子樹上所有結點的值均大於根節點的值,左小右大
2)平衡二叉樹
想象插入的序列越接近有序,生成的二叉搜索樹就越像一個鏈表,查找意義不大。
爲了避免二叉搜索樹變成“鏈表”,我們引入了平衡二叉樹,即讓樹的結構看起來儘量“均勻”,左右子樹的節點數儘量一樣多。
他對二叉樹做了改進,在我們每插入一個節點的時候,必須保證每個節點對應的左子樹和右子樹的樹高度差不超過1。
如果超過了就對其進行調平衡(左左、左右、右左、右右)。
3.紅黑樹
3.1.定義
1)二三樹
在二叉查找樹基礎上增加性質,節點有二節點和三節點之分,
二節點下面有兩個子節點,二節點裏面可以容納一個值,而三節點下面有三個子節點,三節點裏面可以容納兩個值。
具體大小規則如圖
當有新的值插入時假如新值要進入二節點時就直接進去。但如果正好需要放在三節點中,需要將該節點分裂成兩個節點,並將中間的數提到父節點中去,
當然如果將子節點提到父節點的時候導致了父節點裏的數超過了兩個,就繼續向上提,直到滿足了爲止。
2)紅黑樹
和二三樹很相像,基本上就是二三樹的一個變形。滿足以下特徵
· 每個結點或者是黑色,或者是紅色。
· 根結點是黑色。
· 每個葉子結點(無值結點)是黑色。
· 如果一個結點是紅色的,則它的子結點必須是黑色的
· 從一個結點到該結點的子孫結點的所有路徑上包含相同數目的黑節點。
對比看出紅黑樹中紅結點就是二三樹的三節點中較小的值。至於不對比二三樹看紅黑樹是怎麼調平的,也有左旋、右旋、變色的方式來調平。
3.2.性質
紅黑樹相比平衡二叉樹,在檢索的時候效率其實差不多,都是通過平衡來二分查找。但對於插入刪除等操作效率提高很多。
紅黑樹不像平衡二叉樹一樣追求絕對的平衡,他允許局部很少的不完全平衡,這樣對於效率影響不大,但省去了很多沒有必要的調平衡操作,
平衡二叉樹調平衡有時候代價較大,所以效率不如紅黑樹。
3.3.HashMap,TreeMap中的紅黑樹
Java的TreeMap就是紅黑樹。
而HashMap採用 Hash 算法來決定集合中元素的存儲位置。
當系統開始初始化 HashMap 時,系統會創建一個長度爲 capacity(默認16) 的 Entry 數組table,
這個數組裏可以存儲元素的位置被稱爲“桶(bucket)”,每個 bucket 都有其指定索引,
系統可以根據其索引快速訪問該 bucket 裏存儲的元素。
HashMap 的每個“桶”只存儲一個元素(也就是一個 Entry),
由於 Entry 對象可以包含一個引用變量(就是 Entry 構造器的的最後一個參數)用於指向下一個 Entry,
因此可能出現的情況是:HashMap 的 bucket 中只有一個 Entry,
但這個 Entry 指向另一個 Entry ——這就形成了一個 Entry 鏈。
put和get都首先會調用hashcode方法,去查找相關的key,當有衝突時,再調用equals。
如果一個 Entry 鏈元素由少增多至大於8個時轉爲TreeMap即紅黑樹存儲,
當元素個數由多減少至小於6個時轉爲數組存儲。
1)WHY16?
每次放入新元素hashMap會重新計算其index
int index =key.hashCode()&(length-1);
16首先不大不小,既不浪費資源也不會極易觸發擴容,其次還有個隱藏祕籍,
因爲是將二進制進行按位於,(16-1) 是 1111,末位是1,
這樣也能保證計算後的index既可以是奇數也可以是偶數,並且只要傳進來的key足夠分散,
這樣也就減少了hash的碰撞。
2)WHY8?
前提已知,鏈表,查詢成本高,新增成本低。紅黑樹,查詢成本低,新增成本高。
單從平均查找長度n/2和log2(n)來說在n=4時log2(n)開始小於n/2,但根據註釋
“TreeNodes佔用空間是普通Nodes的兩倍,所以只有當bin包含足夠多的節點時纔會轉成TreeNodes”
(Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes)
說白了就是trade-off,空間和時間的權衡。
4.B-Tree
4.1.定義
從完全二叉樹進化到平衡二叉樹提高了查詢能力,
從平衡二叉樹進化到紅黑樹提高了插入能力,
但還是沒有擺脫典型的二叉查找樹結構,查找的時間複雜度O(log2N)且效率與樹的深度緊密相關,
這樣就有了一個實際問題,就是大規模數據存儲中,實現索引查詢這樣一個實際背景下,
數據一般是在磁盤中存儲的,磁盤查找存取的次數往往由樹的高度所決定,所以
二叉查找樹結構由於樹的深度過大而造成磁盤I/O讀寫過於頻繁,進而導致查詢效率低下。
多路查找樹,一個新的查找樹結構。根據平衡二叉樹的啓發,自然就想到平衡多路查找樹結構。
BTree,B即Balanced,平衡的意思(不是Binary…路子多得很)。
磁盤讀取數據是以盤塊(block)爲基本單位的。
讀取分爲查找、等待、傳輸,其中等待取決於磁盤轉速,傳輸時間基本固定,最影響效率的就是讀取。
查找就是磁頭移動找到對應盤塊的過程,位於同一盤塊中的所有數據都能被一次性全部讀取出來,
所以要儘量保證數據都在同一盤塊或者同一柱面再不濟是相鄰柱面上。
對於樹,一個結點內的數據必然在一個盤塊上,但若獲得其子結點就要根據指針再次查找盤塊.
小貼士:B-tree就是指的B樹
這是一個翻譯失誤,國內很多人喜歡把B-tree譯作B-樹。
4.2.性質
一棵m(m>=2)階的B樹(並非m叉樹)有以下特點:
· *樹中每個結點最多含有m個孩子
· *除根結點和葉子結點外,其它每個結點至少有ceil(m / 2)個孩子
· *若根結點不是葉子結點,則至少有2個孩子
· *所有葉子結點(無值結點)都出現在同一層,葉子結點不包含任何關鍵字信息
· *每個非終端結點中包含有n個關鍵字信息ceil(m / 2)-1<= n <= m-1<,且從左至右升序br/>
· 每個非終端結點有關鍵字Ki,指向子樹結點的指針Pi,每個節點都是連續存儲的(同一盤塊)
· 一棵含有n個總關鍵字數的m階的B樹的最大高度是log_ceil(m/2)((N+1)/2)+1
如圖一個3階B樹(除根節點至少2個分支)
以一個5階B樹(除根節點至少3個分支,每個結點2~4個數據)介紹操作
插入{1,2,6,7,11,4,8,13,10,5,17,9,16,20,3,12,14,18,19,15}
刪除{8,16,15,4}
小貼士:
B樹的新值一定落在最底層非葉子節點
如果插入使得超過關鍵字限制則進行結點拆分
如果刪除使得低於關鍵字限制則向兄弟結點借或和孩子結點交換,必要時也可能進行合併
5.B+Tree(還有B*Tree不講了)
5.1.定義
上面是BTree下面是B+Tree
所有的非終端結點可以看成是索引部分
小貼士:
有的B+Tree的介紹中會在葉子結點間增加左向右的指針。
這個並不是原本B+Tree定義帶的,而是對B+Tree一種優化,
目的是提高遍歷效率和範圍查找的能力。
5.2.性質
1)B+tree的磁盤讀寫代價更低
如圖B+tree的內部結點並沒有指向關鍵字具體信息的指針。因此其內部結點相對B 樹更小。
如果把所有同一內部結點的關鍵字存放在同一盤塊中,那麼盤塊所能容納的關鍵字數量也越多。
一次性讀入內存中的需要查找的關鍵字也就越多。相對來說IO讀寫次數也就降低了。
2)B+tree的查詢效率更加穩定
所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當,查詢時間複雜度爲log_m(N)。
5.3.數據庫中的索引
1)Postgre默認BTree
· Hash索引不適用於範圍查詢。
· Hash索引無法被用來避免數據的排序操作,也無法利用Hash索引來加速任何排序操作。
· 當哈希值大量重複且數據量非常大時,Hash固有機制浪費性能(解決衝突等)。
· 不能用部分索引鍵來搜索,因爲組合索引在計算哈希值的時候是一起計算的。
· 不可避免表掃描。
2)BTree優勢的發揮
根據之前的描述,假設樹高度爲H,B/B+樹每次檢索最多檢索H次。
數據庫系統的設計者巧妙利用了磁盤預讀原理,
對於BTree將一個結點的大小設爲等於一個頁,這樣每個結點只需要一次I/O就可以完全載入,保證最多IO次數爲H。
而B+Tree就更加暴力,大極端情況把所有索引層結點放在一個頁,只需2次IO。
在數據庫索引中B/B+樹的階M通常很高,來保證H很小。
5.4. HBase中的LSM樹
1)B+Tree的缺點
儘管B+Tree在對此盤IO效率的優化上已經做出了很大的突破,任然避免不了自己被隨機IO審判的一天。
身爲70年代的老怪物,還能活躍在人們面前的主要原因是,CPU和RAM基本一年左右就可以翻一倍的進化,而磁盤尋道技術一年也就提高5%。
但是數據量的增大由不得硬件支配,總有人會想辦法解決,既然怎麼樣都有隨機IO,那就把隨機編程順序。
既然要變成順序,那就要預先準備好寫入的東西,比如一坨結點而不是一個。
還有一種主流的解釋是:
隨着新數據的插入,葉子節點會慢慢分裂,邏輯上連續的葉子節點在物理上往往不連續,甚至分離的很遠。
但做範圍查詢時,會產生大量讀隨機IO,對於大量的隨機寫也一樣,舉一個插入key跨度很大的例子,如3->1000->7->2000 …
新插入的數據存儲在磁盤上相隔很遠,會產生大量的隨機寫IO。
2)LSMTree(Log-Structured Merge-Tree)
日誌結構合併樹,這裏的‘日誌結構’指的就是數據庫追加型的日誌,‘合併樹’看圖:
LSMTree是把原本一個大樹拆成了幾個分層的樹,每層的容量大概是上一層的10倍,
每當一個分層的容量達到閾值時,就會抽取一部分有序密集的子樹,拿到下一層合併,這樣做就保證了順序寫。
在HBase中,首先是在內存中有個小樹,新插入的數據會先預寫在HLog中再記錄到內存中,當小樹達到一定閾值就觸發刷盤、合併。
不難發現其查詢時間複雜度爲(N/t)log_m(N),t是閾值,這樣看是大於B+Tree,易寫不易讀。
LSM Tree放棄磁盤讀性能來換取寫的順序性,用來大幅進步寫性能。
3)HBase再升級
bloom filter:
就是個帶隨即概率的bitmap,可以快速的告訴你,某一個小的有序結構裏有沒有指定的那個數據的。
於是我就可以不用二分查找,而只需簡單的計算幾次就能知道數據是否在某個小集合裏啦。效率得到了提升,但付出的是空間代價。
compact:
因爲hbase一般是部署在hdfs上,hdfs不支持對文件的update操作,
所以hbase是整體內存flush到HRegion磁盤中(DataNode),這樣MemStore就變成了DataNode上的磁盤文件StoreFile,
定期HRegionServer對DataNode的數據做merge操作,徹底刪除無效空間,多棵小樹在這個時機合併成大樹,來增強讀性能。
compact:
因爲hbase一般是部署在hdfs上,hdfs不支持對文件的update操作,
所以hbase是整體內存flush到HRegion磁盤中(DataNode),這樣MemStore就變成了DataNode上的磁盤文件StoreFile,
定期HRegionServer對DataNode的數據做merge操作,徹底刪除無效空間,多棵小樹在這個時機合併成大樹,來增強讀性能。