数据结构(三)--- B树(B-Tree)

   文章图片代码来自邓俊辉老师的课件

概述

上图就是 B-Tree 的结构,可以看到这棵树和二叉树有点不同---“又矮又肥”。同时子节点可以有若干个小的子节点构成。那么这样一棵树又有什么作用呢?

动机

我们知道电脑的访问内存比访问外的存I/O操作快了,但是内存的容量大小又只有那么一点点(相对于外存),所以计算机访问的过程常常使用高速缓存。使用高速缓存也是在以下两个事实想出的策略。

而B-Tree这种结构就是根据这种情况被发掘出来的。下图 m 指的是每次的数据块数量

B-Tree 介绍

多路平衡

关键码指的是一个超级节点包含的子节点。

B-Tree定义

m阶指的是m路,一个超级节点最大可以分出多少路。二叉树分出两边,左边和右边,就是两路,二阶。

下面是几个定义为不同阶的B-树。

分支数

B-Tree的分支数有个上下限,例如6阶的B-Tree(m=6),又被称为 “(3,6)-树”,类似的还有 “(3,5)-树”,“(2,4)-树”,而(2,4)树就是我们后面要学的红黑树。

最大树高和最小数高

可以看到对于“含N个关键码的m阶B-树”的最大树高和最小树高之间的波动并不大。

代码实现

代码实现主要的两个方法为插入和删除。其中插入的时候需要注意查看某个节点是否超出了阶数,若超出了,需要分裂,最坏的情况就是分裂到根部,而删除操作需要注意查看是否会产生下溢,处理下溢,我们常用的方法就是旋转和合并。

插入

下图分别是分裂和再分裂的图示。    

删除

删除操作的旋转和合并。

旋转可以理解为左右兄弟有足够的节点,向左右兄弟节点借来补充的操作。

假如向兄弟们借都不成功,那就拿父节点的一个元素一起合并,代码实现中有分左合并和右合并。

java版本代码

B-树节点类

package BTree;

import java.util.Vector;

/**
 * B-树的节点Bean
 * 包含一个有序向量 value 和 指向子节点的 child 向量
 *
 */
public class BTreeNode {
    BTreeNode parent;
    Vector<BTreeNode> child;
    Vector<Integer> value;

    public BTreeNode(int value, BTreeNode left, BTreeNode right) {
        if (this.value == null) {
            this.value = new Vector<>();
            this.value.sort(new VectorComparable());
        }
        if (child == null) {
            child = new Vector<>();
        }
        this.value.add(value);
        this.child.add(0, left);
        this.child.add(1, right);
        if (left != null) {
            left.parent = this;
        }
        if (right != null) {
            right.parent = this;
        }
    }

    public BTreeNode() {
        parent = null;
        if (this.value == null) {
            this.value = new Vector<>();
            this.value.sort(new VectorComparable());
        }
        if (child == null) {
            child = new Vector<>();
        }
    }

    /**
     * 一个关键块内的查找 查找到与否都返回一个index
     * 返回最靠近的值的原因是为了下面的节点继续查找
     *
     * @param value 查找的值
     * @return 不存在的情况返回最靠近的index 值 , -1
     */
    public int search(int value) {
        int hot = -1;
        for (int i = 0; i < this.value.size(); i++) {
            if (this.value.get(i) > value) {
                return hot;
            } else if (this.value.get(i) < value) {
                hot = i;
            } else { // 相等
                return i;
            }
        }
        return this.value.size() - 1;
    }

