T树索引的学习与实现(一)

来源:互联网 发布:买水果的软件 编辑:程序博客网 时间:2024/05/29 13:45

前言:

有段时间在研究T树算法,在网上只找到一篇,借鉴了一下,但是原博客中的代码出现了大量的错误,所以自己

按照原博客的思想与大部分代码重新推算演练了一下,因为站在巨人的肩膀上,所以整理的容易了一些,但是毕竟努力了很长时间,所以发篇博客吧。

一、原理与思想

磁盘数据库系统的典型的索引技术是B-tree索引。B-tree结构的主要目的是减少完成数据文件的索引查找所需要

的磁盘I/O的数量。B-tree通过控制节点内部的索引值达到这个目的,在节点中包含尽可能多的索引条目(增加一次

磁盘I/O可以访问的索引条目)。T-Tree是针对主存访问优化的索引技术。T-tree是一种一个节点中包含多个索引条

目的平衡二叉树,T-tree的索引项无论是从大小还是算法上都比B-tree精简得多。T-tree的搜索算法不分搜索的值

在当前的节点还是在内存中的其他地方,每访问到一个新的索引节点,索引的范围减少一半。

T树索引用来实现关键字的范围查询。T树是一棵特殊平衡的二叉树(AVL),它的每个节点存储了按键值排序的一

组关键字。T树除了较高的节点空间占有率,遍历一棵树的查找算法在复杂程度和执行时间上也占有优势。T树节点结

构如图所示。

T-Tree具有以下特点:

(1)T-Tree中有平衡点balancebalance=右子树的深度-左子树的深度,balance的绝对值小于等于1,也就是说T-Tree

是一颗和平衡二叉树相似的树。如果插入节点,导致balance绝对值大于1,需要经行旋转来平衡树。

(2)T-Tree中的节点可以存储n个数据,n=存储上限。在上面的节点结构图中,K1最小,Kn最大。

(3)为了保证每个节点的空间利用率,每个节点中存储的数据个数必须大于等于存储下限,一般存储下限=存储上限-2

(4)T-Tree节点的左子节点中存储的数据要小于当前节点中的最小数据K1T-Tree节点的右子节点中存储的数据要

大于当前结点中的最大数据Kn

(5)同时拥有左右子树的节点被称为内部节点,只拥有一个子树的节点被称为半叶一,没有子树的节点被称为叶子。

二、操作算法

程序中定义的节点结构如下:

typedef struct TTREENODE         //树节点的结构

{

    TTREENODE *left;             //节点的左子树指针

    TTREENODE *right;            //节点的右子树指针

    unsigned short int nItems;      //节点中键值的数目

    ElementKey key[ttPageSize];       //key值数组

ElementData data[ttPageSize];     //对应数据行的指针数组

int balance;          //balance(平衡因子),其绝对值不大于1balance =右子树高度-左子树高度;

} TTREENODE;

1、查找算法

根据给予的key值,查找对应的data值。从根节点开始,把key值分别与当前结点的最小keykey[0]

最大keykey[nItems-1]比较:如果key值小于当前结点的最小key值,则把key值与当前节点的左子树节点中的数据

进行比较;如果key值大于当前结点的最大key值,则把key值与当前结点的右子树节点中的数据进行比较;

如果不符合上述情况,则证明key值就在当前结点数组的范围内,采用二分查找在节点key值数组中找到与key值相等

位置,然后返回data数组中对应位置的数据即可。其他情况则是查找失败,返回NULL

伪码描述:

算法:T-Tree FindRecord

输入:key

输出:data

步骤:

1,定义一个节点pNode =根节点root

2whilepNode=NULL

3{

4 ifkey>=当前结点的最小keykey[0]&&<=当前节点的最大keykey[nItems-1])

5, {

6, index=在当前结点的key值数组中采用二分查找算法查找出于与key值相等位置。

7 return pNode->data[index]

8 }

9 else ifkey<当前结点的最小keykey[0]

10 {

11 pNode = pNode->left; //当前结点=此节点的左子树节点;

12 }

13else

