數據結構大總結系列之B樹和R樹


數據結構大總結系列之B樹

一,B-樹

B樹是為磁盤或其他直接存儲輔助存儲設備而設計的一種平衡二叉查找樹(通常說的B樹是B-樹,在1972年由R.Bayer和E.M.McCreight提出,B+樹是B樹的一種變形),B樹與紅黑樹類似,但在降低磁盤I/O操作次數方面要更好一些,數據庫就是通常用B樹來進行存儲信息。

    B樹的結點可以有許多子女,從幾個到幾千個不等,一個B樹結點可以擁有的子女數是由磁盤頁的大小所決定,這是因為一個結點的大小通常相當於一個完整的磁盤頁。磁盤存取次數是按需要從盤中讀出或向盤中寫入的信息的頁數來度量的,所以,存取磁盤的總時間可以近似為讀或寫的頁數。因此,B樹一般都選擇大的分支因子,這樣可以大大降低樹的高度,以及尋找任意關鍵字所需的磁盤存取次數。一棵分支因子為1001, 高度為2的B樹,可以儲存超過10億個關鍵字,同時因為根節點可以持久地保留在內存中,故在這棵樹中,尋找一個關鍵字至多只需要兩次磁盤存取。

相信,從上圖你能輕易的看到,一個內結點x若含有n[x]個關鍵字,那么x將含有n[x]+1個子女。如含有2個關鍵字D H的內結點有3個子女,而含有3個關鍵字Q T X的內結點有4個子女。

B樹的性質:

1) 每個節點x的域:

a) n[x],x中的關鍵字數,若x是B樹中的內節點,則x有n[x] + 1個子女。

b) n[x]個關鍵字本身,以非降序排列,key1[x] <= key2[x] <= … <= keyn[x][x]

c) leaf[x],布爾值,如果x是葉節點,則為TRUE,若為內節點,則為FALSE

2) 每個內節點x還包含n[x] + 1個指向其子女的指針c1[x], c2[x], …, cn[x] + 1[x]

3) 如果ki為存儲在以ci[x]為根的子樹中的關鍵字,則k1 <= key1[x] <= k2 <= key2[x] <= … <= keyn[x][x] <= keyn[x] + 1

4) 每個葉節點具有相同的深度

5) B樹的最小度數t

a) 每個非根的節點必須至少有t – 1個關鍵字

b) 每個節點可包含至多2t – 1個關鍵字。

B樹的數據結構:

B樹插入關鍵字

B樹插入是指插入到一個已知的葉節點上,因為不能把關鍵字插入到一個滿的葉結點上,故引入一個操作,將一個滿的結點y(有2t – 1個關鍵字)按其中間關鍵字key[y]分裂成兩個各含t – 1個關鍵字的節點,中間關鍵字提升到y的雙親結點,如果y的雙親也是滿的,則自底向上傳播分裂。

    如同二叉查找樹,插入時,需要從根部沿着樹下降到葉子,當沿着樹往下查找新關鍵字所屬位置時,就分裂遇到的每一個滿結點,這樣就能保證,要分裂一個滿結點y時,就能確保它的雙親不是滿的。

分裂圖示:

插入結點偽代碼:

B-TREE-INSERT(T, k)作用是對B樹用單程下行遍歷方式插入關鍵字。3~9行處理根結點r為滿的情況。

B-TREE-SPLIT-CHILD(x, i, y) 第1~8行行創建一個新結點,並將y的t – 1個最大關鍵字以及相應的t個子女給它,第九行調整關鍵字計數。第10~16行將z插入為x的一個孩子,提升y的中間關鍵字到x來分裂y和z,並調整x的關鍵字計數。

B-TREE-INSERT-NONFULL(x, k) 第3~8行處理x是葉子的情況,將關鍵字k插入x;如果不是,則第9~11確定向x的哪個子結點遞歸下降。第13行檢查遞歸是否將降至一個滿子結點上,若是,14行用B-TREE-SPLIT-CHILD將該子結點分類成兩個非滿的孩子,第15~16行確定向兩個孩子中的哪一個下降是正確的。

