计算机技术一锅炖

多路搜索树B树

2018-01-14  本文已影响18人  _shen

二叉搜索树可以看作是2路的搜索树,也就是每个节点只有2个子节点,如果将这种情况推广,也就是说每个节点可以有多个(≧2)个节点,那么这种树就可以称为是多路树,结合搜索树的特点,节点左侧的大于右侧,这也就是B树。
B树的定义:
①每个节点x中存储的关键字个数为n(在二叉树中,每个节点只包含一个关键字);每个节点中关键字按照升序排列,即x.key1≦x.key2≦…x.key3;如果该节点为叶子节点,那么节点属性x.leaf为true,否则为false。
②非叶子节点(或称作内部节点)的孩子数量为n+1,有n+1个指向孩子的指针;叶子节点没有孩子。
③非叶子节点的关键字和叶子孩子的关键字
④每个叶子节点具有相同的深度,即树的高度h。
⑤每个节点所包含的关键字个数有上界和下界。用树的最小度数d(d≧2)进行描述,除根节点以外,其余节点中包含的关键字数量n满足:d-1≦n≦2d-1;当一个节点恰好有2d-1个关键字,就说该节点是满的。

B树结构图
// 关键字类型
#define KeyType int
// 关键字的最大数量
#define N 3

// B树节点结构体
typedef struct BTreeNode
{
    int keyNum=0;                 // 节点中关键字的数量
    KeyType key[N];               // 存储关键字的数组
    struct BTreeNode *sub[N+1];   // 指向子节点的指针数组
    boolean isleaf=false;         // 当前节点是否为叶子节点,false:不是,true:是
}BTreeNode, *pBTreeNode;

B树的高度:
如果n≧1,那么对于任意一棵包含n个关键字、高度为h、最小度数d≧2的B树T,有

h ≦ logd(n+1)/2

这个我们很容易通过之前对二叉树的分析得出。
B树的查找:
查找的方式和二叉搜索树基本上类似,只不过在每个节点上多了一些比较而已,也就是多路分支选择,可采取递归的方式进行查询:

// B树查询结果结构体
typedef struct
{
    BTreeNode *pointer;           // 指向节点的指针
    int pos;                  // 关键字在数组中的索引
}BTreeSearchResult;

/**
 * 查找节点
 * T : 根节点
 * k :节点key值
 * return : 查询的结果
 */
BTreeSearchResult SearchBTree( BTreeNode T, KeyType k )
{
    BTreeNode temp = T;
    BTreeSearchResult res;
    int i = 0;

    // 循环查找
    while ((i < temp->keyNum) && (k > temp->key[i])) {
        i++;
    }

    // 找到结果
    if ((i < T->keyNum) && (k == temp->key[i])) {
        res.pointer = temp;
        res.i = i;
        return res;
    } else if (temp->isleaf) { // 叶子节点
        return NULL
    } else { // 递归查找
        ReadDisk(temp->sub[i])
        return SearchBTree(temp->sub[i], key)
    }
}

B树的插入:
为确保B树在插入一个新的关键字后,仍旧是一棵合法的B树,我们就不能像二叉树中插入节点那样,直接创建一个节点并作为某个节点的子节点直接插入,而是要将关键字插入到已经存在的叶子节点中去。这里要注意的是插入到B树的叶子节点,不是非叶子节点,也不是创建一个新的叶子节点。其具体步骤是:自定向下遍历,根据关键字的大小选择相应的分支,继续向下遍历,直到找到相应的叶子节点。然后判断叶子节点是否已满,如果未满,就直接插入到相应的关键字位置。如果节点已满,就需要对节点进行分裂,其做法是将中间的节点提出来,然后加入到父节点中,父节点的两个指针分别指向分裂的两个节点。现在有个问题是,如果父节点也已经满了,我们需要对父节点进行分裂,因此为了使问题变得简单,我们自顶向下的遍历过程中就对所有已经满的节点进行分裂,确保在分裂一个节点时其父节点不是满的。

/**
 * 写入磁盘
 * 写入的节点 : 节点指针
 */
void WriteDisk( pBTreeNode n ){

    // 这里为了演示,循环输出关键字的数值
    int i = N;

    printf("写入磁盘:\n");
    while ( i-- ) {
        printf("%c ", n->key[i]);
    }
    printf("\n");
}

