1、樹查找html
在之間介紹數據結構的文章中咱們介紹過二叉樹查找,若是忘記的你們能夠查看下這篇文章數據結構-樹的那些事(四),這裏對二叉樹就不作介紹,咱們來講一下二叉排序樹;java
二叉排序樹(Binary Search Tree):又被稱爲二叉查找樹或者二叉搜索樹,固然首先是二叉樹,另外特色以下:node
1.若它的左子樹不爲空,則左子樹的結點小於它根節點的值;git
2.若它的右子樹不爲空,則右子樹的結點大於它根節點的值;github
3.它的左、右子樹也分別爲二叉排序樹;算法
明白特色接下來咱們來講一下查找的性能,二叉排序樹查找性能取決於二叉樹的形狀,可是二叉樹排序的形狀是不肯定,最壞的狀況查找性能爲O(n),最好狀況查找性能爲O(logn),以下圖數據庫
接下來咱們談一下二叉排序樹的增長和刪除操做,查詢就不必說明,就寫下代碼實現下就好;網絡
增長操做:數據結構
1.若當前的二叉樹排序樹爲空,則插入元素的根節點;ide
2.若插入的值小於根節點,則插入到左子樹當中;
3.若插入的值大於根節點,則插入到右子樹當中;
刪除操做:
1.若刪除的是子節點,則直接刪除該結點;
2.若刪除的節點僅有左或者右子樹的結點,則讓該結點的子樹與父親節點相連,刪除該結點便可;
3.若刪除的節點左子樹和右子樹都有子結點,以下面兩幅圖,這裏面強調一下第二幅圖,刪除47之後用37或者48均可以知足二叉排序樹,這裏咱們爲何選擇47而要放棄37,是由於二叉排序樹按照中序遍歷之後造成的是一個有序序列(由小到大排序),選擇37之後不知足這個特徵;
上面咱們談到二叉排序樹性能問題,接下來咱們來講一下平衡二叉樹看如何處理二叉樹性能問題,全部代碼在介紹完成之後提供實現;
平衡二叉樹(AVL樹):首先平衡二叉樹是一顆二叉樹排序樹,他的特色是每個節點左子樹和右子樹的高度差至多等於1,這樣就能避免照成單枝樹,影響查詢效率,來個看圖識AVL樹,下圖中圖二知足二叉排順序樹的特徵,58節點的左子樹大於59,圖三58的左子樹的高度爲2,右子樹的高度爲0,高度差大於1,因此不知足平衡二叉樹,接下來咱們談談平衡二叉樹插入和刪除照成失衡之後的操做(平衡二叉樹的旋轉)。
失衡之後操做:
失衡之後可能形成4種狀況:LL(左左)、RR(右右)、LR(左右)、RL(右左),接下來咱們用圖說話,看一下這4種狀況;
對這4種狀況進行統一整理就是:插入或刪除一個節點後,根節點的左子樹的右子樹還有非空子節點,致使"根的左子樹的高度"比"根的右子樹的高度"大2,致使AVL樹失去了平衡。就用第一種說一下當插入D的時候致使,致使5左子樹的高度爲3,5的右子樹爲1,差值大於1,這個時候咱們抓住3,使5進行右旋,3的右孩子變成5的左孩子這個時候平衡達成。
這裏簡單講一下對應結點的抽象,不一樣於二叉樹的狀況就是這裏要增長一個高度的屬性,好用來判斷左子樹和右子樹的差值;
B樹:首先要明白B樹要處理什麼問題,咱們上面提到過的AVL樹、二叉排序樹以及咱們沒有提到過的紅黑樹,這些都是在內存中內部查找樹,而B樹是前面平衡樹算法的擴展,它支持保存在磁盤或者網絡上的符號表進行外部查找,這些文件可能比咱們之前考慮的輸入要大的多(難以存入內存)。既然內容保存在磁盤中,那麼天然會由於樹的深度過大而形成磁盤I/O讀寫過於頻繁(磁盤讀寫速率是有限制的),進而致使查詢效率低下。那麼下降樹的深度天然很重要了。所以,咱們引入了B樹,多路查找樹。
明白了B樹是幹什麼用的,咱們來個他下個定義,B樹是一種平衡的多路查找樹,結點最大的孩子數目爲B樹的階,一個m階的B樹特徵以下:
1.每一個根節點至少有2個孩子節點;
2.每一個非根的分支節點都包含k-1個元素和k個孩子,其中 m/2 <= k <= m;
3.每個葉子節點都包含k-1個元素,其中 m/2 <= k <= m;
4.全部的葉子節點都位於同一層次;
5.每一個節點中的元素從小到大排列,節點當中k-1個元素正好是k個孩子包含的元素的值域分劃(這個在下面解釋下);
接下來咱們以3階樹爲例子來講說上面這些特徵是如何體現的,而後在討論下B樹的增長和刪除節點狀況,
上圖是一顆3階樹,首先最大的孩子數目爲3,因此是3階樹,咱們來看下12,這是2結點,包含1個根節點和2個孩子結點,知足第2,3條,左子樹11小於12,右子樹1三、15大於12,接下來咱們看2,6結點,3結點包含二、6兩個元素,和3個孩子結點,左子樹1小於二、6,右子樹8大於二、6,三、5位於左、右子樹中間,知足以上條件;
增長操做有3種狀況:
1.對於空樹,插入一個2結點便可;
2.插入到一個2結點的葉子上。這種狀況考慮將其升級爲3結點便可,這裏進行解釋一下,當插入3的時候,經過遍歷會發現3比4小,只能插入到4的左子樹,4有個子結點,這個時候只能將其升級爲3結點,3比1大,插入到1的後繼,這個時候造成右圖樣子。
3.插入3結點的狀況相對比較麻煩,3結點是3階樹最大的容量,當向3結點插入的時候就要考慮拆分的狀況,咱們來看一下這幾種狀況。
第一種狀況:向2結點滿元素的子節點插入時候,左圖知足這種狀況,當向4的右孩子結點6,7插入5的時候,六、7已是3結點的,不能在增長,這個時候發現4結點是2結點,就須要將其升級爲3結點,因而講六、7拆分,6與4組成3結點,5成爲中間孩子,7成爲右孩子,如右圖。
第二種狀況:當向3結點的滿元素的子節點插入時候,左圖知足這種狀況,當向十二、14的左孩子九、10插入11的時候,發現九、10已經知足3結點,不能在增長,可是父親節點也是3結點,這個時候在向上查找,發現十二、14的父親結點8,還能夠進行插入,這個時候講8升級爲3結點,將8與12合併,最終生成右圖樣子。
再來看一種比較特殊的狀況:當向四、6左孩子結點一、3插入元素的時候,發現都是3結點沒法在進行拆分,這個是將一、3結點、四、6結點、八、12結點都進行拆分,造成右圖樣子;
刪除操做:
1.刪除位於3結點的葉子結點,刪除改元素便可,不會影響到別的結構,以下圖:
2.刪除元素位於一個2結點上,這裏的狀況比較複雜,咱們分狀況介紹:
第一種狀況:該結點的雙親都是2結點,且擁有一個3結點的孩子,以下圖,刪除結點1,這種狀況只須要左旋,6成爲父親節點,4成爲6的左孩子,7成爲6的右孩子。
第二種狀況:該節點的雙親是2結點,右孩子也是2結點,以下圖,刪除4,左旋致使沒有右孩子,不知足B樹定義的第一條,因此這個時候須要對樹的結構進行調整,首先將7進行左旋,將8進行左旋,9也進行左旋,造成圖三所示的樣子。
第三種狀況:該結點雙親是一個3結點,以下圖所示,刪除10結點,十二、14不能成爲3結點,因而將此結點拆分,並將十二、13合併成左孩子。
第四種狀況:滿二叉樹的狀況,刪除任何一個葉子都會使整棵樹不能知足第一條的定義,以下圖所示,當刪除8的時候,須要考慮將整棵樹的層數減小,這個時候將六、7合併爲9的左孩子,這個時候不知足第4條定義,須要將九、14合併爲,最終造成右圖所示。
3.刪除的元素元素若是不是葉子結點,則考慮按照中序遍歷後獲得的該元素的前驅或者後繼來進行替換該結點。這裏咱們就不上圖來講明瞭。
接下來咱們作一些思考,由二叉樹查找、二叉樹排序樹、平衡二叉樹、B樹咱們能得出一個結論,樹的高度越小查找的速度越快,當元素的數目必定的時候,怎麼樣才能使樹的高度下降,固然是擴展樹的階數才能下降樹的高度,以前很火的什麼4Y個URL中查詢某個URL等之類的題,我想看到這裏的時候你們會有一些想法了吧,等等之後我會對類問題進行一次總結,此次先不談了。
再來作一個思考,對於n個關鍵字的m階樹,最壞須要須要幾回查找?
1.由於根至少有兩個孩子,所以第2層至少有兩個結點。
2.除去跟結點和葉子外,每一個分支的結點至少都有m/2個孩子;
3.第三層結點的個數2*m/2個節點;
4.第四層結點的個數2*(m/2)^2個結點;
5.第K+1層結點的個數2*(m/2)^k-1個結點,這裏K+1層就是葉子結點;
6.B樹有n個關鍵字,當你找到葉子節點,就等於查找不成功的結點爲n+1,所以n+1>=2*(m/2)^k-1,即k<=log(m/2)((n+1)/2)+1;
7.結論根結點到關鍵字結點涉及的結點個數不操過k<=log(m/2)((n+1)/2)+1;
總結以下,一顆n個關鍵字結點的m階樹最大高度爲k<=log(m/2)((n+1)/2)+1,B樹的每一個非根的分支節點m個孩子,其中 m/2 <= m<= m,隨着m的增長,B樹的高度降低,查找性能提高;
B+樹:
接下來咱們繼續探討一個問題,看下圖,當咱們對B樹進行中序遍歷的時候,假設每個結點都在硬盤不一樣的頁面上,這個時候必然會通過頁2---頁1--頁3--頁4--頁1---頁4---頁1---頁5,這樣會致使每次那一個元素都要對結點的元素進行遍歷,咱們有沒有什麼辦法讓元素只被訪問一次,帶着這個問題咱們來看下B+樹。
接下來咱們仍是老套路看圖說話,下圖是一顆B+樹,其有以下特色:出如今分支結點中的元素會被看成分支結點位置的中序後繼結點再次列出,另外每一個葉子結點都會保存一個執行後一個葉子結點的指針。
一顆m階樹的B+樹和m階的B樹差別以下:
1.n顆子樹的結點包含n個關鍵字;
2.全部的葉子結點包含所有關鍵字的信息,以及指向這些關鍵字記錄的指針,葉子結點自己按照從小到大的順序進行排列;
3.全部分支結點均可以當作索引,結點中含有子樹的最大(或最小)關鍵字。
若是上面的B+樹看的還不是很直觀,那麼咱們再來看一顆更直觀一點的;這裏咱們就不談什麼頁分裂問題,這個問題待索引的時候再來探討,咱們來講下B+樹和B樹在查找方面的比較,首先2者與二叉樹比較,提高了磁盤I/O效率,可是B樹沒有解決遍歷元素時效率低的問題,如同上面提出的那個問題,這裏咱們能夠經過B+樹來解決,B+樹只須要經過葉子節點遍歷就能夠實現對整棵樹的遍歷。B+更大的優點在於範圍查找,這一點是B樹沒法比較的,進行範圍查找是隻須要在葉子結點上遍歷便可。B+樹的插入和刪除都與B樹相似,可是插入和刪除都是在葉子結點上進行。
上面介紹樹的幾種不一樣的數據結構,主要是爲了引出B+樹,明白這種結構的好處才能爲咱們後面數據庫索引打下基礎,另外這裏沒有介紹紅黑樹,這個等HashMap源碼解讀的時候再來看,Java代碼實現主要提供平衡二叉樹的實現,B樹的實現仍是給你們提供一個參考https://github.com/int32bit/algorithms/blob/master/B-Tree/src/BTree.java;
2、Java代碼實現
public class AVLNode<T extends Comparable<T>> { public T key;//關鍵字 public int height;//高度 public AVLNode<T> lchild;//左孩子 public AVLNode<T> rchilid;//右孩子 public AVLNode(T key){ this(key,null,null); } public AVLNode(T key,AVLNode<T> lchild,AVLNode<T> rchilid){ this.key=key; this.lchild=lchild; this.rchilid=rchilid; this.height=0; } } public class AVLTree<T extends Comparable<T>> { private AVLNode<T> root; public AVLNode<T> getRoot() { return root; } public AVLTree(){ this.root=null; } public AVLTree(T key){ this.root=new AVLNode<T>(key); } //獲取樹的高度 private int height(AVLNode<T> node){ if (node!=null){ return node.height; } return 0; } public int height(){ return height(root); } //比較兩個值的大小 private int max(int a,int b){ return a>b?a:b; } //遞歸前序遍歷 private void preOrder(AVLNode<T> node){ if (node!=null){ System.out.print(node.key+" "); preOrder(node.lchild); preOrder(node.rchilid); } } public void preOrder(){ preOrder(root); } //中序遍歷 private void midOrder(AVLNode<T> node){ if (node!=null){ midOrder(node.lchild); System.out.print(node.key+" "); midOrder(node.rchilid); } } public void midOrder(){ midOrder(root); } //後序遍歷 private void postOrder(AVLNode<T> node){ if (node!=null){ postOrder(node.lchild); postOrder(node.rchilid); System.out.print(node.key+" "); } } public void postOrder(){ postOrder(root); } //遞歸查找key元素 private AVLNode<T> search(AVLNode<T> node,T key){ if (node==null) return node; int compare=key.compareTo(node.key); if (compare<0) return search(node.lchild,key); else if (compare>0) return search(node.rchilid,key); else return node; } public AVLNode<T> search(T key){ return search(root,key); } //LL旋轉 private AVLNode<T> llRotation(AVLNode<T> node){ AVLNode<T> newNode; newNode=node.lchild; node.lchild=newNode.rchilid; newNode.rchilid=node; node.height=max(height(node.lchild),height(node.rchilid))+1; newNode.height=max(height(node.lchild),node.height)+1; return newNode; } //RR旋轉 private AVLNode<T> rrRotation(AVLNode<T> node){ AVLNode<T> newNode; //根結點右旋 newNode=node.rchilid; node.rchilid=newNode.lchild; newNode.lchild=node; node.height=max(height(node.lchild),height(node.rchilid))+1; newNode.height=max(height(newNode.rchilid),node.height)+1; return newNode; } //LR旋轉 private AVLNode<T> lrRotation(AVLNode<T> node){ node.lchild=rrRotation(node.lchild); return llRotation(node); } //RL旋轉 private AVLNode<T> rlRotation(AVLNode<T> node){ node.rchilid=llRotation(node.rchilid); return rrRotation(node); } //插入結點 private AVLNode<T> insert(AVLNode<T> node,T key){ if (node==null){ node=new AVLNode<T>(key); }else { int cmp=key.compareTo(node.key);//比較節點的值 if (cmp<0){//小於插入左子樹 node.lchild=insert(node.lchild,key); //判斷是否失衡 //向左側插入結點只能照成ll或者lr狀況 if (height(node.lchild)-height(node.rchilid)==2){ //插入的結點和當前結點的左子樹比較 //大於說明是LR狀況否者LL if (key.compareTo(node.lchild.key)>0) node=lrRotation(node); else node=llRotation(node); } }else if (cmp>0){ //同上下面不作介紹 node.rchilid=insert(node.rchilid,key); if (height(node.rchilid)-height(node.lchild)==2){ if (key.compareTo(node.rchilid.key)>0) node=rrRotation(node); else node=rlRotation(node); } }else { System.out.println("添加失敗:不容許添加相同的節點!"); } } node.height=max(height(node.lchild),height(node.rchilid))+1; return node; } public void insert(T key){ root=insert(root,key); } //查找最大值 private AVLNode<T> findMax(AVLNode<T> node){ if (node==null) return null; while (node.rchilid!=null){ node=node.rchilid; } return node; } public T finMax(){ AVLNode<T> p=findMax(root); if (p!=null){ return p.key; } return null; } //查找最小值 private AVLNode<T> finMin(AVLNode<T> node){ if (node==null) return null; while (node.lchild!=null){ node=node.lchild; } return node; } public T finMin(){ AVLNode<T> p=finMin(root); if (p!=null){ return p.key; } return null; } //刪除結點 private AVLNode<T> remove(AVLNode<T> node,AVLNode<T> del){ if (node==null||del==null) return null; //刪除的結點和當前結點比較 int cmp=del.key.compareTo(node.key); if (cmp<0){ //遞歸向左查找結點 node.lchild=remove(node.lchild,del); //在左子樹中刪除後該節點失衡,若失衡,則能夠確定的是該節點的右子樹比左子樹高 if (height(node.rchilid)-height(node.lchild)==2){ AVLNode<T> rTree=node.rchilid;//右子樹失衡2種狀況 右右和右左 if (height(rTree.lchild)>height(rTree.rchilid)) node=rlRotation(node); else node=rrRotation(node); } }else if (cmp>0){ node.rchilid=remove(node.rchilid,del);//同上相反左邊失衡 if (height(node.lchild)-height(node.rchilid)==2){ AVLNode<T> lTree=node.lchild; if (height(lTree.rchilid)>height(lTree.lchild)) node=lrRotation(node); else node=llRotation(node); } }else { //找到了要刪除的節點,該節點左右子樹都不爲空 if ((node.lchild!=null)&&(node.rchilid!=null)){ //判斷左右孩子的高度 if (height(node.lchild)>height(node.rchilid)){ //若是左子樹高度大於右子樹 //則找到左子樹最大的結點替換當前結點 //這樣操做會避免失衡 AVLNode<T> maxNode=findMax(node.lchild); node.key=maxNode.key; node.lchild=remove(node.lchild,maxNode); }else { //同上 AVLNode<T> minNode=finMin(node.rchilid); node.key=minNode.key; node.rchilid=remove(node.rchilid,minNode); } }else {//單一結點則刪除 node=(node.lchild!=null)?node.lchild:node.rchilid; } } return node; } public void remove(T key){ AVLNode<T> removeNode=search(key); if (removeNode!=null) root=remove(root,removeNode); } } public class AVLTest { private static int arr[]= {1,2,3,4,5,6,7,8,9,10,12,11,13,14,15 }; public static void main(String[] args) { AVLTree<Integer> tree = new AVLTree<Integer>(); for (int i=0;i<arr.length;i++){ System.out.printf("%d ", arr[i]); tree.insert(arr[i]); } System.out.printf("\n前序遍歷: "); tree.preOrder(); System.out.printf("\n中序遍歷: "); tree.midOrder(); System.out.printf("\n後序遍歷: "); tree.postOrder(); System.out.printf("\n"); System.out.printf("高度: %d\n", tree.height()); System.out.printf("最小值: %d\n", tree.finMin()); System.out.printf("最大值: %d\n", tree.finMax()); tree.remove(7); System.out.printf("\n高度: %d", tree.height()); System.out.printf("\n中序遍歷: "); tree.midOrder(); } }
3、數據庫索引
已經探討B+樹的好處,接下來咱們來看一下B+插入和刪除操做;
插入操做:
B+樹插入必須保證插入後的葉子結點記錄依然排序,另外還須要考慮插入到B+樹的三種狀況,接下來咱們來談一下這3種狀況:
1.當Leaf Page和Index Page都未滿的時候,直接將記錄插入到葉子節點便可(Leaf Page和Index Page分別指的是葉子結點和父親結點);
當插入28這個值的時候,Leaf Page和Index Page都未滿,直接插入到葉子節點
2.當Leaf Page滿,Index Page未滿時候,首先拆分Leaf Page,將中間結點放入到Inde Page中,而後小於中間節點的放左邊,大於或者等於中間結點的放右邊;
再次插入70這個值的時候,Leaf Page已經滿,可是Index Page還沒滿,在根節點插入葉子節點中間結點,而後在進行頁分裂;
3.當Leaf Page和Index Page都滿的時候,首先拆分Leaf Page,小於中間結點的記錄放左邊,大於或者等於中間結點的記錄放右邊,接下來拆分Index Page,至關於提高整顆樹的高度,小於中間結點的放入到左邊,大於或者等於中間結點的放入到右邊,最後講中間節點放入到上一層Index Page;
接下來插入95,這個時候Leaf Page和Index Page都滿值,沒辦法插入,這個時候就須要進行2次拆分,首先拆分葉子結點,最後在拆分根結點。
以上圖2和3都沒有添加雙向鏈表,主要是爲讓你們看明白分裂的狀況。
刪除操做:
B+樹使用填充因子來控制樹的刪除刪除變化,50%是填充因子可設置的最小的值,小於這個值這個時候就須要作合併操做,刪除操做同時也必須保證刪除後的結點依然排序。
1.Leaf Page和Index Page都大於填充因子,若是刪除的是葉子節點直接刪除就好,若是刪除的是Index Page的元素,使用該結點的右結點代替;
如上圖刪除70這個節點,直接刪除就能夠了
2.Leaf Page小於填充因子,Index Page大於填充因子,這個時候合併葉子節點和他的兄弟節點,更新Index Page;
接下來刪除25,在這個時候知足第一種狀況,可是這個值又在Index Page中,刪除25之後,還須要將右兄弟替換到Page Index中。
3.Leaf Page和Index Page都小於填充因子,這個時候須要葉子節點作合併,當刪除掉Index Page結點之後Index Page也須要作合併。
接下來咱們刪除60,這個時候刪除60之後Leaf Page不知足填充因子,進行合併,同時刪除Index Page的值也須要作合併。
B+樹索引的本質就是B+樹在數據庫中的實現,B+樹在數據庫中的高度通常爲2-4層,這個怎麼實現的咱們先不要去管,可是咱們要知道這樣作就會致使咱們查詢速度很快,高度爲2-4層意味着咱們查找一個數據所須要作的IO操做爲2-4次,磁盤每秒能夠作100次的IO操做,這就意味着咱們能在0.02-0.04秒查找出數據。是否是很快。數據庫中分爲彙集索引和非彙集索引,這二者的差異在於葉子節點是否存在一整行數據。
彙集索引:
這裏思考一個問題,咱們在建表的時候會創建一個主鍵,這是爲何?其實這個主鍵就是咱們的彙集索引,要是不創建主鍵,那麼咱們存儲的數據就會在無序的存在磁盤中,固然查詢會慢,可是創建主鍵之後,數據庫中的數據會按照B+樹的特色進行構建,整個表變成一個索引,葉子節點存放的是表中的整行數據,這就是咱們所說的彙集索引,非葉子節點的索引存放的是鍵值以及數據頁的偏移量。彙集索引的好處在於,對於主鍵查找的和排序很是快,由於葉子節點存放的就是咱們的數據,另外每一個葉子節點之間的鏈接都是雙向鏈表,不須要再一次進行查找。
非彙集索引:
非彙集索引的葉子節點並不包含全部行的數據,葉子節點除了包含鍵值之外,每一個葉子節點的索引包含一個書籤,用來保存相對應的行數據的彙集索引的鍵。因此當經過非彙集索引查詢數據的時候,首先會遍歷非彙集索引並經過葉子節點的指針得到指向彙集索引的鍵,而後經過彙集索引查找與之匹配的葉子節點。每次給表中增長一個非彙集索引,表中的字段就會被複製出來一份,生成索引,因此給表添加索引就會增長表空間,減慢插入時候的速度。
聯合索引:
聯合索引就是在表上的多個列創建索引,聯合索引本質上仍是一棵B+樹,不一樣的是聯合索引的鍵值數量不是1,而是大於等於2。爲何須要聯合索引的存在?
1.假設(a,b)列創建索引,對於查詢條件而言,當查詢的條件爲a和b,則可使用,條件單獨爲a的時候也可使用,可是爲b的狀況不可使用;
2.對於(a,b)列創建的索引,第二個列是排序的,這樣當咱們按照b條件排序的時候就不須要進行排序操做;
明白了索引的原理我想你們之後在優化查詢的時候也有清晰的思路了吧,以上是B-Tree索引介紹,另外Hash索引咱們之後再聊。
4、結束語
主要參考《大話數據結構》和《MySQL技術內幕:InnoDB存儲引擎》
提早祝福你們新年快樂!今天是個人最後工做日了~能夠回家看望父母了!另外有什麼不懂能夠進羣諮詢438836709~