各種情況都包含的插入圖示:(最小度數t為3)

 

3)刪除操作:

書上沒有給出偽代碼,只給出了基本思路,設關鍵字為k,x為節點

1) 若k在x中,且x是葉節點,則從x中刪除k

2) 若k在x中,且x是內節點,則

a) 若x中前於k的子節點y包含至少t個關鍵字,則找出k在以y為根的子樹中的前驅k’。遞歸地刪除k’,並在x中用k’取代k。

b) 若x中后於k的子節點z包含至少t個關鍵字,則找出k在以z為根的子樹中的后繼k’。遞歸地刪除k’,並在x中用k’取代k。

c) 否則,將k和z所有關鍵字合並進y,然后,釋放z並將k從y中遞歸刪除。

3) 若k不在x中,則確定必包含k的正確的子樹的根ci[x]。若ci[x]只有t – 1個關鍵字,則執行a或b操作。然后,對合適的子節點遞歸刪除k。

a) 若ci[x]只包含t-1個關鍵字,但它的相鄰兄弟包含至少t個關鍵字,則將x中的某一個關鍵字降至ci[x],將ci[x]的相鄰兄弟中的某一個關鍵字升至x,將該兄弟中合適的子女指針遷移到ci[x]中。

b) 若ci[x]與其所有相鄰兄弟節點都包含t-1個關鍵字,則將ci[x]與一個兄弟合並,將x的一個關鍵字移至新合並的節點。

刪除結點圖示:(最小度數為2)

完整實現代碼: 


/*

按關鍵字的順序遍歷B-樹:

(3, 11)

(7, 16)

(12, 4)

(24, 1)

(26, 13)

(30, 12)

(37, 5)

(45, 2)

(50, 6)

(53, 3)

(61, 7)

(70, 10)

(85, 14)

(90, 8)

(100, 9)

 

請輸入待查找記錄的關鍵字: 26

(26, 13)

 

5

沒找到

 

37

(37, 5)

 

*/

 

#include<iostream>

#include<cstdio>

#include<cstdlib>

#include<cmath>

using namespace std;

 

#define m 3  // B樹的階,暫設為3

//3階的B-數上所有非終點結點至多可以有兩個關鍵字

#define N 16 // 數據元素個數

#define MAX 5 // 字符串最大長度 + 1  



//記錄類型

struct Record{

    int key; // 關鍵字

    char info[MAX];

};  



//B-樹ADT

struct BTreeNode {

    int keynum; // 結點中關鍵字個數

    struct BTreeNode * parent; // 指向雙親結點

    struct Node { // 結點類型

        int key; // 關鍵字

        Record * recptr; // 記錄指針

        struct BTreeNode * ptr; // 子樹指針

    }node[m + 1]; // key, recptr的0號單元未用

}; 



typedef BTreeNode BT;

typedef BTreeNode * Position;

typedef BTreeNode * SearchTree;

 

//B-樹查找結果的類型

typedef struct {

    Position pt; // 指向找到的結點

    int i; // 1..m,在結點中的關鍵字序號

    int tag; // 1:查找成功,O:查找失敗

}Result; 



inline void print(BT c, int i) {// TraverseSearchTree()調用的函數

    printf("(%d, %s)\n", c.node[i].key, c.node[i].recptr->info);

}

 

//銷毀查找樹

void DestroySearchTree(SearchTree tree) {

    if(tree) {// 非空樹

        for(int i = 0; i <= (tree)->keynum; i++ ) {

            DestroySearchTree(tree->node[i].ptr); // 依次銷毀第i棵子樹

        }

        free(tree); // 釋放根結點

        tree = NULL; // 空指針賦0

    }

}

 

//在p->node[1..keynum].key中查找i, 使得p->node[i].key≤K<p->node[i + 1].key