14 { //此时,只剩下key>当前结点的最大key值得情况。

15 pNode = pNode->right;//当前结点=此节点的右子树节点;

16 }

17}

18return NULL//遍历所以节点仍然没有查找到数据并返回,证明此树中没有对应key值的数据,

2、插入算法

给予一个key值可一个data值,需要根据key值在T-Tree中找到相应的位置,把key值和data值插入。这里只给出key

的插入描述,data值的操作与key值一致。首先,从根节点开始,把key值与当前结点的最小keykey[0]和最大key

key[nItems-1]进行比较。

如果key值小于当前结点的最小key值:(1)判断当前结点中的数据量nItems是否小于最小上限,如果小于最小上限,

并且,如果当前结点没有左子树或者key值大于当前结点左子树中最大key值,则可以把key值插入当前结点key数组

0位置,不过,当前结点的key数组需要整体向后挪一位,同理,把data插入当前节点的data数组(2)如果当前结点

的数据量nItems大于等于最小上限,但是左子树=NULL,则需要开辟新节点,把keydata插入新节点中的数组中。

注意,此时由于开辟了新节点,整棵树可能不平衡,需要检测新节点的父节点的平衡点,如果不平衡,只需要旋转

一次即可。(3)如果上述情况不符合,需要把keydata递归插入当前节点的左子树节点。

如果key值大于当前结点的最大key值,与上述key值小于当前结点的最小key值得操作大体相同,不过(1)插入时

需要插入到数组的nItems位置,并且数组不需要移动,在(2)中,如果右子树=NULL才需要开辟新节点。在(3

中,需要把keydata递归插入当前结点的右子树节点。

如果key值在当前结点key数组的范围内:(1)如果当前结点的数据量nItems小于最小上限,只需在当前结点数组的

合适位置插入即可。(2)如果当前结点的数据量等于最大上限,需要把数组中最大或最小的值拿出来,然后插入到

子树中。为了保持整棵树的平衡,尽量不引起旋转,如果当前结点的balance大于等于0,证明节点有右子树,没有

左子树,需要把当前结点中的最小值拿出来,重新插入到当前节点中。同理,如果当前节点的balance小于0,需要

把当前结点的最大值拿出来,重新插入到当前结点中。

伪码描述:

算法:T-Tree InsertRecord

输入:key值和data

输出:插入新节点,返回TRUE;没有插入新节点,返回FALSE

步骤:

1if(根节点root==NULL

2{

3 root = 开辟新节点;

4key值和data值插入当前结点。

5}

6else

7{ 定义pNode =当前结点;

8 if(key<当前结点的最小key值)

9 {

10 if(如果当前结点的数据量nItems<最小上限,&&(当前结点没有左子树||key>当前结点左子树中最大key值))

11 {

12 把当前结点的数组整体向后挪一位;

13 把数据插入当前结点数组的最前面;

14 return FALSE

15 }

16 else if(当前结点没有左子树)

17 {

18 开辟新节点赋给当前结点的左子树,并插入数据;此时不需要返回,因为插入新节点, 需要验证是否需要旋转;

19 }

20 else

21 {

22 success = 把数据递归插入当前节点的左子树节点;

23 ifsuccess==FALSE)递归没有插入新节点

24 {

25 return FALSE

26 }

27 }

//程序运行到这,即插入了新节点,需要进行平衡判断。

28 if(当前结点的balance>=0)

29, {

30, 树没有失衡,重新计算结点的balance

31 }

32 else

33 {

34 树失衡,需要通过判断树的左子树的 balance,来进行右旋还是左右旋转;

35 }

36 }

37 else ifkey>当前结点的最大key值)

38 {

39 处理操作与上述key小于当前结点的最小key相似,只不过把左子树换成右子树;

40 }

41 else

42 { //此时,key值处于节点中的数组范围内;

43 int r = 利用二分查找找到需要插入数组位置;

44 ifpNode的数组量nItems<存储下限)

45 {

46 pNode数组在r位置到末尾向后移一位;

47 插入数据;

48 return FALSE //没有插入新节点

49 }

50 else

51,      {//节点中数据已满,只需要考虑把节点往前挤掉一个,还是往后挤掉一个

52 ifpNodebalance>0

53 {//为了避免插入可能带来的增加节点导致旋转引起效率下降,插入需要把数组中

 //最前面的数据挤掉,重新插入。

54 pNode数组在0~r位置向前移一位;

55 插入数据;

56 把原数组最小数据递归插入当前结点;

57 }

58 else

59 {

60 pNode数组在r位置到末尾向后移一位;

61 插入数据;

62 把原数组最大数据递归插入当前结点;

63 }

64 }

65 }

