B树
2015-03-29 21:50
148 查看
概念:B树就是一种数据结构,是为磁盘或者其它直接存取的辅助存储设备而设计的一种平衡查找树。许多数据库系统都是采用B树或者B树的变形来存储信息。
磁盘的结构:定位一个磁盘中的数据,一般由柱面(磁道组成的圆柱)、盘面和盘块号三部分组成,通常cpu都是以盘块大小为基本单位进行读取的,盘块的大小一般为几KB.故而在磁盘中读取数据所消耗的时间主要就由查找磁道(查找时间)和等待时间(盘块号旋转到磁头下)的时间组成,通常前者较好时间,不管怎样,读取磁盘比读取内存中的数据耗时间多了,因此我们是否需要为在查找磁盘上数据的时候是否应该选择某种数据结构来存储数据,以便降低磁盘IO的操作次数?
此外,在一般的数据库系统如mysql:以某id主键存储信息,如果不用数据结构存储这些信息,那么查找的时候只能线性扫描,将是非常耗时的,而一般情况我们都是用B树或者它的变形B+树、B*树来存储信息。此外如以关键字建立的文件索引:比如以谱哈希得到的汉明码作为一张图片的索引建立的文件索引,此时便可以汉明码作为关键字建立一棵B树,那么查找时将是非常省时的。
通常B树的一个结点为一个磁盘块的大小,因此在B树上查找数据,读取IO磁盘的次数即为B树的高度,虽然B树是一种平衡树,但我们在后面讲B树的高度时,会发现它的实际高度要比红黑树低很多。
1:B树定义
B树一般有两种定义,分别是按阶和度来定义
(1)一个m阶的B树需要满足以下性质:
<1>每个结点至多含有m个子女
<2>除了根的内结点至少含有ceil(m/2)个子女,亦即除了根的结点至少含有ceil(m/2)-1个关键字
<3>如果根结点不是叶结点,那么跟结点至少含有2个子女。
<4>叶子结点都在同一层,且叶节点至多含有ceil(m/2)-1~m-1个关键字,关键字按照升序排列的,他们都没有孩子或者说他们的孩子都为NULL
<5>除叶子结点的每个结点具有n个关键字(n,p0,k0,p1,k1,…kn-1,pn)
其中:(a)关键字k0,k1,k2,k3…kn-1是按照升序排列的。
(b)pi为指向子树根的结点,且pi所指向子树中所有结点的关键字均小于ki,且大于ki-1
(c)关键字n必须满足:ceil(m/2)-1
<n< m-1
(2)按照度来定义,一个B树的度为t,那么
<1>除了根结点每个结点至少含有t-1个关键字
<2>所有的结点都至多含有2t-1个关键字
t=2的B树是最简单的,这时每个内结点有2、3或者4个子女,亦即一棵2-3-4树。
其实这里的t
就等于(m+1)/2
2:B树的数据结构
#define N 5 // 最多包含t个关键字
#define t (N+1)/2 // B 树的度
struct BNode{
int count; // 关键字的个数
bool leaf; // 是否为叶子结点
BNode* pChild[N+1]; // 孩子结点指针
char key
; // 关键字 当然这里关键所对应的附属数据没有列出来
BNode():count(0), leaf(true){ // 初始化
memset(key, 0, sizeof(char)*N);
memset(pChild, 0, sizeof(BNode*)*(N+1));
}
};
这里的附属数据指的是比如在数据库系统中存放的是一个学生表,那么关键字可能是其学号,而附属数据就是该学号所指向的学生的姓名年纪等详细信息。
3:B树的高度
具有n个关键字的m阶B树的高度即为logceil(m/2)((n+1)/2)。以度t来衡量的话就是logt(n+1)/2.我们知道红黑树也是平衡树,有多少个关键字就有多少个结点,具有n个内部结点的红黑树的高度为2lg(n+1)。都是n的对数,那么为什么B树的高度会低很多呢?这里是因为B树是以关键字个数作为底的。通常为50到2000,因此采用B树能大大降低磁盘IO的访问次数。
4:B树的基本操作
这里主要讲解了B树的创建,查找,插入,遍历输出,没有讲解删除,这个复杂点,以后有时间再补。
查找:
bool BTree::searchBtree(int value, BNode *p){
int i = 0;
while(i < p->count && value > p->key[i])
i++;
if(value == p->key[i]) return true;
if(!p->leaf)
return searchBtree(value, p->pChild[i]);
return false;
}
查找一个B树最坏的情况下需要磁盘访问h次,即为磁盘的高度,因此所需要的最坏情况下的时间复杂度就为O(th).
t为度,h为高度。
创建和插入
插入的过程,这里就不讲解了,想知道的可以看算法导论,这里给出实现的代码
void BTree::createBtree(char *cArray, int n){
for(int i = 0; i < n; i++){
insertBtree(root, cArray[i]);
}
}
遍历输出
// 中序输出B 树
void BTree::inOrder(BNode *p){
if(!p->leaf){
for(int i = 0; i < p->count; i++){
inOrder(p->pChild[i]);
cout << p->key[i] << " ";
}
inOrder(p->pChild[p->count]);
}else{
for(int i = 0; i < p->count; i++){
cout << p->key[i] << " ";
}
}
}
完整代码:
BTree.h
#include <string.h>
#include <iostream>
using namespace std;
#define N 5 // 最多包含t个关键字
#define t (N+1)/2 // B 树的度
struct BNode{
int count; // 关键字的个数
bool leaf; // 是否为叶子结点
BNode* pChild[N+1]; // 孩子结点指针
char key
; // 关键字 当然这里关键所对应的附属数据没有列出来
BNode():count(0), leaf(true){ // 初始化
memset(key, 0, sizeof(char)*N);
memset(pChild, 0, sizeof(BNode*)*(N+1));
}
};
class BTree{
public:
BTree(){
root = new BNode();
}
void Btree_split_child(BNode *x, int i, BNode *y); // 对满的结点进行分裂操作
void Btree_insert_nonfull(BNode *p, char value); // 插入的结点没有满
void insertBtree(BNode *p, char value);
void inOrder(BNode *p); // 输出B树
void createBtree(char *cArray, int n);
~BTree(){destroy(root);}
BNode * root;
bool searchBtree(int value, BNode *p);
private:
void destroy(BNode *p);
};
BTree.cpp
#include "BTree.h"
using namespace std;
// 对满的结点y进行分裂 void BTree::Btree_split_child(BNode *x, int i, BNode *y){ // 操作z BNode *z = new BNode(); z->leaf = y->leaf; z->count = t-1; for(int j = 0; j < t-1; j++) z->key[j] = y->key[t+j]; if(!y->leaf){ for(int j = 0; j < t-1; j++){ z->pChild[j] = y->pChild[j+t]; } } // 操作y y->count = t-1; // 操作 x for(int j = x->count; j >= i+1; j--){ x->pChild[j+1] = x->pChild[j]; } x->pChild[i+1] = z; for(int j = x->count-1; j>=i; j--) { x->key[j+1] = x->key[j]; } x->key[i] = y->key[t-1]; x->count = x->count + 1; } // 将value插入未满的结点 void BTree::Btree_insert_nonfull(BNode *p, char value){ int i = p->count-1; if(p->leaf){ while(i >= 0){ if(p->key[i] > value){ p->key[i+1] = p->key[i]; i--; } else break; } p->key[i+1] = value; p->count = p->count + 1; }else{ while(i >= 0 && p->key[i] > value){ i--; } i = i+1; if(p->pChild[i]->count == N){ Btree_split_child(p, i, p->pChild[i]); if (value > p->key[i]) i++; } Btree_insert_nonfull(p->pChild[i], value); } } void BTree::insertBtree(BNode *p, char value){ if(p->count < N ){ // 结点个数少于N并且为叶节点 Btree_insert_nonfull(p, value); } else{ // 多于5个 要分裂 BNode *s = new BNode(); s->leaf = false; s->pChild[0] = p; root = s; Btree_split_child(s, 0, p); Btree_insert_nonfull(s, value); } }
// 中序输出B 树
void BTree::inOrder(BNode *p){
if(!p->leaf){
for(int i = 0; i < p->count; i++){
inOrder(p->pChild[i]);
cout << p->key[i] << " ";
}
inOrder(p->pChild[p->count]);
}else{
for(int i = 0; i < p->count; i++){
cout << p->key[i] << " ";
}
}
}
void BTree::destroy(BNode *p){
if(!p->leaf){
for(int i = 0; i <= p->count; i++){
destroy(p->pChild[i]);
}
delete p;
}else
delete p;
}
void BTree::createBtree(char *cArray, int n){
for(int i = 0; i < n; i++){
insertBtree(root, cArray[i]);
}
}
bool BTree::searchBtree(int value, BNode *p){
int i = 0;
while(i < p->count && value > p->key[i])
i++;
if(value == p->key[i]) return true;
if(!p->leaf)
return searchBtree(value, p->pChild[i]);
return false;
}
Main.cpp
#include <string.h>
#include "BTree.h"
#include <iostream>
using namespace std;
int main(){
char cArray[] = {"CNGAHEKQMFWLTZDPRXYS"};
BTree btree;
btree.createBtree(cArray, strlen(cArray));
btree.inOrder(btree.root);
cout << endl;
//btree.insertBtree(btree.root, 'I');
//btree.inOrder(btree.root);
//cout << endl;
cout << btree.searchBtree('S', btree.root) << endl;
return 0;
}
5:B树的变形-B+树
B+树其实就是将B树每个结点中的附属数据全部存入到了叶子结点中,而内部结点只含有关键字和子女的指针(结点大小变小了),这样每个结点所能存储的关键字的个数就变多了。自然高度就变低了,磁盘IO的访问次数也就变少了。
磁盘的结构:定位一个磁盘中的数据,一般由柱面(磁道组成的圆柱)、盘面和盘块号三部分组成,通常cpu都是以盘块大小为基本单位进行读取的,盘块的大小一般为几KB.故而在磁盘中读取数据所消耗的时间主要就由查找磁道(查找时间)和等待时间(盘块号旋转到磁头下)的时间组成,通常前者较好时间,不管怎样,读取磁盘比读取内存中的数据耗时间多了,因此我们是否需要为在查找磁盘上数据的时候是否应该选择某种数据结构来存储数据,以便降低磁盘IO的操作次数?
此外,在一般的数据库系统如mysql:以某id主键存储信息,如果不用数据结构存储这些信息,那么查找的时候只能线性扫描,将是非常耗时的,而一般情况我们都是用B树或者它的变形B+树、B*树来存储信息。此外如以关键字建立的文件索引:比如以谱哈希得到的汉明码作为一张图片的索引建立的文件索引,此时便可以汉明码作为关键字建立一棵B树,那么查找时将是非常省时的。
通常B树的一个结点为一个磁盘块的大小,因此在B树上查找数据,读取IO磁盘的次数即为B树的高度,虽然B树是一种平衡树,但我们在后面讲B树的高度时,会发现它的实际高度要比红黑树低很多。
1:B树定义
B树一般有两种定义,分别是按阶和度来定义
(1)一个m阶的B树需要满足以下性质:
<1>每个结点至多含有m个子女
<2>除了根的内结点至少含有ceil(m/2)个子女,亦即除了根的结点至少含有ceil(m/2)-1个关键字
<3>如果根结点不是叶结点,那么跟结点至少含有2个子女。
<4>叶子结点都在同一层,且叶节点至多含有ceil(m/2)-1~m-1个关键字,关键字按照升序排列的,他们都没有孩子或者说他们的孩子都为NULL
<5>除叶子结点的每个结点具有n个关键字(n,p0,k0,p1,k1,…kn-1,pn)
其中:(a)关键字k0,k1,k2,k3…kn-1是按照升序排列的。
(b)pi为指向子树根的结点,且pi所指向子树中所有结点的关键字均小于ki,且大于ki-1
(c)关键字n必须满足:ceil(m/2)-1
<n< m-1
(2)按照度来定义,一个B树的度为t,那么
<1>除了根结点每个结点至少含有t-1个关键字
<2>所有的结点都至多含有2t-1个关键字
t=2的B树是最简单的,这时每个内结点有2、3或者4个子女,亦即一棵2-3-4树。
其实这里的t
就等于(m+1)/2
2:B树的数据结构
#define N 5 // 最多包含t个关键字
#define t (N+1)/2 // B 树的度
struct BNode{
int count; // 关键字的个数
bool leaf; // 是否为叶子结点
BNode* pChild[N+1]; // 孩子结点指针
char key
; // 关键字 当然这里关键所对应的附属数据没有列出来
BNode():count(0), leaf(true){ // 初始化
memset(key, 0, sizeof(char)*N);
memset(pChild, 0, sizeof(BNode*)*(N+1));
}
};
这里的附属数据指的是比如在数据库系统中存放的是一个学生表,那么关键字可能是其学号,而附属数据就是该学号所指向的学生的姓名年纪等详细信息。
3:B树的高度
具有n个关键字的m阶B树的高度即为logceil(m/2)((n+1)/2)。以度t来衡量的话就是logt(n+1)/2.我们知道红黑树也是平衡树,有多少个关键字就有多少个结点,具有n个内部结点的红黑树的高度为2lg(n+1)。都是n的对数,那么为什么B树的高度会低很多呢?这里是因为B树是以关键字个数作为底的。通常为50到2000,因此采用B树能大大降低磁盘IO的访问次数。
4:B树的基本操作
这里主要讲解了B树的创建,查找,插入,遍历输出,没有讲解删除,这个复杂点,以后有时间再补。
查找:
bool BTree::searchBtree(int value, BNode *p){
int i = 0;
while(i < p->count && value > p->key[i])
i++;
if(value == p->key[i]) return true;
if(!p->leaf)
return searchBtree(value, p->pChild[i]);
return false;
}
查找一个B树最坏的情况下需要磁盘访问h次,即为磁盘的高度,因此所需要的最坏情况下的时间复杂度就为O(th).
t为度,h为高度。
创建和插入
插入的过程,这里就不讲解了,想知道的可以看算法导论,这里给出实现的代码
void BTree::createBtree(char *cArray, int n){
for(int i = 0; i < n; i++){
insertBtree(root, cArray[i]);
}
}
// 对满的结点y进行分裂 void BTree::Btree_split_child(BNode *x, int i, BNode *y){ // 操作z BNode *z = new BNode(); z->leaf = y->leaf; z->count = t-1; for(int j = 0; j < t-1; j++) z->key[j] = y->key[t+j]; if(!y->leaf){ for(int j = 0; j < t-1; j++){ z->pChild[j] = y->pChild[j+t]; } } // 操作y y->count = t-1; // 操作 x for(int j = x->count; j >= i+1; j--){ x->pChild[j+1] = x->pChild[j]; } x->pChild[i+1] = z; for(int j = x->count-1; j>=i; j--) { x->key[j+1] = x->key[j]; } x->key[i] = y->key[t-1]; x->count = x->count + 1; } // 将value插入未满的结点 void BTree::Btree_insert_nonfull(BNode *p, char value){ int i = p->count-1; if(p->leaf){ while(i >= 0){ if(p->key[i] > value){ p->key[i+1] = p->key[i]; i--; } else break; } p->key[i+1] = value; p->count = p->count + 1; }else{ while(i >= 0 && p->key[i] > value){ i--; } i = i+1; if(p->pChild[i]->count == N){ Btree_split_child(p, i, p->pChild[i]); if (value > p->key[i]) i++; } Btree_insert_nonfull(p->pChild[i], value); } } void BTree::insertBtree(BNode *p, char value){ if(p->count < N ){ // 结点个数少于N并且为叶节点 Btree_insert_nonfull(p, value); } else{ // 多于5个 要分裂 BNode *s = new BNode(); s->leaf = false; s->pChild[0] = p; root = s; Btree_split_child(s, 0, p); Btree_insert_nonfull(s, value); } }
遍历输出
// 中序输出B 树
void BTree::inOrder(BNode *p){
if(!p->leaf){
for(int i = 0; i < p->count; i++){
inOrder(p->pChild[i]);
cout << p->key[i] << " ";
}
inOrder(p->pChild[p->count]);
}else{
for(int i = 0; i < p->count; i++){
cout << p->key[i] << " ";
}
}
}
完整代码:
BTree.h
#include <string.h>
#include <iostream>
using namespace std;
#define N 5 // 最多包含t个关键字
#define t (N+1)/2 // B 树的度
struct BNode{
int count; // 关键字的个数
bool leaf; // 是否为叶子结点
BNode* pChild[N+1]; // 孩子结点指针
char key
; // 关键字 当然这里关键所对应的附属数据没有列出来
BNode():count(0), leaf(true){ // 初始化
memset(key, 0, sizeof(char)*N);
memset(pChild, 0, sizeof(BNode*)*(N+1));
}
};
class BTree{
public:
BTree(){
root = new BNode();
}
void Btree_split_child(BNode *x, int i, BNode *y); // 对满的结点进行分裂操作
void Btree_insert_nonfull(BNode *p, char value); // 插入的结点没有满
void insertBtree(BNode *p, char value);
void inOrder(BNode *p); // 输出B树
void createBtree(char *cArray, int n);
~BTree(){destroy(root);}
BNode * root;
bool searchBtree(int value, BNode *p);
private:
void destroy(BNode *p);
};
BTree.cpp
#include "BTree.h"
using namespace std;
// 对满的结点y进行分裂 void BTree::Btree_split_child(BNode *x, int i, BNode *y){ // 操作z BNode *z = new BNode(); z->leaf = y->leaf; z->count = t-1; for(int j = 0; j < t-1; j++) z->key[j] = y->key[t+j]; if(!y->leaf){ for(int j = 0; j < t-1; j++){ z->pChild[j] = y->pChild[j+t]; } } // 操作y y->count = t-1; // 操作 x for(int j = x->count; j >= i+1; j--){ x->pChild[j+1] = x->pChild[j]; } x->pChild[i+1] = z; for(int j = x->count-1; j>=i; j--) { x->key[j+1] = x->key[j]; } x->key[i] = y->key[t-1]; x->count = x->count + 1; } // 将value插入未满的结点 void BTree::Btree_insert_nonfull(BNode *p, char value){ int i = p->count-1; if(p->leaf){ while(i >= 0){ if(p->key[i] > value){ p->key[i+1] = p->key[i]; i--; } else break; } p->key[i+1] = value; p->count = p->count + 1; }else{ while(i >= 0 && p->key[i] > value){ i--; } i = i+1; if(p->pChild[i]->count == N){ Btree_split_child(p, i, p->pChild[i]); if (value > p->key[i]) i++; } Btree_insert_nonfull(p->pChild[i], value); } } void BTree::insertBtree(BNode *p, char value){ if(p->count < N ){ // 结点个数少于N并且为叶节点 Btree_insert_nonfull(p, value); } else{ // 多于5个 要分裂 BNode *s = new BNode(); s->leaf = false; s->pChild[0] = p; root = s; Btree_split_child(s, 0, p); Btree_insert_nonfull(s, value); } }
// 中序输出B 树
void BTree::inOrder(BNode *p){
if(!p->leaf){
for(int i = 0; i < p->count; i++){
inOrder(p->pChild[i]);
cout << p->key[i] << " ";
}
inOrder(p->pChild[p->count]);
}else{
for(int i = 0; i < p->count; i++){
cout << p->key[i] << " ";
}
}
}
void BTree::destroy(BNode *p){
if(!p->leaf){
for(int i = 0; i <= p->count; i++){
destroy(p->pChild[i]);
}
delete p;
}else
delete p;
}
void BTree::createBtree(char *cArray, int n){
for(int i = 0; i < n; i++){
insertBtree(root, cArray[i]);
}
}
bool BTree::searchBtree(int value, BNode *p){
int i = 0;
while(i < p->count && value > p->key[i])
i++;
if(value == p->key[i]) return true;
if(!p->leaf)
return searchBtree(value, p->pChild[i]);
return false;
}
Main.cpp
#include <string.h>
#include "BTree.h"
#include <iostream>
using namespace std;
int main(){
char cArray[] = {"CNGAHEKQMFWLTZDPRXYS"};
BTree btree;
btree.createBtree(cArray, strlen(cArray));
btree.inOrder(btree.root);
cout << endl;
//btree.insertBtree(btree.root, 'I');
//btree.inOrder(btree.root);
//cout << endl;
cout << btree.searchBtree('S', btree.root) << endl;
return 0;
}
5:B树的变形-B+树
B+树其实就是将B树每个结点中的附属数据全部存入到了叶子结点中,而内部结点只含有关键字和子女的指针(结点大小变小了),这样每个结点所能存储的关键字的个数就变多了。自然高度就变低了,磁盘IO的访问次数也就变少了。