    public int getIndexInValue(int compare) {
        for (int i = 0; i < this.value.size(); i++) {
            if (compare == value.get(i)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 查找当前node在父节点中的index
     *
     * @return -1 为父类不存在或是父类为null ,其他为当前节点在父节点为位置
     */
    public int getIndexFromParent() {
        if (parent == null) {
            return -1;
        }
        for (int i = 0; i < parent.child.size(); i++) {
            if (parent.child.get(i) == this) {
                return i;
            }
        }
        return -1;
    }

    public void addValue(int index, int val) {
        value.add(index, val);
        value.sort(new VectorComparable());
    }

    public void addValue(int val) {
        value.add(val);
        value.sort(new VectorComparable());
    }

}

B-树数据结构方法。

package BTree;

public class BTree {
    private BTreeNode root;
    private int degree; // m阶B-树 ,阶树至少为3

    /*
     * 私有方法
     */

    /**
     * 查找在哪个BTreeNode ,假如到了外部节点,返回该外部节点 返回的结果只有两种 :
     * - 存在,返回该节点
     * - 不存在,返回值应该插入的节点
     *
     * @param val 查找的值
     * @return 返回搜索结果,假如该关键块不存在(到达了外部节点)就返回该关键快
     */
    private BTreeNode searchSurroundNode(int val) {
        BTreeNode node, hot = null;
        int rank;
        node = root;
        while (node != null) {
            rank = node.search(val);
            if (rank != -1 && node.value.get(rank) == val) { // 找到对应的值
                return node;
            } else {
                hot = node;
                if (node.child.get(rank + 1) == null) {
                    return hot;
                }
                node = node.child.get(rank + 1);
            }
        }
        // 到了外部节点
        return hot;
    }

    private void addNodeForBtNode(BTreeNode node, int rank, int val) {
        node.addValue(val);
        if (rank != -1) {
            node.child.add(rank + 2, null);
        } else {
            node.child.add(0, null);
        }
    }

    /*
     *  下面为可调用的方法
     */

    public BTree(int degree) {
        this.degree = degree;
    }

    /**
     * 返回值所在的节点
     *
     * @param val 插入的值
     * @return 找到的话返回节点,找不到返回 null
     */
    public BTreeNode search(int val) {
        BTreeNode node = searchSurroundNode(val);
        if (node.value.get(node.search(val)) == val) { // 该节点存在该值
            return node;
        }
        return null;
    }

    /**
     *
     * 插入的值都会进入到底部节点
     * @param val 插入的值
     * @return 是否插入成功
     */
    public boolean insert(int val) {
        if (root == null) {
            root = new BTreeNode(val, null, null);
            return true;
        }
        //root 已经创建,插入的值最终会到达底部,然后插进去
        BTreeNode node = searchSurroundNode(val);
        int rank = node.search(val);
        if (rank != -1 && node.value.get(rank) == val) { // 该节点存在该值,返回插入失败
            return false;
        } else { // 值将会插入该关键码
            addNodeForBtNode(node, rank, val);
            split(node);
            return true;
        }
    }

    private void split(BTreeNode node) {
        while (node.value.size() >= degree) {
            // 1.取中数
            int midIndex = node.value.size() / 2;
            BTreeNode rightNode = new BTreeNode();
            for (int i = midIndex + 1; i < node.value.size(); i++) {
                rightNode.addValue(node.value.remove(i));
                if (i == midIndex + 1) {
                    rightNode.child.add(node.child.remove(i));
                }
                rightNode.child.add(node.child.remove(i));
            }
            for (BTreeNode rn : rightNode.child) {
                if (rn != null) {
                    rn.parent = rightNode;
                }
            }

            // 移除原节点记得移除对应它的子节点
            int insertValue = node.value.remove(midIndex);
            if (node.parent != null) { // 存在父节点,把分裂点添加在父节点上
                node.parent.addValue(insertValue);
                /*
                 * 对插入的节点的子节点进行处理
                 * 1.得出插入点的index
                 * 2.左边子节点连接原node,右节点连接 rightNode
                 */
                int indexInValue = node.parent.getIndexInValue(insertValue);
                node.parent.child.add(indexInValue + 1, rightNode);
                rightNode.parent = node.parent;
                node = node.parent;
            } else { // 不存在父节点,并且当前节点溢出
                root = new BTreeNode(insertValue, node, rightNode);
                break;
            }
        }
    }

    public boolean delete(int val) {
        //node 为要删除的val所在的节点
        BTreeNode node = search(val);
        if (node != null) {
            int rank = node.getIndexInValue(val);
            // 找到继承结点并代替
            if (node.child.get(0) != null) { //非底部节点
                BTreeNode bottom = node.child.get(rank + 1);
                while (bottom.child.get(0) != null) {
                    bottom = bottom.child.get(0);
                }
                node.value.set(rank, bottom.value.get(0));
                bottom.value.set(0, val);
                node = bottom;
                rank = 0;
            }
            // 此时 node 一定是外部节点了(最底层)
            node.value.remove(rank);
            node.child.remove(rank + 1);
            // 由于删除了某个值,所以需要从兄弟中借一个来拼凑(旋转)
            // 当兄弟自己已到达下限,与父类合并成更大的节点,原来父节点所在的节点有可能-1后
            // 导致又达到了下限,然后循环
            solveUnderflow(node);
            return true;
        }
        return false;
    }

    /**
     * 下溢的节点 :
     * - 外部节点
     * - 非外部节点
     *
     * @param node 下溢的节点
     */
    public void solveUnderflow(BTreeNode node) {
        //没有达到下溢的条件
        int condition = (degree + 1) / 2;
        if (node.child.size() >= condition) {
            return;
        }
        BTreeNode parent = node.parent;
        if (parent == null) { //到了根节点
            if (node.value.size() == 0 && node.child.get(0) != null) {
                root = node.child.get(0);
                root.parent = null;
                node.child.set(0, null);
            }
            return;
        }
        int rank = node.getIndexFromParent();

        //旋转
        if (rank > 0 && parent.child.get(rank - 1).child.size() > condition) { //左旋转,从左兄弟拿一个
            BTreeNode ls = parent.child.get(rank - 1);
            node.addValue(0, parent.value.remove(rank - 1));
            parent.addValue(rank - 1, ls.value.remove(ls.value.size() - 1));
            /*
             *   被取走的节点可能存在子节点,需要放在新的位置
             *   有可能上一次进行合并操作中,父节点的关键码为空了,
             *   但是父节点还存在子节点(不为null)
             */
            node.child.add(0, ls.child.remove(ls.child.size() - 1));
            if (node.child.get(0) != null) {
                node.child.get(0).parent = node;
            }
            return;
        } else if (rank < parent.child.size() - 1 && parent.child.get(rank + 1).child.size() > condition) {  //右旋转,从右兄弟拿一个
            BTreeNode rs = parent.child.get(rank + 1);
            node.addValue(parent.value.remove(rank));
            parent.addValue(rs.value.remove(0));

            node.child.add(node.child.size(), rs.child.remove(0));
            if (node.child.lastElement() != null) {
                node.child.lastElement().parent = node;
            }
            return;
        }

        // 合并
        if (rank > 0) { // 左合并
            BTreeNode ls = parent.child.get(rank - 1);
            //父类节点转入到左节点
            ls.addValue(ls.value.size(), parent.value.remove(rank - 1));
            parent.child.remove(rank);
            //当前节点转入到左节点
            ls.child.add(ls.child.size(), node.child.remove(0));
            if (ls.child.get(ls.child.size() - 1) != null) {
                ls.child.get(ls.child.size() - 1).parent = ls;
            }
            // 当前节点有可能value为空,但是child不为空。
            // value 为空不移动,不为空移动
            while (node.value.size() != 0) {
                ls.addValue(node.value.remove(0));
                ls.child.add(ls.child.size(), node.child.remove(0));
                if (ls.child.get(ls.child.size() - 1) != null) {
                    ls.child.get(ls.child.size() - 1).parent = ls;
                }
            }

        } else {   //右合并,有可能 rank = 0
            BTreeNode rs = parent.child.get(rank + 1);
            //父类节点转入到右节点
            rs.addValue(0, parent.value.remove(rank));
            //父类节点断开与当前节点的连接
            parent.child.remove(rank);
            //当前节点转入到右节点
            rs.child.add(0, node.child.remove(0));
            if (rs.child.get(0) != null) {
                rs.child.get(0).parent = rs;
            }
            while (node.value.size() != 0) {
                rs.addValue(0, node.value.remove(0));
                rs.child.add(0, node.child.remove(0));
                if (rs.child.get(0) != null) {
                    rs.child.get(0).parent = rs;
                }
            }

        }
        solveUnderflow(parent);
    }

    public int height() {
        int h = 1;
        BTreeNode node = root;
        while (node != null) {
            if (node.child.get(0) != null) {
                h++;
                node = node.child.get(0);
            } else {
                break;
            }
        }
        return h;
    }

}

最后是一个测试的方法。

package BTree;

public class BTreeTest {
    public static void main(String[] args) {
        BTree tree = new BTree(3);
        tree.insert(53);
        tree.insert(97);
        tree.insert(36);
        tree.insert(89);
        tree.insert(41);
        tree.insert(75);
        tree.insert(19);
        tree.insert(84);
        tree.insert(77);
        tree.insert(79);
        tree.insert(51);
//        System.out.println(tree.height());
//        tree.insert(23);
//        tree.insert(29);
//        tree.insert(45);
//        tree.insert(87);
//        System.out.println("-------------");
        System.out.println("插入节点以后的树的高度 : "+tree.height());
        System.out.println("-------------");
//        tree.delete(41);
//        tree.delete(75);
//        tree.delete(84);
//        tree.delete(51);

        tree.delete(36);
        tree.delete(41);
        System.out.println("删除节点以后的树的高度 : "+tree.height());

    }
}

参考资料

  • 邓俊辉老师的数据结构课程

原文地址:https://www.cnblogs.com/Benjious/p/10353733.html

时间: 2024-10-09 22:40:00

数据结构(三)--- B树(B-Tree)的相关文章

【数据结构】线段树(interval tree)

线段树(interval tree),也叫区间树.也是一种二叉搜索树,同一般的BST不同之处在于:线段树的每一个结点包含的是一个区间而不是一个数.具体的描述如下: 从图上可以看出,线段树的每一个结点都是一个线段(区间),子节点是对父结点的进一步分划,每个子节点的长度都是父节点的二分,每个叶子结点就是一个元素. 每个节点可以用一个变量hit_count来计算在每一段的命中率,这样可以用来统计此线段线段或者区间内的命中率. 区间树主要用在一些跟统计和分部相关的计算中,可以快速找到相应的数据.

SDUT 3375 数据结构实验之查找三:树的种类统计

数据结构实验之查找三:树的种类统计 Time Limit: 400MS Memory Limit: 65536KB Submit Statistic Problem Description 随着卫星成像技术的应用,自然资源研究机构可以识别每一个棵树的种类.请编写程序帮助研究人员统计每种树的数量,计算每种树占总数的百分比. Input 输入一组测试数据.数据的第1行给出一个正整数N (n <= 100000),N表示树的数量:随后N行,每行给出卫星观测到的一棵树的种类名称,树的名称是一个不超过20

笛卡尔树cartesian tree

笛卡尔树cartesian tree 笛卡尔树是一种特定的二叉树数据结构,可由数列构造,在范围最值查询.范围top k查询(range top k queries)等问题上有广泛应用.它具有堆的有序性,中序遍历可以输出原数列.笛卡尔树结构由Vuillmin(1980)[1]在解决范围搜索的几何数据结构问题时提出.从数列中构造一棵笛卡尔树可以线性时间完成,需要采用基于栈的算法来找到在该数列中的所有最近小数. 定义 无相同元素的数列构造出的笛卡尔树具有下列性质: 结点一一对应于数列元素.即数列中的每

《数据结构》线段树入门(一)

今天介绍一种非常特殊的数据结构——线段树 首先提出一个问题: 给你n个数,有两种操作: 1:给第i个数的值增加X 2:询问区间[a,b]的总和是什么? 输入描述 输入文件第一行为一个整数n,接下来是n行n个整数,表示格子中原来的整数.接下一个正整数q,再接 下来有q行,表示q个询问,第一个整数表示询问代号,询问代号1表示增加,后面的两个数x和A表示给 位置X上的数值增加A,询问代号2表示区间求和,后面两个整数表示a和b,表示要求[a,b]之间的区间和. 样例输入 4 7 6 3 5 2 1 1

线段树 Interval Tree

一.线段树 线段树既是线段也是树,并且是一棵二叉树,每个结点是一条线段,每条线段的左右儿子线段分别是该线段的左半和右半区间,递归定义之后就是一棵线段树. 例题:给定N条线段,{[2, 5], [4, 6], [0, 7]}, M个点{2, 4, 7},判断每个点分别在几条线段出现过? 1.构建线段树 2.处理线段 三条线段分割之后 3.查询 对于每一个值我们就可以开始遍历这一颗线段树,加上对于结点的count字段便是在线段中出现的次数 比如对于4,首先遍历[0, 7],次数 = 0+1=1:4在

数据结构和算法 (二)数据结构基础之树、二叉树

Java面试宝典之二叉树的实现 我们接着上一篇数据结构继续讲解.本章系数据结构之树与二叉树,从这章开始,我们就要介绍非线性结构了,这些内容理解起来比线性表稍难一些,我尽量写的通俗一些,如果读的过程中有任何问题,请按上述方式联系我! 一.树 树 形结构是一类重要的非线性结构.树形结构是结点之间有分支,并具有层次关系的结构.它非常类似于自然界中的树.树结构在客观世界中是大量存在的,例如家 谱.行政组织机构都可用树形象地表示.树在计算机领域中也有着广泛的应用,例如在编译程序中,用树来表示源程序的语法结

《数据结构》线段树入门(二)

今天继续介绍——线段树之延迟标记 接上期<数据结构>线段树入门(一):http://www.cnblogs.com/shadowland/p/5870339.html 在上期介绍了线段树的最基本内容(线段树单点修改,区间查询),这次将介绍:区间修改,区间查询. Question: 给你N个数,有两种操作: 1:给区间[a,b]的所有数增加X 2:询问区间[a,b]的数的和. 输入描述: 第一行一个正整数n,接下来n行n个整数,再接下来一个正整数Q,每行表示操作的个数,如果第一数是1,后接3个正

可持久化数据结构之主席树

转自:http://finaltheory.info/?p=249 HomeACM可持久化数据结构之主席树 06十2013 可持久化数据结构之主席树 Written by FinalTheory on. Posted in ACM 引言 首先引入CLJ论文中的定义: 所谓的“持久化数据结构”,就是保存这个数据结构的所有历史版本,同时利用它们之间的共用数据减少时间和空间的消耗. 本文主要讨论两种可持久化线段树的算法思想.具体实现以及编码技巧. 核心思想 可持久化线段树是利用函数式编程的思想,对记录

大话数据结构(十三)——树的理论知识

1.树的定义 树(Tree)是n(n>=0)个结点的有限集. n=0时称为空树. 在任意一棵非空树中: (1)有且仅有一个特定的称为根(root)的结点. (2)当n>1时,其余结点可以分为m(m>0)个互不相交的有限集T1,T2,T3--Tm,其中每个集合本身又是一棵树,并且称为根的子树(SubTree). 1.1 结点分类 树的结点包含一个数据元素及若干指向其子树的分支. (1)结点拥有的子树数称为结点的度(Degree). (2)度为0的结点称为叶结点(leaf)或者终端结点:度不

《ACM/ICPC 算法训练教程》读书笔记 之 数据结构(线段树详解)

依然延续第一篇读书笔记,这一篇是基于<ACM/ICPC 算法训练教程>上关于线段树的讲解的总结和修改(这本书在线段树这里Error非常多),但是总体来说这本书关于具体算法的讲解和案例都是不错的. 线段树简介 这是一种二叉搜索树,类似于区间树,是一种描述线段的树形数据结构,也是ACMer必学的一种数据结构,主要用于查询对一段数据的处理和存储查询,对时间度的优化也是较为明显的,优化后的时间复杂为O(logN).此外,线段树还可以拓展为点树,ZWK线段树等等,与此类似的还有树状数组等等. 例如:要将