66}

3、删除算法

如果key值不再当前结点的key值范围内,则递归调用本方法在子节点中删除key值对应的数据

  如果key值在范围内:

(1)如果当前结点的nItems>minSize,则只需要删除节点中的数据,nItems-1即可

2)如果当前结点的nItems=minSize,需要调用一个子节点中的数据添加到本节点中。至于调用子节点中小于

当前结点最小值的最大值,还是大于当前结点的最大值的最小值,需要看当前结点的balance,尽可能避免删除

带来的旋转

3)因为子节点的数据需要向上补充,所以有的子节点中的数据可能小于minSize,当节点中的数据=1,并且

需要删除的时候,本节点也需要删除,因此可能导致T树不平衡,导致旋转

T树与AVL树删除的不同的是,由于再删除中间节点的时候,子节点中的数据需要向上补充,所以删除的一直是

子节点。

删除算法是T-Ttee算法最大的难点,因为删除节点可能导致多次旋转,而且,如果删除上层节点的数据,如果

上层节点的数据小于最小上限时,需要下层节点的数据往上补充,如果上层节点是下层节点的父节点,只需要

在递归删除时调用一次平衡节点函数,然后逐渐递归平衡到root节点;但是,如果上层节点与下层节点差几层,

那么在递归删除中,需要递归平衡上层节点到下层节点这一路所有的节点。

伪码描述:

算法:T-Tree DeleteRecord

输入:当前结点,key

输出:-1:没有删除成功; 1:删除一个节点; 0:没有删除节点,但删除数据成功

步骤:

1从根节点root开始递归删除

2定义pNode=当前节点;

3ifkey<pNode的最小key&&pNode->left=NULL

4{

5 int h = 递归删除(pNode->leftkey);

6 ifh>0)平衡节点pNode

7 else return 0

8}

9else ifkey>pNode的最大key&&pNode->right=NULL

10{

11 int h = 递归删除(pNode->rightkey);

12 ifh>0)平衡节点pNode

13 else return 0

14}

15else

16{

17 for(循环节点中的数组)

18 {

19 ifkey==数组key[index]

20 {

21 ifpNode->nItems==1&&pNode左右节点不存在)

22 { 删除节点;return 1}

23 ifpNode->nItems<=最小上限)

24 {

25 ifpNode->balance<=0)

26, {

27, 删除数组index位置的数据;

28 数组从0~index-1位置的数据向后移动一位。

29 数组[0]=左子树中最大的key

30 int h =递归删除(左子树,数组[0]);

31 ifh>0

32 {

33 判断是否隔层调入节点,如果是则递归平衡这一路节点,否则,单独平衡此节点;

34 }

35 return h

36 }

37 else

38 {

39 与上述if中的程序相似,不过需要数组从末尾~index+1向前移动一位,把右子树中的最小值调入数组,

然后删除从右子树调入的值,平衡节 点。

40 }

41 }

42 else

43 { //pNode->nItems>最小上限

44 删除数组中对应的数据;

45 return 0

46 }

47 }

48 }

49}

50return 0

关键代码(平衡右子树与平衡左子树大致相同,不再展出):

/*

 * 平衡左子树

 * 由于调用直接子节点数据,导致子节点删除的情况

 *

 */

int TTree::balanceLeftBranch(TTREENODE *&pNode)