//返回剛好小於等於K的位置

int Search(Position p, int K) {

    int location = 0;

    for(int i = 1; i <= p->keynum; i++ ) {

        if(p->node[i].key <= K) {

            location = i;

        }

    }

    return location;

}

 

/*

在m階B樹tree上查找關鍵字K,返回結果(pt, i, tag)。

若查找成功,tag = 1,指針pt所指結點中第i個關鍵字等於K;

若查找失敗,tag = 0,等於K的關鍵字應插入在指針Pt所指結點中第i和第i + 1個關鍵字之間。

*/

Result SearchPosition(SearchTree tree, int K) {

    Position p = tree, q = NULL; // 初始化,p指向待查結點,q指向p的雙親

    bool found = false;

    int i = 0;

    Result r;

    while(p && !found) {

        i = Search(p, K); // p->node[i].key≤K<p->node[i + 1].key

        if(i > 0 && p->node[i].key == K) {// 找到待查關鍵字

            found = true;

        } else {

            q = p;

            p = p->node[i].ptr;

        }

    }

    r.i = i;

    if(found) {// 查找成功

        r.pt = p;

        r.tag = 1;

    } else {//  查找不成功,返回K的插入位置信息

        r.pt = q;

        r.tag = 0;

    }

    return r;

}

 

//將r->key、r和ap分別插入到q->key[i + 1]、q->recptr[i + 1]和q->ptr[i + 1]中

void Insert(Position q, int i, Record * r, Position ap) {

    for(int j = q->keynum; j > i; j--) {// 空出q->node[i + 1]

        q->node[j + 1] = q->node[j];

    }

    q->node[i + 1].key = r->key;

    q->node[i + 1].ptr = ap;

    q->node[i + 1].recptr = r;

    q->keynum++;

}

 

// 將結點q分裂成兩個結點,前一半保留,后一半移入新生結點ap

void split(Position &q, Position &ap) {

    int s = (m + 1) / 2;

    ap = (Position)malloc(sizeof(BT)); // 生成新結點ap

    ap->node[0].ptr = q->node[s].ptr; // 后一半移入ap

    for(int i = s + 1; i <= m; i++ ) {

        ap->node[i-s] = q->node[i];

        if(ap->node[i - s].ptr) {

            ap->node[i - s].ptr->parent = ap;

        }

    }

    ap->keynum = m - s;

    ap->parent = q->parent;

    q->keynum = s - 1; // q的前一半保留,修改keynum

}

 

// 生成含信息(T, r, ap)的新的根結點*T,原T和ap為子樹指針

void NewRoot(Position &tree, Record *r, Position ap) {

    Position p;

    p = (Position)malloc(sizeof(BT));

    p->node[0].ptr = tree;

    tree = p;

    if(tree->node[0].ptr) {

        tree->node[0].ptr->parent = tree;

    }

    tree->parent = NULL;

    tree->keynum = 1;

    tree->node[1].key = r->key;

    tree->node[1].recptr = r;

    tree->node[1].ptr = ap;

    if(tree->node[1].ptr) {

        tree->node[1].ptr->parent = tree;

    }

}

 

/*

在m階B-樹tree上結點*q的key[i]與key[i + 1]之間插入關鍵字K的指針r。若引起

結點過大, 則沿雙親鏈進行必要的結點分裂調整, 使tree仍是m階B樹。

*/

