本篇博文旨在介紹一種適合外尋找的樹---B樹,以及B樹的延伸B+樹;比較了B樹和B+樹的各自優缺點;說明了B樹B+樹的應用情境;以及實現了B樹的代碼
B樹 B樹的概念和性質
B樹是一種適合外查詢的樹,它是一種多叉平衡樹
除此之外,它還滿足如下的性質:
(1)根節點有至少兩個孩子
(2)非根節點的孩子數量在【M/2 , M】之間
(3)每個節點的key值的數量在【(M/2) - 1,M-1】之間,關鍵字並按照升序排序
(4)所有的葉子節點均在同一層
(5)key[i],key[i+1],這兩個關索引值之間的孩子的值在key[i]到key[i+1]之間
一顆M=3的B樹
B樹節點的定義
//定義B樹的節點template<typename K,typename V,size_t M = 3>struct BTreeNode{pair<K,V> _kv[M];BTreeNode<K, V> *_subs[M+1];BTreeNode<K, V> *_parent;//表示儲存了多少個Key值size_t _size;//節點的建構函式BTreeNode():_size(0), _parent(NULL){for (size_t i = 0; i <= M; ++i){_subs[i] = NULL;}}}; 我們按照B樹的概念及其性質定義了如上的節點類
B樹如何尋找一個key值
//尋找函數,其傳回值的設計實現了Insert的複用pair<Node*,int> Find(const K& key){//若查詢不到,則用parent記錄需要插入的節點的父節點//實現了Insert的複用Node* cur = _root;Node* parent = NULL;while (cur){size_t index = 0;while (index < cur->_size){if (key > cur->_kv[index].first){index++;}else if (key == cur->_kv[index].first){//查詢到,返回查詢的節點return make_pair(cur, index);}else{break;}}parent = cur;cur = cur->_subs[index];}//沒有查詢到,返回需要插入節點的父親節點//實現了Insert函數的複用return make_pair(parent, -1);} 這裡和搜尋樹的方式相同,只是由於定義的不同而實現的略有區別而已,概念都是一樣的
這裡使用了pair,pair是一個結構體類型,傳回值返回的是節點的指標以及尋找成功與否的bool B樹的插入方法
插入的時候可能遇到以下情況: (1)B樹為空白
創造新節點,並給根賦值 (2)插入的時候,B樹已經存在了該Key值
根據find傳回值pair結構體的第二個參數,可以判斷原B樹是否存在對應的Key值
若存在,則插入失敗;
否則再插入 (3)插入一個節點後,該節點未滿
插入完成,直接返回TRUE (4)插入一個節點後,該節點的關索引值滿了,需要分裂
分裂方法:
將該節點的右半區間移到一個新的節點中(移動時也要把孩子移過去)
產生節點後,還要注意之前有沒有父親節點;
若沒有,就需要將中間的元素提到新的根節點; (5)分裂時,若分裂的節點有父親節點
這種情況可能繼續調整,因為之前分裂會將一個key值給父親節點
父親節點的key值數量+1,也就有可能滿了
上面的情況是分裂節點為空白的情況,不用繼續調整,就可結束
代碼中有對所有情況的分析和注釋
bool Insert(const pair<K, V>& kv){//情況1.當根節點為空白的時候,需要對_root進行賦值if (_root == NULL){_root = new Node;_root->_size = 1;_root->_kv[0] = kv;return true;}//情況2.不為空白,則找到插入的位置//如果second的值不為-1,則表示已經存在,不需要插入pair<Node*, int> ret = Find(kv.first); if (ret.second != -1)return false;//進行插入Node* cur = ret.first;Node* temp = cur;pair<K, V> newKV = kv;Node* sub = NULL;while (1){//在cur節點插入kv和sub_Insert(cur, newKV,sub);//情況3.如果cur沒滿,則表示插入成功//否則需要進行分裂if (cur->_size < M)return true;//情況4.構造一個新節點tmpNode* tmp = new Node;size_t mid = M >> 1;size_t i = mid+1;size_t j = 0;//將右半區間的元素移到tmp中while (i < cur->_size){tmp->_kv[j] = cur->_kv[i];cur->_kv[i] = pair<K, V>();tmp->_subs[j] = cur->_subs[i];if (cur->_subs[i])cur->_subs[i]->_parent = tmp;//調整i++;j++;tmp->_size++;cur->_size--;}//拷走最後一個右孩子tmp->_subs[j] = cur->_subs[i];cur->_subs[i] = NULL;if (cur->_subs[i])cur->_subs[i]->_parent = tmp;//情況5.如果cur沒有父親節點,需要建立//如果cur有父親節點,那麼需要將中間元素插入到父親節點裡if (cur->_parent){//繼續向上調整,判別是否parent會滿newKV = cur->_kv[mid];sub = tmp;cur->_size--;cur = cur->_parent;}else{//定義一個新根節點,將中間元素放入該節點中Node* newRoot = new Node;newRoot->_kv[0] = cur->_kv[mid];newRoot->_size = 1;//設定新根節點的兩個孩子cur和tmp//並修改他們的父親節點newRoot->_subs[0] = cur;cur->_parent = newRoot;newRoot->_subs[1] = tmp;tmp->_parent = newRoot;cur->_size--;//將根節點重新賦值_root = newRoot;return true;}}}
B樹的代碼實現
#pragma once#include<iostream>using namespace std;/** author:haohaosong* date:2017/2/23* note:B樹的實現 *///B樹的增刪查改的時間複雜度 O(logM(N))//定義B樹的節點template<typename K,typename V,size_t M = 3>struct BTreeNode{pair<K,V> _kv[M];BTreeNode<K, V> *_subs[M+1];BTreeNode<K, V> *_parent;//表示儲存了多少個Key值size_t _size;//節點的建構函式BTreeNode():_size(0), _parent(NULL){for (size_t i = 0; i <= M; ++i){_subs[i] = NULL;}}};//B樹的定義template<typename K, typename V,size_t M = 3>class BTree{typedef BTreeNode<K, V> Node;public://建構函式BTree():_root(NULL){}//尋找函數,其傳回值的設計實現了Insert的複用pair<Node*,int> Find(const K& key){//若查詢不到,則用parent記錄需要插入的節點的父節點//實現了Insert的複用Node* cur = _root;Node* parent = NULL;while (cur){size_t index = 0;while (index < cur->_size){if (key > cur->_kv[index].first){index++;}else if (key == cur->_kv[index].first){//查詢到,返回查詢的節點return make_pair(cur, index);}else{break;}}parent = cur;cur = cur->_subs[index];}//沒有查詢到,返回需要插入節點的父親節點//實現了Insert函數的複用return make_pair(parent, -1);}bool Insert(const pair<K, V>& kv){//當根節點為空白的時候,需要對_root進行賦值if (_root == NULL){_root = new Node;_root->_size = 1;_root->_kv[0] = kv;return true;}//不為空白,則找到插入的位置//如果second的值不為-1,則表示已經存在,不需要插入pair<Node*, int> ret = Find(kv.first); if (ret.second != -1)return false;//進行插入Node* cur = ret.first;Node* temp = cur;pair<K, V> newKV = kv;Node* sub = NULL;while (1){//在cur節點插入kv和sub_Insert(cur, newKV,sub);//如果cur沒滿,則表示插入成功//否則需要進行分裂if (cur->_size < M)return true;//構造一個新節點tmpNode* tmp = new Node;size_t mid = M >> 1;size_t i = mid+1;size_t j = 0;//將右半區間的元素移到tmp中while (i < cur->_size){tmp->_kv[j] = cur->_kv[i];cur->_kv[i] = pair<K, V>();tmp->_subs[j] = cur->_subs[i];if (cur->_subs[i])cur->_subs[i]->_parent = tmp;//調整i++;j++;tmp->_size++;cur->_size--;}//拷走最後一個右孩子tmp->_subs[j] = cur->_subs[i];cur->_subs[i] = NULL;if (cur->_subs[i])cur->_subs[i]->_parent = tmp;//如果cur沒有父親節點,需要建立//如果cur有父親節點,那麼需要將中間元素插入到父親節點裡if (cur->_parent){//繼續向上調整,判別是否parent會滿newKV = cur->_kv[mid];sub = tmp;cur->_size--;cur = cur->_parent;}else{//定義一個新根節點,將中間元素放入該節點中Node* newRoot = new Node;newRoot->_kv[0] = cur->_kv[mid];newRoot->_size = 1;//設定新根節點的兩個孩子cur和tmp//並修改他們的父親節點newRoot->_subs[0] = cur;cur->_parent = newRoot;newRoot->_subs[1] = tmp;tmp->_parent = newRoot;cur->_size--;//將根節點重新賦值_root = newRoot;return true;}}}//中序遍曆void InOrder(){_InOrder(_root);cout << endl;}protected:void _Insert(Node* cur,const pair<K,V>& newKV,Node* sub){//這裡要用int,不能用size_t,會影響while迴圈的判斷條件int index = (cur->_size)-1;while (index >= 0){//找到合適的插入位置,跳出if (newKV.first > cur->_kv[index].first)break;//將key值向後移動,並且將孩子也移動cur->_kv[index + 1] = cur->_kv[index];cur->_subs[index + 2] = cur->_subs[index + 1];index--;}//插入新的KV以及孩子cur->_kv[index+1] = newKV;cur->_subs[index+2] = sub;//孩子存在,給其父親賦值if (sub)sub->_parent = cur;cur->_size++;}//中序遍曆遞迴函式void _InOrder(Node* root){if (root == NULL)return;//訪問每一個key值及其左孩子size_t index = 0;while (index < root->_size){_InOrder(root->_subs[index]);cout << root->_kv[index].first<<" ";index++;}//列印最後一個key值的右孩子_InOrder(root->_subs[root->_size]);}protected:Node* _root;};void TestBtree(){BTree<int, int> t;int a[] = { 53, 75, 139, 49, 145, 36, 101 };for (size_t i = 0; i < sizeof(a) / sizeof(a[0]); ++i){t.Insert(make_pair(a[i], i));t.InOrder();}t.InOrder();}
B+樹 B樹的特點
B+樹是B樹的一種變形樹,它的基本特性和B樹一致
只是有下列區別:
(1)每個節點的key值和子節點的個數對應
(2)非葉子節點只是起到索引的作用,只有葉子節點內部儲存資料
(3)所有的葉子節點由一個鏈表串起來,並且是有序的,可以根據關索引值的次序遍曆全部記錄
B樹和B+樹的對比 B樹的優點:
和B+樹相比,每一個節點都有key和對應的value,如果訪問的元素離根節點較近,則訪問很迅速 B+樹的優點:
(1)葉子節點是用鏈表串起來的,遍曆一遍的速度很快;相反如果採用B樹,則需要遍曆整顆樹
(2)B+樹的資料是順序排列的,並且是相連的所以在區間內尋找時(比如尋找成績在70~80分之間的學生),B+樹很方便就可以辦到
而如果採用B樹,B樹要對每一層遞迴的進行遍曆,效率低;
(3)空間局部性高,快取命中率高 B樹和B+樹的主要應用:
它們主要運用在檔案儲存體系統以及資料庫系統中
一些主流的資料庫,如MySQL,Oracle對B數,B+樹都有運用
當然是進行最佳化過後的