{

    if(pNode->balance < 0)

    {

        pNode->balance = 0;

        //至于返不返回1,还需要看是不是(祖祖)祖父节点调用最下层节点的情况,(决定重新写一个这种情况下的平衡方法)

        return 1;//h>=1,证明子节点一层全部删除,pNode左子树少一层,可能导致旋转,返回1

    }

    else if(pNode->balance == 0)

    {

        pNode->balance = 1;

        return 0; //删除了pNode的左子树,但是还有右子树支撑平衡

    }

    else

    {

        TTREENODE *pRightId = pNode->right;

        int prightbf = pRightId->balance;

        if(prightbf >= 0)  //

        {

            pNode = singleRotateRight(pNode);

            if(prightbf == 0)   //第二次寻BUG:用prightbf代替pRightId->balance,因为是指针,在旋转后,balance变化,导致前后不一致

            {

                pNode->balance = -1;

                pNode->left->balance = 1;

                return 0;

            }

            else

            {

                pNode->balance = 0;

                pNode->left->balance = 0;

                return 1; //旋转平衡导致pNode的深度-1,可能对上层造成影响

            }

        }

        else

        {

            pNode = doubleRotateRightLeft(pNode);

            return 1;

        }

    }

    return 0;

}

/*

 * 平衡左子树分支

 * 由于删除的另一种情况,节点跨层调用底层节点的数据,导致节点删除,可能需要平衡旋转

 * 需要递归从最底层开始查看是否需要旋转,如果最底层高度不变,则对整棵树的平衡不会造成影响

 */

int TTree::balanceLeftBranchInterlayer(TTREENODE *&pNode)

{

    int h;

    //qDebug() << "pNode->left: " << pNode->left;

    if(pNode->left == NULL)

    {

      //  qDebug() << "最底层,开始balanceLeftBranch";

        if(pNode->right == NULL)                                                         //第二次修改:两种情况,这种是虽然是隔层删除节点,但是还有邻层删除。

        {

            return 1;

        }

        h = balanceLeftBranch(pNode);

        return h;

    }

 

    TTREENODE *tempNode = pNode->left;

    if((h = balanceLeftBranchInterlayer(tempNode)) > 0)

    {

        h = balanceLeftBranch(pNode);                                                       

    }

    else

    {

        return 0;

    }

 

    return h;

}


4、旋转操作算法

由于T-Tree是一颗特殊的平衡二叉树,所以当整棵树失衡时,需要旋转来保持平衡,有四种类型旋转,

分别为LL旋转,RR旋转,LR旋转,RL旋转。

4、1 LL型旋转


关键代码:

//LL类型的旋转需要右旋一次,然后返回新的根节点

TTREENODE *TTree::singleRotateLeft(TTREENODE *pNode)

{

    TTREENODE *k = pNode->left;

    pNode->left = k->right;

    k->right = pNode;

 

    pNode->balance = getBalance(pNode);

    k->balance = getBalance(k);

    return k;

}

4、2 RR型旋转


关键代码:

//RR类型的旋转需要左旋一次,然后返回新的根节点

TTREENODE *TTree::singleRotateRight(TTREENODE *pNode)

{

    TTREENODE *k = pNode->right;

    pNode->right = k->left;

    k->left = pNode;

 

    pNode->balance = getBalance(pNode);

    k->balance = getBalance(k);

    return k;

}

4、3 LR型旋转


关键代码:

//LR类型的旋转需要根节点的左子树先左旋一次,变成LL类型,然后根节点再右旋一次,然后返回新的根节点

TTREENODE *TTree::doubleRotateLeftRight(TTREENODE *pNode)

{

    pNode->left = singleRotateRight(pNode->left);

 

    pNode->balance = getBalance(pNode);

 

    return singleRotateLeft(pNode);

}

4、4 RL型旋转


关键代码:
//RL类型的旋转需要根节点的右子树先右旋一次,变成RR类型,然后根节点再左旋一次,返回根节点
TTREENODE *TTree::doubleRotateRightLeft(TTREENODE *pNode)
{
    pNode->right = singleRotateLeft(pNode->right);
    pNode->balance = getBalance(pNode);
    return singleRotateRight(pNode);
}


五、未解决的问题

T-Tree再删除数据时,可能引起旋转导致某个关键字数目少于最小上限的某个节点旋转到上层;也就是说,

在删除时还需要特殊的旋转算法,暂时没有实现。





1 0