void InsertPosition(SearchTree &tree, Record &r, Position q, int i) {

    Position ap = NULL;

    bool finished = false;

    Record *rx = &r;

 

    while(q && !finished) {

        // 將r->key、r和ap分別插入到q->key[i + 1]、q->recptr[i + 1]和q->ptr[i + 1]中

        Insert(q, i, rx, ap);

        if(q->keynum < m) {

            finished = true; // 插入完成

        } else { // 分裂結點*q

            int s = (m + 1) >> 1;

            rx = q->node[s].recptr;

            // 將q->key[s + 1..m], q->ptr[s..m]和q->recptr[s + 1..m]移入新結點*ap

            split(q, ap);

            q = q->parent;

            if(q) {

                i = Search(q, rx->key); // 在雙親結點*q中查找rx->key的插入位置

            }

        }

    }

    if(!finished) {// T是空樹(參數q初值為NULL)或根結點已分裂為結點*q和*ap

        NewRoot(tree, rx, ap); // 生成含信息(T, rx, ap)的新的根結點*T,原T和ap為子樹指針

    }

}

 

/*

操作結果: 按關鍵字的順序對tree的每個結點調用函數Visit()一次且至多一次

*/

void TraverseSearchTree(SearchTree tree, void(*Visit)(BT, int)) {

    if(tree) {// 非空樹

        if(tree->node[0].ptr) {// 有第0棵子樹

            TraverseSearchTree(tree->node[0].ptr, Visit);

        }

        for(int i = 1; i <= tree->keynum; i++ ) {

            Visit(*tree, i);

            if(tree->node[i].ptr) { // 有第i棵子樹

                TraverseSearchTree(tree->node[i].ptr, Visit);

            }

        }

    }

}

 

int main() {

    Record r[N] = {{24, "1"}, {45, "2"}, {53, "3"}, {12, "4"},

                   {37, "5"}, {50, "6"}, {61, "7"}, {90, "8"},

                   {100, "9"}, {70, "10"}, {3, "11"}, {30, "12"},

                   {26, "13"}, {85, "14"}, {3, "15"}, {7, "16"}};

    SearchTree tree = NULL;//初始化一棵空樹

    Result res;//存放結果

 

    int i;

    for(i = 0; i < N; i++ ) {

        res = SearchPosition(tree, r[i].key);

        if(!res.tag) {

            InsertPosition(tree, r[i], res.pt, res.i);

        }

    }

 

    printf("按關鍵字的順序遍歷B-樹:\n");

    TraverseSearchTree(tree, print);

    printf("\n請輸入待查找記錄的關鍵字: ");

    while (scanf("%d", &i)) {

        res = SearchPosition(tree, i);

        if(res.tag) {

            print(*(res.pt), res.i);

        } else {

            printf("沒找到\n");

        }

        puts("");

    }

    DestroySearchTree(tree);

} 

B+-tree:是應文件系統所需而產生的一種B-tree的變形樹。

一棵m階的B+樹和m階的B樹的差異在於:

1.n棵子樹的結點中含有n個關鍵字; (而B 樹n棵子樹有n-1個關鍵字)

2.所有的葉子結點中包含了全部關鍵字的信息,及指向含有這些關鍵字記錄的指針,且葉子結點本身依關鍵字的大小自小而大的順序鏈接。 (而B 樹的葉子節點並沒有包括全部需要查找的信息)

3.所有的非終端結點可以看成是索引部分,結點中僅含有其子樹根結點中最大(或最小)關鍵字。 (而B 樹的非終節點也包含需要查找的有效信息)

a)     為什么說B+-tree比B 樹更適合實際應用中操作系統的文件索引和數據庫索引?

1) B+-tree的磁盤讀寫代價更低

B+-tree的內部結點並沒有指向關鍵字具體信息的指針。因此其內部結點相對B 樹更小。如果把所有同一內部結點的關鍵字存放在同一盤塊中,那么盤塊所能容納的關鍵字數量也越多。一次性讀入內存中的需要查找的關鍵字也就越多。相對來說IO讀寫次數也就降低了。

舉個例子,假設磁盤中的一個盤塊容納16bytes,而一個關鍵字2bytes,一個關鍵字具體信息指針2bytes。一棵9B-tree(一個結點最多8個關鍵字)的內部結點需要2個盤快。而B+ 樹內部結點只需要1個盤快。當需要把內部結點讀入內存中的時候,B 樹就比B+ 樹多一次盤塊查找時間(在磁盤中就是盤片旋轉的時間)