/**
 * 读入磁盘
 * 返回值:读入的节点
 */
void ReadDisk( pBTreeNode* n ){
    // 这里为了演示,循环输出关键字的数值
    int i = N;

    printf("写入磁盘:\n");
    while ( i-- ) {
        printf("%c ", (*n)->key[i]);
    }
    printf("\n");
}
/**
 * 插入一个关键字
 * T : 根节点
 * k :关键字key值
 * return : 查询的结果 0:成功 ,-1:失败
 */
int InsertBTree( pBTreeNode tree,  KeyType k)
{
    pBTreeNode newnode,
               np = *tree;
    // 节点为空
    if ( NULL == np ) {
        // 从内存中分配一个节点
        newnode = (pBTreeNode)malloc(sizeof(BTreeNode));
        // 默认为叶子节点
        newnode.isleaf = true;
        // 关键字数量为0
        newnode.keyNum = 0;
        // 树指针指向根节点
        *T = newnode;
        // 写入磁盘
        WriteDisk(newnode);

        // 直接返回
        return 0;
    }

    // 遍历寻找插入位置,检查节点是否已满
    // 如果已满需要进行分裂
    if ( np.keyNum == N ) {
        // 创建一个新节点
        newnode = (pBTreeNode)malloc(sizeof(BTreeNode));
        newnode.isleaf = false;
        newnode.keyNum = 0;
        newnode.sub[0] = np;
        // 重新定义为根
        np = newnode;
        // 分裂原节点
        SplitBTree( np, 0 );
        insertNotFullNodeBTree( np, k );
    } else {// 如果未满,直接插入
        insertNotFullNodeBTree( np, k );
    }

    return 0;
}
/**
 * 分裂节点
 * np : 节点
 * i : 待分裂的子节点索引
 * return : 分裂结果 0:成功,-1:失败
 */
int SplitBTree( BTreeNode &np, int i)
{
    int j,m;
    // 裂开之后的左半部分
    pBTreeNode nl = np.sub[i];
    // 裂开之后的右半部分
    pBTreeNode nr = CreateBTreeNode();

    // 找到中间关键字
    m = (N+1) / 2;
    // 移动关键字
    for ( j = 0; j < N-m; j++ ) {
        nr.key[j] = nl.key[m+j];
    }
    // 如果是非叶子节点,还需要移动子节点
    if ( !nl.isleaf) {
        for ( j = 0; j < N-m; j++ ) {
            nr.sub[j] = nl.sub[m+j];
        }
    }

    // 更新左部分数量
    nl.keyNum = m-1;

    // 更新指针
    for ( j = np.keyNum; j > i; j-- ) {
        np.sub[j] = np.sub[j-1];
    }
    // 设置指向分裂出来的右半部分
    np.sub[i] = nr;
    // 更新关键字
    for ( j = np.keyNum; j > i; j-- ) {
        np.key[j] = np.key[j-1];
    }
    // 设置提取出来的关键字
    np.key[i] = nl.key[m-1];
    // 更新父节点关键字数量
    np.keyNum++;

    return 0;
}

/** 
 * 插入关键字到未满节点中
 * np : 节点
 * i : 待分裂的子节点索引
 * return : 插入结果 0:成功,-1:失败
 */
int insertNotFullNodeBTree( BTreeNode &np, int k )
{
    int i = np.keyNum;// 节点的关键字数量

    // 如果是叶子节点,选择合适的位置插入
    if ( np.isleaf ) {
        while ( i-- && np.key[i] > k ) {
            np.key[i] = np.key[i-1];
        }
        // 找到位置
        np.key[i] = k;
        np.keyNum++;
        // 写入磁盘
        WriteDisk(np);

        return 0;

    } else {// 寻找指定的非叶子节点,递归插入

        while ( i-- && np.key[i] > k );

        // 找到位置,递归插入,送入的是节点的地址
        ReadDisk( &np.sub[i] );

        // 如果节点已满,需要进行分裂
        if (np.sub[i].keyNum == N ) {
            SplitBTree(np.sub[i], i);
            // 分裂之后,再对待插入关键字和提取上来的关键字作大小比较
            if ( k > np.key[i] ) {
                i++;
            }
        }
        // 此时递归插入关键字到未满节点中
        insertNotFullNodeBTree( np.sub[i], k );
    }
}
上一篇下一篇

猜你喜欢

热点阅读