心里没有B树吗


前言

基本的搜索结构

种类数据格式时间复杂度
顺序查找无要求O(N)
二分查找有序O(log2 N)
二叉搜索树无要求O(N)
二叉平衡树(AVL树 和 红黑树)无要求O(log2 N)
哈希表无要求O(1)
位图无要求O(1)
布隆过滤器无要求O(K),K为哈希函数的数量

对于顺序查找,他的时间复杂度比较高,每次都整体遍历一遍,显然不太好;

二分查找虽然可以将时间复杂度变为log2N,但是他的使用有一个前提就是必须有序(升序,降序都可);

对于二叉树类型的查找,先是二叉搜索树,他的设计虽然是基于查找的,但是由于没有旋转的操作,就很容易造成所插入的数据只有一个单边的情况(顺序插入);在这之后有了基于旋转的AVL树,以及优化后减少旋转次数的红黑树

为了可以快速,准确的直接找到数据,又出现了哈希表,他可以将数据查找的时间复杂度近似于O(1),虽然很厉害,但是还是存在哈希冲突的问题,极端情况下可能还是需要查找N次;

在哈希表的设计之后,为了可以用更少的内存去存储大数据,使用一个位(1 bit = 8 byte)来表示一个数据,就有了哈希位图

布隆过滤器,就是使用多个哈希表,对于同一个数据,对多个哈希表计算的位置进行插入,所以说时间复杂度就是哈希表的个数,但是他对于数据,所判断的结果只能是可能存在,或者一定不存在


B 树的由来

对于上面的数据结构,当我们的数据都在磁盘中的时候,因为数据的访问速度CPU -> 内存 -> 磁盘IO。如果我们的数据在磁盘中存储,当使用红黑树这种数据结构的时候

1000 数据,最多需要读取磁盘 10 次
100w 数据,最多需要读取磁盘 20 次
10亿 数据,最多需要读取磁盘 30 次

这样看似很快,但是因为数据是存储在磁盘中,所以读取数据的速度相对来说还是很慢的,他的时间主要花费在了磁盘中的跳转中,因为地址都是随机,不连续的。
在这里插入图片描述
这就需要一种新的数据结构来表示,那些大佬们就设计出了B树,从此心中有了一个B数

B- 树

1970年,R.Bayer和E.mccreight 提出了一种适合外查找的树,它是一种平衡的多叉树,称为B树或者B-树(不是B减树)。一棵M阶(M>2)的B树,是一棵平衡的M路平衡搜索树,可以是空树或者满足一下性质:

  1. 根节点至少有两个孩子
  2. 每个非根节点至少有M/2(上取整)个孩子,至多有M个孩子
  3. 每个非根节点至少有M/2-1(上取整)个关键字,至多有M-1个关键字,并且以升序排列
  4. key[i]和key[i+1]之间的孩子节点的值介于key[i]、key[i+1]之间
  5. 所有的叶子节点都在同一层

他的大概思路就是,还是采用的树形数据结构,还是那个熟悉的搜索树的思路(比自己大的在右子树,比自己小的在左子树)。与之不同的是,对于树的每一个结点,以前的规定是只能存储一个数据,但是现在就让他可以存储多个数据,这样不就减少了频繁访问内存的次数。

在这里插入图片描述

然后对于他的孩子节点,比如说我们定义一个节点可以存储 M 个数据,那么就会产生 M+1 个范围,也就是 M + 1 个子节点的位置
在这里插入图片描述
对于B-树节点的描述,我们可以这样来

    template<class T,int M = 3>
    struct BTreeNode {
            T               _keys[M];   //存放元素
        BTreeNode<T,M>*     _pSub[M+1]; //存放孩子节点,比数据多一个
        BTreeNode<T,M>*     _pParent;   //存放父亲节点,分裂后需要向上调整
            size_t          _size;      //当前有效元素个数

        BTreeNode()
            :_pParent(nullptr),_size(0) {
                //初始化每个孩子
                for(size_t i = 0; i < M; i++) {
                    _pSub[i] = nullptr;
                    _keys[i] = T();
                }
                _pSub[M] = nullptr;
            }
        ~BTreeNode() {
            _pParent = nullptr;
            for(size_t i = 0; i <= _size; i++) {
                _pSub[i] = nullptr;
            }
            _size = 0;
        }
    };

B树的实现

在这里插入图片描述
先对整个B树进行一个描述,对于他的节点类型,我们使用typedef进行一个简单的描述,bNode

template<class T,int M = 3>
    class BTree {
    public:
        typedef BTreeNode<T,M> bNode;
        BTree() 
            :_root(nullptr)
        {}

        ~BTree() {
            std::cout<< " clear " << std::endl;
            clear(_root);
            std::cout<<std::endl;
        }

        // B数的查找元素,返回值为元素所在的节点,以及元素在节点中的位置
        pair<bNode*,int> Find(const T& key);

        // 中序遍历
        void Show();
        // 插入元素
        bool Insert(const T& key);
    private:
        // 中序遍历
        void _InorderBTree(bNode* root);

        // 插入排序,插入一个key
        void _InsertKey(bNode* cur,const T& key,bNode* sub);
        // 清理函数
        void clear(bNode* root);
    private:
        bNode* _root;
    };

因为他的本质还是一个二叉树的形式,那么比较简单的一部分就是他的打印了,按照他的设计来看,那么他的中序遍历的结果就是一个有序的集合

遍历

在这里插入图片描述