2) B+-tree的查詢效率更加穩定

由於非終結點並不是最終指向文件內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查找必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。

b)    B+-tree的應用: VSAM(虛擬存儲存取法)文件(來源論文 the ubiquitous Btree 作者:D COMER - 1979 )

5.B*-tree

B*-treeB+-tree的變體,在B+ 樹非根和非葉子結點再增加指向兄弟的指針;B*樹定義了非葉子結點關鍵字個數至少為(2/3)*M,即塊的最低使用率為2/3(代替B+樹的1/2)。給出了一個簡單實例,如下圖所示:

B+樹的分裂:當一個結點滿時,分配一個新的結點,並將原結點中1/2的數據復制到新結點,最后在父結點中增加新結點的指針;B+樹的分裂只影響原結點和父結點,而不會影響兄弟結點,所以它不需要指向兄弟的指針。

B*樹的分裂:當一個結點滿時,如果它的下一個兄弟結點未滿,那么將一部分數據移到兄弟結點中,再在原結點插入關鍵字,最后修改父結點中兄弟結點的關鍵字(因為兄弟結點的關鍵字范圍改變了);如果兄弟也滿了,則在原結點與兄弟結點之間增加新結點,並各復制1/3的數據到新結點,最后在父結點增加新結點的指針。

所以,B*樹分配新結點的概率比B+樹要低,空間使用率更高;

 

二,B+樹

B+樹:是應文件系統所需而產生的一種B-的變形樹。

一棵m階的B+樹和m階的B樹的差異在於:

1.n棵子樹的結點中含有n個關鍵字; (而B 樹n棵子樹有n-1個關鍵字)

2.所有的葉子結點中包含了全部關鍵字的信息,及指向含有這些關鍵字記錄的指針,且葉子結點本身依關鍵字的大小自小而大的順序鏈接。 (而B 樹的葉子節點並沒有包括全部需要查找的信息)

3.所有的非終端結點可以看成是索引部分,結點中僅含有其子樹根結點中最大(或最小)關鍵字。 (而B 樹的非終節點也包含需要查找的有效信息)

a)     為什么說B+-tree比B 樹更適合實際應用中操作系統的文件索引和數據庫索引?

1) B+-tree的磁盤讀寫代價更低

B+-tree的內部結點並沒有指向關鍵字具體信息的指針。因此其內部結點相對B 樹更小。如果把所有同一內部結點的關鍵字存放在同一盤塊中,那么盤塊所能容納的關鍵字數量也越多。一次性讀入內存中的需要查找的關鍵字也就越多。相對來說IO讀寫次數也就降低了。

舉個例子,假設磁盤中的一個盤塊容納16bytes,而一個關鍵字2bytes,一個關鍵字具體信息指針2bytes。一棵9B-tree(一個結點最多8個關鍵字)的內部結點需要2個盤快。而B+ 樹內部結點只需要1個盤快。當需要把內部結點讀入內存中的時候,B 樹就比B+ 樹多一次盤塊查找時間(在磁盤中就是盤片旋轉的時間)

2) B+-tree的查詢效率更加穩定

由於非終結點並不是最終指向文件內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查找必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。

B-樹:

 image

B+樹:

image

5.B*-tree

B*-treeB+-tree的變體,在B+ 樹非根和非葉子結點再增加指向兄弟的指針;B*樹定義了非葉子結點關鍵字個數至少為(2/3)*M,即塊的最低使用率為2/3(代替B+樹的1/2)。給出了一個簡單實例,如下圖所示:

B+樹的分裂:當一個結點滿時,分配一個新的結點,並將原結點中1/2的數據復制到新結點,最后在父結點中增加新結點的指針;B+樹的分裂只影響原結點和父結點,而不會影響兄弟結點,所以它不需要指向兄弟的指針。

