B树学习总结
B樹
多路搜索樹
當數據規模足夠大的時候, 大部分數據存儲到外存中, 常規的平衡二叉搜索樹將無法滿足需求理由如下:
常規二叉平衡搜索樹每當進行search(),insert(),remove()操作的時候就會進行節點的切換,從而導致大量的IO操作,就此提出了多路搜索,嘗試將原本的多個節點合在一起,用于減少IO操作; 適用于在磁盤與設備上直接進行動態查找;
如下圖二叉搜索及多路搜索拓撲結構:
每個節點中具有多個關鍵碼, 每個關鍵碼又對應著多個引用
它與二叉平衡搜索樹的區別在于,將多個節點進行合并變為關鍵碼,減少IO操作
多路搜平衡索樹
m階B-樹(m>2):
- 在每個節點中具有不超過m-1個關鍵碼, 并且外部節點(而非葉節點,包含一下為空的節點)的深度均相同;
- 每個內部節點存有不超過m-1個關鍵碼, 每個關鍵碼所對應的分支n不超過m個引用
- 內部節點的分支數不能太少,除根節點以外, 所有內部節點都應滿足n+1>=m/2(m/2的值為向上取整), 在非空的B樹中,根節點滿足n>=1
- 各節點所對應的分支數介于m/2到m之間
如:(2,4) (3,5) (4,7) (4,8)樹
圖中空白方格則是存儲指向下一層的引用(即孩子向量), 存儲數字的方格則是相應的關鍵碼向量
B樹接口
//需要用到向量vector template<typename T>class BTNode{public:BTNode<T>* parent;//父節點vector<T> key;//用于存放關鍵碼向量vector<BTNode<T>*> child;//存放孩子向量,用于保存下一層節點地址的指針BTNode(){parent=null; child.insert(0,null);}BTNode(T e,BTNode<T>* lc=null,BTNode<T>* rc=null){parent=null;key.insert(0,e);//B樹初始化,只存有一個_root,key中保存一個關鍵碼,倆孩子child.insert(0,lc);child.insert(1,rc);if(lc) lc-parent=this;if(rc) rc->parent=this;//孩子向量的大小總是比關鍵碼向量大小多一個}//~BTNode(){key.clear();child.clear();},析構函數待會處理 }; template<typename T>class BTREE{protected:int _size;//關鍵碼總數int _order;//B樹階次BTNode<T>* _root,_hot;//根節點,以及進行search訪問后所指向的節點位置void solveOverflow(BTNode<T>*);//節點發生上溢進行分裂void solveUnderflow(BTNode<T>*);//節點發生下溢進行合并public:BTREE(int order=3):_order(order),size(0);{_root=new BTNode<T>();}//構造函數,默認階數為3// ~BTREE(){if(_root)}int const order(){return _order;}int const size(){return _size;}BTNode<T>* & root(){return _root;}bool empty()const{return !_root;}//判斷樹根是否為空bool insert(const T& e);bool remove(const T& e);BTNode<T>* search(); };search實現
//對比上圖,思考查找方式 //查找方式與常規的二叉平衡搜索樹一致,需要指出的一點是,在每個關鍵碼中進行查找的時候若關鍵碼較少的時候就可直接使用順序查找策略,而非二分查找 template<typename T>BTNode<T>* BTREE<T>::search(const T& e){BTNode<T>* v=_root;_hot=null;//從根節點查找while(v){for(int i=0;i<key.size();i++){//在當前的關鍵碼向量中順序查找是否有滿足條件的節點if(e==v.key[i])return v;else if(e<v.key[i]){//由于關鍵碼在向量中是順序存放,當所要查找的關鍵碼小于key[i],i所對應的孩子引用位置就是查找的關鍵碼下一層所在的節點位置_hot=v;//記錄查找失敗的節點位置v=v->child[i];}}}return null;//遍歷完所有節點還未找到 } //查找過程中總共需要訪問O(logn)個節點,盡管查找過程中沒有漸進上的意義,但是極大地減少了I/O操作次數,提高了訪問效率關鍵碼插入
template<typename T>bool BTREE<T>::insert(const T& e){BTNode<T>* v=search(e); if(v) return false;//確認目標節點不存在int r=_hot->key.find(e);//find()在此處簡寫,需要重寫find操作,查找失敗返回失敗的位置(這個位置是e剛好小于該關鍵碼的位置)//在查找失敗的_hot位置處的向量內定位具體e的位置,然后進行插入操作_hot->key.insert(r,e);_hot->child.insert(r+1,null);//孩子向量總是比key向量大1,所以在r+1的位置插入一個空子樹指針_size++;solveOverflow(_hot);return true;//插入成功 }由于B樹在節點插入,或者節點刪除時會出現當前key向量, child向量不滿足B樹的定義要求, 當關鍵碼向量出現超出當前階數所規定的最大值時,則需要進行分裂處理, 反之小于最小值則需要進行合并處理
上溢
當剛發生上溢的時候, key中恰好有m個關鍵碼, 以m/2為分界,前后兩部分為等長的的子節點, 令m/2升入上一層, 歸入父節點中合適的位置, 將子節點歸入m/2的左右孩子, 如此分裂后, 就可滿足m階B-樹關于節點分支數的條件
例: 以37為界進行分裂上溢操作
如果將上溢的節點添加到父節點一層又導致父節點一層發生上溢操作稱之為上溢的向上傳遞; 但是這種傳遞并不是沒有盡頭, 最遠到達樹根位置, 上溢操作修復之后全樹的高度將上升一層(但這種概率特別低), 整個過程中所做的分裂次數必然不會超過全樹的高度,即O(logn)
根據上溢過程的處理方式, 可實現如下代碼:
上述過程中必須要清楚v, v->child, v->parent, u, u->child, u-parent, p, p->child 之間的含義以及聯系:
- v是要發生上溢的節點, v->child中存儲著每個關鍵碼的左右孩子,v->parent則是整個v節點的父親
- u中存儲著v前半段的關鍵碼以及child, 由于u是重新申請的空間需要與p重連
- p中存儲v->parent的信息以及v中上溢m/2關鍵碼的信息,需要與v-parent, v, u, 重連
參考如下圖:
關鍵碼刪除
template<typename T>bool BTREE<T>::remove(const T& e){BTNode<T>* v=search(e); if(!v) return false;//關鍵碼未找到int r=v->key.find(e);//查找關鍵碼在節點v中的位置if(v->child[0]){//判斷是否為葉子節點,若為葉子節點則不需要將r處的v->child轉移到u中BTNode<T>* u=v->child[r+1];while(u->child[0]) u=u->child[0];v->key[r]=u->key[0];v=u;r=0;//將v->child[r+1]移動至u中,完成重連,然后一路向下,找到v的直接后繼,將u->key[0]把v->key[r]覆蓋,使v與v->child[r+1]互換位置,v->child[r+1]代替v成為父節點 }v.key.remove(r);v->child.remove(r+1); _size--;//刪除vsolveUnderflow(v);//有必要進行合并return true; }下溢
通過上述操作,剛好發生下溢的節點v必須恰好包含m/2-2關鍵碼和m/2-1個分支, 可根據左右兄弟所包含關鍵碼數目,分3中情況處理
1.v的左兄弟存在,且包含至少m/2個關鍵碼, 右兄弟含有m/2-2個關鍵碼:v的右兄弟向父節點p借一個關鍵碼,使之關鍵碼數為m/2-1, 然后父節點向v的左兄弟借一個關鍵碼用于維持原有狀態, 至此局部滿足B-樹的條件,整個過程可以看做是關鍵碼之間的旋轉
2. 該情況與1中的情況相反, 右兄弟存在,且包含至少m/2個關鍵碼, 左兄弟含有m/2-2個關鍵碼, 解決方法與1一致
3. v的左右兄弟存在或者不存在, 或者包含的關鍵碼均不足m/2個, 解決方法可采用向父節點借一個關鍵碼, 然后將v的左右兄弟以及借來的關鍵碼合在一起, 此時關鍵碼總數: (m/2-1)+1+(m/2-2)=m-2<=m-1 注:m/2做向上取整處理
上述方法完成對下溢的修復,但是有可能還會出現下溢的傳遞(與上溢傳遞情況一致); 當傳遞到樹根的位置時,整棵樹的高度將下降一層, 整個過程中至多進行O(logn)次合并操作
通過上述, 有如下代碼
上述過程如下圖:
刪除操作的復雜度依然可以保證在O(logn)內,單次操作平均只需做常數次節點的合并
總結
- 上一篇: 复习Collection_迭代器使用细节
- 下一篇: rabbitmq如何保证消息不被重复消费