对于遍历,我们采用中序遍历的方式

	public:
		// 中序遍历
        void Show() {
            _InorderBTree(_root);
        }
    private:
        // 中序遍历
        void _InorderBTree(bNode* root) {
            if(root == nullptr) return ;
            for(size_t i = 0; i < root->_size; i++) {
                _InorderBTree(root->_pSub[i]);
                std::cout<< root->_keys[i] << " ";
            }
            _InorderBTree(root->_pSub[root->_size]);
        }

查找

对于查找而言,维护两个节点的指针,父亲节点和对应的孩子节点。规定返回值是一个pair的数组,其中一个值是查找数据所在的节点,另一个就是查找数据在这个节点中的下标。

我们规定如果下标为-1,那么就说明这次查找没有找到,也就是这个数据不存在

        // B数的查找元素,返回值为元素所在的节点,以及元素在节点中的位置
        pair<bNode*,int> Find(const T& key) {
            bNode* cur      = _root;
            bNode* parent   = nullptr;
            int    pos      = 0;

            while(cur) {
                pos = 0;
                while(pos < cur->_size) {
                    if(key == cur->_keys[pos])  {
                        return make_pair(cur,pos);
                    } else if (key < cur->_keys[pos]) {
                        break;  //B数节点按升序排列
                    }
                    //没找到顺序后延
                    pos++;
                }
                parent = cur;
                cur = cur->_pSub[pos];
            }
            //没有找到
            return make_pair(parent,-1);
        }

插入

先用一组数据来模拟一下插入的过程

53, 139, 75, 49, 145, 36, 101 构建一个 M = 3 的B树

在这里插入图片描述

插入53,139后

在这里插入图片描述

插入75 (调整)

  • 插入的过程
    在这里插入图片描述
  • 向上调整
    在这里插入图片描述

插入49,145

在这里插入图片描述

插入36

  • 没有调整前
    在这里插入图片描述
  • 调整后
    在这里插入图片描述

插入101

  • 没有进行调整前
    在这里插入图片描述
  • 第一次调整后(根节点不满足条件了)
    在这里插入图片描述
  • 第二次调整
    在这里插入图片描述

总结

  1. 如果树为空,直接插入新节点中,该节点为树的根节点
  2. 树非空,找待插入元素在树中的插入位置 (找到的插入节点位置一定在叶子节点中)
  3. 检测是否找到插入位置(假设树中的key唯一,即该元素已经存在时则不插入,find函数的第二个返回值为-1)
  4. 按照插入排序的思想将该元素插入到找到的节点中
  5. 检测该节点是否满足B-树的性质:即该节点中的元素个数是否等于M,如果小于则满足
  6. 如果插入后节点不满足B树的性质,需要对该节点进行分裂:
    先申请新节点,再找到该节点的中间位置,再将该节点中间位置右侧的元素以及其孩子搬移到新节点中,最后将中间位置元素以及新节点往该节点的双亲节点中插入,即继续4的循环
  7. 如果向上已经分裂到根节点的位置,插入结束

在这里插入图片描述

        // 插入元素
        bool Insert(const T& key) {
            if (_root == nullptr) {
                _root = new bNode();
                _root->_keys[0] = key;
                _root->_size++;
                return true;
            }

            // 先找到插入位置
            pair<bNode*,int> ret = Find(key);
            if (-1 != ret.second) {
                // 插入的该元素已经存在,不需要插入
                return false;
            }

            T tKey = key;
            bNode* tNode = nullptr;     //孩子节点
            bNode* cur   = ret.first;   //插入位置节点
            while (true) {
                _InsertKey(cur,tKey,tNode);
                if (cur->_size < M) {
                    return true;    //插入成功
                }

                // 当前节点已经满了,需要向上分裂
                tNode = new bNode();
                int mid = M>>1; //向上分裂一半
                for (size_t i = mid+1; i < cur->_size; i++) {
                    tNode->_keys[tNode->_size] = cur->_keys[i];
                    tNode->_pSub[tNode->_size++] = cur->_pSub[i];

                    if(cur->_pSub[i] != nullptr) {
                        cur->_pSub[i]->_pParent = tNode;
                    }
                }
                // 孩子节点比关键字多一个
                tNode->_pSub[tNode->_size] = cur->_pSub[cur->_size];
                if (cur->_pSub[cur->_size] != nullptr) {
                    cur->_pSub[cur->_size]->_pParent = tNode;
                }

                // 更新 cur 节点剩余节点的个数
                cur->_size -= tNode->_size + 1;

                //向上分裂
                if (cur == _root) {
                    _root = new bNode();
                    _root->_keys[0] = cur->_keys[mid];
                    _root->_pSub[0] = cur;
                    _root->_pSub[1] = tNode;
                    _root->_size = 1;
                    cur->_pParent = tNode->_pParent = _root;
                    return true;
                } else {
                    // 继续循环进行插入
                    tKey = cur->_keys[mid];
                    cur = cur->_pParent;
                }
            }
            return true;
        }

总结,结果验证

#include "head.hpp"
#include <time.h>

void test() {
    srand((int)time(0));
    dcl::BTree<int> bt;
    for (size_t i = 0; i < 100; i++) {
        bt.Insert(rand() % 1000);
    }

    bt.Show();
    std::cout<<std::endl;
}

int main() {

    test(); 
    return 0;
}

采用模拟大数据的形式,以随机数来对结果进行验证,主要是插入和打印,以及结果的释放,是否有内存泄漏
在这里插入图片描述

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值