B*樹的分裂:當一個結點滿時,如果它的下一個兄弟結點未滿,那么將一部分數據移到兄弟結點中,再在原結點插入關鍵字,最后修改父結點中兄弟結點的關鍵字(因為兄弟結點的關鍵字范圍改變了);如果兄弟也滿了,則在原結點與兄弟結點之間增加新結點,並各復制1/3的數據到新結點,最后在父結點增加新結點的指針。

所以,B*樹分配新結點的概率比B+樹要低,空間使用率更高;

在大規模數據存儲的文件系統中,B~tree系列數據結構,起着很重要的作用,對於存儲不同的數據,節點相關的信息也是有所不同,這里根據自己的理解,畫的一個查找以職工號為關鍵字,職工號為38的記錄的簡單示意圖。(這里假設每個物理塊容納3個索引,磁盤的I/O操作的基本單位是塊(block),磁盤訪問很費時,采用B+樹有效的減少了訪問磁盤的次數。)

對於像MySQLDB2Oracle等數據庫中的索引結構得有較深入的了解才行,建議去找一些B 樹相關的開源代碼研究。

 

三,R樹的數據結構

如上所述,R樹是B樹在高維空間的擴展,是一棵平衡樹。每個R樹的葉子結點包含了多個指向不同數據的指針,這些數據可以是存放在硬盤中的,也可以是存在內存中。根據R樹的這種數據結構,當我們需要進行一個高維空間查詢時,我們只需要遍歷少數幾個葉子結點所包含的指針,查看這些指針指向的數據是否滿足要求即可。這種方式使我們不必遍歷所有數據即可獲得答案,效率顯著提高。下圖1是R樹的一個簡單實例:

我們在上面說過,R樹運用了空間分割的理念,這種理念是如何實現的呢?R樹采用了一種稱為MBR(Minimal Bounding Rectangle)的方法,在此我把它譯作“最小邊界矩形”。從葉子結點開始用矩形(rectangle)將空間框起來,結點越往上,框住的空間就越大,以此對空間進行分割。有點不懂?沒關系,繼續往下看。在這里我還想提一下,R樹中的R應該代表的是Rectangle(此處參考wikipedia),而不是大多數國內教材中所說的Region(很多書把R樹稱為區域樹,這是有誤的)。我們就拿二維空間來舉例吧。下圖是Guttman論文中的一幅圖。

我來詳細解釋一下這張圖。先來看圖(b)吧。首先我們假設所有數據都是二維空間下的點,圖中僅僅標志了R8區域中的數據,也就是那個shape of data object。別把那一塊不規則圖形看成一個數據,我們把它看作是多個數據圍成的一個區域。為了實現R樹結構,我們用一個最小邊界矩形恰好框住這個不規則區域,這樣,我們就構造出了一個區域:R8。R8的特點很明顯,就是正正好好框住所有在此區域中的數據。其他實線包圍住的區域,如R9,R10,R12等都是同樣的道理。這樣一來,我們一共得到了12個最最基本的最小矩形。這些矩形都將被存儲在子結點中。下一步操作就是進行高一層次的處理。我們發現R8,R9,R10三個矩形距離最為靠近,因此就可以用一個更大的矩形R3恰好框住這3個矩形。同樣道理,R15,R16被R6恰好框住,R11,R12被R4恰好框住,等等。所有最基本的最小邊界矩形被框入更大的矩形中之后,再次迭代,用更大的框去框住這些矩形。我想大家都應該理解這個數據結構的特征了。用地圖的例子來解釋,就是所有的數據都是餐廳所對應的地點,先把相鄰的餐廳划分到同一塊區域,划分好所有餐廳之后,再把鄰近的區域划分到更大的區域,划分完畢后再次進行更高層次的划分,直到划分到只剩下兩個最大的區域為止。要查找的時候就方便了吧。

有關R樹的詳細介紹請看博文http://blog.csdn.net/v_july_v/article/details/6530142

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM