Use rust programming language to create a b+ tree.

Overview

B+树实现细节

目录结构

./src
├── index
│   ├── b_plus_tree.rs
│   └── mod.rs
├── iterator
│   ├── b_plus_tree_iterator.rs
│   └── mod.rs
├── main.rs
└── page
    ├── b_plus_tree_page.rs
    └── mod.rs

m阶B+树的定义

B+树是B树的一种变形形式,m阶B+树满足以下条件:

  • 每个节点最多有m个孩子,至多有m-1个关键字。
    • 意味着对于内部节点,最多只有m-1个关键字,最多可以有m个孩子(也就是m个子节点指针)。
    • 意味着对于叶子节点,最多可以有m-1个关键字和孩子(也就是m-1个子节点指针,对于叶子节点来说,关键字和孩子节点指针是一一对应的)。
  • 除根节点和叶子节点外,每个节点至少有(m + 1) / 2个孩子,也就是说至少有(m - 1) / 2个关键字。
  • 如果根节点不为空,根节点至少有2个孩子。
  • 所有叶子节点增加一个链表指针串联起所有的叶子节点,所有关键字都在叶子节点出现。

举例,当m=3的时候,m阶B+树满足以下条件:

  • 内部节点最多只有2个关键字,最多可以有3个节点指针。
  • 叶子节点最多可以有2个关键字,最多可以有2个节点指针,因为叶子节点的关键字和节点指针是一一对应的。
  • 如果根节点不为空,根节点至少有2个孩子。
  • 所有叶子节点增加一个链表指针串联起所有的叶子节点,所有关键字都在叶子节点出现。

B+树节点布局

B+树的节点分为两种,一种是内部节点(internal page),另一种是叶子节点(leaf page)

内部节点

如图所示,下图为B+树内部节点的布局,可以很容易看出,key相对于value来说少一个。也就是说,在内部节点中,关键字相对于子节点指针来说少一个,而在程序中,我们一般用Vector来表示这种关系,Vector中每个item为一个kv键值对,key代表提供索引的关键字,而value代表指向子节点的指针。由于关键字相对于子节点来说少一个,因此下标为0位置的key一般不用管它,也就是第0个位置的key不提供关键字检索功能。 image.png

叶子节点

如图所示,下图为B+树叶子节点的布局,可以很容易看出,叶子节点的key与value是一一对应的。同时还存在一个指向下一个叶子节点的next指针,将所有的叶子节点串联成为一个链表。通过将叶子节点串联成一个链表,B+树就可以很方便的实现区间范围检索功能。 image.png

B+树节点的代码实现

B+树的节点分为两种,一种是内部节点(internal page),另一种是叶子节点(leaf page)。这里利用rust中的枚举来体现这两类节点类型。

#[derive(PartialEq)]
pub enum BPlusTreePageType {
    InvalidIndexPage,
    InternalPage,
    LeafPage
}

内部节点和叶子节点的公共类

由于Rust语言不太好实现OOP中的继承,因为我在代码中采用了聚合(组合)的方式,将内部节点和叶子节点组合成为一个公共类BPlusTreePage。 这边利用type关键字定义了3个重命名变量,这边简单解释一下:

  • Page代表子节点指针,由于子节点指针可能为空,因此最外层用Option包裹。由于子节点可能需要在代码中共享传递,因此需要再利用Rc智能指针包一层,由于父节点需要通过子节点指针来修改子节点中的值,所以需要为子节点指针提供内部可变性,因此最里面用RefCell包一层。
  • RcPage代表不为空的子节点指针。在代码编写过程中,很多操作传递的子节点指针一点不为空,而这里的Option显然多余了,于是我们又抽象出一个RcPage代表绝对不为空的子节点指针。
pub type Page = Option<Rc<RefCell<BPlusTreePage>>>;
pub type RcPage = Rc<RefCell<BPlusTreePage>>;
pub type SizeT = usize;

pub struct BPlusTreePage {
    page_id_: usize,
    page_type_: BPlusTreePageType,
    max_size_: SizeT,
    page_data_: Vec<MappingType>,
    parent_page_: Page,
    next_page_: Page
}
属性 类型 备注
page_id_ usize 节点ID
page_type_ BPlusTreePageType 节点类型(内部节点/叶子节点)
max_size_ SizeT 该节点的最大容量
page_data_ Vec 该节点存储的数据
parent_page_ Page 该节点的父节点
next_page_ Page 如果该节点为叶子节点,则代表该叶子节点的下一个叶子节点

节点存储KV数据的MappingType类型

如上所述,我们采用Vector来存储B+树的节点中的简直对结构。其中Vector中的类型即为下面的MappingType类型,它包含一个key和value。追求方便,这边我就简单将键和值的类型都定为i32,后续可以利用范型来实现B+树类型的定制。

#[derive(Clone, Default, PartialEq)]
pub struct MappingType {
    pub key: i32,
    pub value: ValueType,
}

考虑到内部节点和叶子节点的value类型并不一样,内部节点的value类型为子节点指针,而叶子节点的value类型为B+树所存储的值。于是我利用名为ValueType的枚举类型来表示这两种value类型,如下所示。

#[derive(Clone)]
pub enum ValueType {
    Page(Page), // 指代page
    Value(Option<i32>) // 指代真正的value
}

节点页面部分方法说明

get_min_size方法

首先get_min_size是一个非常重要的方法,也是一个及其容易出错的方法。顾名思义,该方法显然是获取当前节点页面能存储最少键值对的数量。根据上文所述的B+树定义,分为以下几种情况:

  • 如果当前页面为根节点:
    • 如果当前根节点为内部节点,对于内部节点来说最少要有2个键值对(最少可以只有一个关键字,因为下标为0的关键字一般弃用)。
    • 如果当前根节点为叶子节点,对于叶子节点来说最少要有1个键值对。
  • 如果当前页面不为根节点:
    • 如果当前页面为内部节点,那么最少要有(m + 1) / 2个键值对(当m为3时,内部节点最少要有2个键值对,也就是最少要有两个指向子节点的指针,最少只有1个关键字)。
    • 如果当前页面为叶子节点,那么最少要有(m - 1) / 2个键值对(当m为3时,叶子节点最少要有1个键值对)。
pub fn get_min_size(&self) -> SizeT {
    return if self.is_root_page() {
        if self.is_internal_page() {
            2
        } else if self.is_leaf_page() {
            1
        } else {
            0
        }
    } else {
        if self.is_internal_page() {
            (self.max_size_ + 1) / 2
        } else if self.is_leaf_page() {
            self.max_size_ / 2
        } else {
            0
        }
    }
}

lookup方法

lookup方法一般用于在节点页面中,通过传入一个key,找到该key对应的value。由于B+树节点的key天然具有排序的属性,因此可以采用二分法来加速键值对的查找。可以自己手写upper bound算法,或者调用Vector提供的binary_search方法(使用这种方式需要实现PartialEqEqTrait)。

pub fn lookup(&self, key: i32) -> ValueType {
    if self.is_internal_page() {
        let mut left = 1;
        let mut right = self.get_size() - 1;

        while left <= right {
            let mid = left + (right - left) / 2;
            if self.key_at(mid) > key {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        let target_index = left;
        assert!(target_index - 1 >= 0);
        self.value_at(target_index - 1)
    } else if self.is_leaf_page() {
        let target_index = self.key_index(key);
        if target_index == self.get_size() || self.key_at(target_index) != key {
            ValueType::Value(None)
        } else {
            self.page_data_[target_index].value.clone()
        }
    } else {
        ValueType::Value(None)
    }
}

move_half_to方法

在B+树中有这么一种情况,某个页面插入一个键值对后它的size超过来max_size,此时需要new一个新的页面来分担部分键值对,因此需要调用move_half_to方法来将当前页面一般的键值对转移到新的页面中去。

注意:转移的value如果是子节点指针的话,不要忘记修改子节点的parent_page属性了,因为键值对转移后,子节点的父节点就变成了新new出来的页面了。

image.png

pub fn move_half_to(&mut self, recipient: RcPage) {
    let start_index = self.get_min_size();
    let pre_size = self.get_size();
    let move_num = pre_size - start_index;
    let mut moved_items = self.page_data_.split_off(start_index);
    for item in &mut moved_items {
        match &mut item.value {
            ValueType::Page(child_page) => {
                (*child_page).as_ref().unwrap().borrow_mut().parent_page_ = Some(recipient.clone());
            }
            ValueType::Value(_) => {
                // todo nothing!
            }
        }
    }
    recipient.borrow_mut().page_data_.append(&mut moved_items);
    assert_eq!(pre_size - move_num, self.get_size());
}

move_all_to方法

在B+树中还有一种情况,如果某个页面因为删掉一个键值对后,其size小于min size,同时与它相邻的节点页面也没有足够的键值对能够分配,也就是说,当前页面的size + 相邻页面的size <= max_size�,此时这个页面也就没有存在的必要了。因为该页面需要调用move_all_to方法将当前页面剩余的键值对全部移动到它相邻的节点页面中去。

注意:转移的value如果是子节点指针的话,不要忘记修改子节点的parent_page属性了,因为键值对转移后,子节点的父节点就变成了新new出来的页面了。

image.png

pub fn move_all_to(&mut self, recipient: RcPage, middle_key: i32) {
    if self.is_internal_page() {
        self.set_key_at(0, middle_key);
    }

    let mut moved_items = self.page_data_.to_vec();
    self.page_data_.clear();
    for item in &mut moved_items {
        match &mut item.value {
            ValueType::Page(child_page) => {
                (*child_page).as_ref().unwrap().borrow_mut().parent_page_ = Some(recipient.clone());
            }
            ValueType::Value(_) => {
                // todo nothing!
            }
        }
    }
    recipient.borrow_mut().page_data_.append(&mut moved_items);
}

move_first_to_end_of方法和move_last_to_front_of方法

在B+树中还有一种情况,如果某个页面因为删掉一个键值对后,其size小于min size,但与它相邻的页面有足够的键值对能借给当前页面。也就是说,当前页面的size + 相邻页面的size > max_size�,这个时候就可以调用move_first_to_end_of方法或者move_last_to_front_of方法,来实现键值对在两个页面之间的重分配。

注意:转移的value如果是子节点指针的话,不要忘记修改子节点的parent_page属性了,因为键值对转移后,子节点的父节点就变成了新new出来的页面了。

image.png

pub fn move_first_to_end_of(&mut self, recipient: RcPage, middle_key: i32) {
    if self.is_internal_page() {
        self.set_key_at(0, middle_key);
    }
    assert!(self.page_data_.len() > 0);
    let mut first_item = self.page_data_.remove(0);
    if self.is_internal_page() {
        match &mut first_item.value {
            ValueType::Page(child_page) => {
                (*child_page).as_ref().unwrap().borrow_mut().parent_page_ = Some(recipient.clone());
            }
            ValueType::Value(_) => {
                // todo nothing!
            }
        }
    }
    recipient.borrow_mut().page_data_.push(first_item);
}

pub fn move_last_to_front_of(&mut self,  recipient: RcPage, middle_key: i32) {
    recipient.borrow_mut().set_key_at(0, middle_key);
    assert!(self.page_data_.len() > 0);
    let mut last_item = self.page_data_.pop().unwrap();
    if self.is_internal_page() {
        match &mut last_item.value {
            ValueType::Page(child_page) => {
                (*child_page).as_ref().unwrap().borrow_mut().parent_page_ = Some(recipient.clone());
            }
            ValueType::Value(_) => {
                // todo nothing!
            }
        }
    }
    recipient.borrow_mut().page_data_.insert(0, last_item);
}

B+树的实现

通过上述代码实现B+树的节点页面后,我们就可以开始构建B+树了。对于一般的B+树来说,不可或缺的元素包括internal_max_size_leaf_max_size_root_page_等。也就是说,一个B+树一定要有内部节点最大容量和叶子节点最大容量,二者可以相同也可以不同,但一般来说是相同的。同时还需要存储B+树的根节点,B+树的查找、插入和删除操作都是从根节点开始的。

pub struct BPlusTree {
    index_name_: String,
    internal_max_size_: SizeT,
    leaf_max_size_: SizeT,
    root_page_: Page
}
属性 类型 备注
index_name_ String B+树名称
internal_max_size_ SizeT B+树内部节点容量
leaf_max_size_ SizeT B+树叶子节点容量
root_page_ Page B+树的根节点

B+树部分方法说明

find_leaf_page方法

顾名思义,该方法的作用就是查找叶子节点页面。也就是说,该方法通过传入key,返回key所在的叶子节点页面。这个方法通过一个while循环实现,如果当前节点为内部节点,则调用当前节点的lookup方法得到key所对应的value**(当前节点页面可以能没有key,但因为lookup方法是upper bound算法,返回比第一个大于等于key的位置即可)**,也就得到了子节点页面的指针,如此一直循环到节点页面为叶子节点为止。返回最后的叶子节点页面即可。

fn find_leaf_page(&self, key: i32, operation: Operation, left_most: bool, right_most: bool) -> Page {
    if self.root_page_.is_none() {
        return None
    }

    let mut cur_page = self.root_page_.clone().unwrap();

    while cur_page.borrow().is_internal_page() {
        let child_page;
        if left_most {
            child_page = cur_page.borrow().value_at(0);
        } else if right_most {
            child_page = cur_page.borrow().value_at(cur_page.borrow().get_size() - 1);
        } else {
            child_page = cur_page.borrow().lookup(key);
        }
        assert!(child_page != ValueType::Page(None));

        if let ValueType::Page(page) = child_page {
            cur_page = page.unwrap();
        } else {
            unreachable!();
        }
    }

    Some(cur_page)
}

split方法

顾名思义,这个方法用于节点页面的分裂。当一个页面的size >= max size,则以为该节点页面容量已经爆了,需要分裂。而分裂的方法很简单,就是上面所提到的move_half_to方法。先new一个新的节点页面,然后将当前节点一般的键值对移动到新的节点页面中。

fn split(&mut self, cur_page: RcPage) -> Page {
    if cur_page.borrow().is_internal_page() {
        let new_page = BPlusTreePage::new(InternalPage, self.internal_max_size_, cur_page.borrow().get_parent_page());
        cur_page.borrow_mut().move_half_to(new_page.clone());
        return Some(new_page);
    } else if cur_page.borrow().is_leaf_page() {
        let new_page = BPlusTreePage::new(LeafPage, self.leaf_max_size_, cur_page.borrow().get_parent_page());
        cur_page.borrow_mut().move_half_to(new_page.clone());
        return Some(new_page);
    }
    None
}

insert_into_parent方法

insert_into_parent方法是一个递归方法,用于递归地向parent插入新的用于索引的关键字。如图所示,当一个节点overflow后,首先执行上述的split操作,然后还需调用insert_into_parent方法,将最中间的键值对插入到父节点的相应位置。如果插入新的键值对后父节点也overflow,父节点递归的执行split方法insert_into_parent方法,进行同样的操作,直到所有的节点都安全为止。 image.png image.png image.png

fn insert_into_parent(&mut self, old_page: RcPage, middle_key: i32, new_page: RcPage) {
    if old_page.borrow().is_root_page() {
        let new_root = BPlusTreePage::new(InternalPage, self.internal_max_size_, None);
        new_root.borrow_mut().create_new_root(old_page.clone(), middle_key, new_page.clone());
        old_page.borrow_mut().set_parent_page(Some(new_root.clone()));
        new_page.borrow_mut().set_parent_page(Some(new_root.clone()));
        self.root_page_ = Some(new_root);
        return;
    }

    let parent_page = old_page.borrow().get_parent_page().unwrap();
    let new_size = parent_page.borrow_mut().insert_node_after(old_page.clone(), middle_key, new_page.clone());
    // TODO
    new_page.borrow_mut().set_parent_page(Some(parent_page.clone()));

    // -1是去掉下标为0的item
    if new_size - 1 < self.internal_max_size_ {
        return;
    }

    let new_parent_sibling_node = self.split(parent_page.clone()).unwrap();
    let middle_key = new_parent_sibling_node.borrow().key_at(0);
    self.insert_into_parent(parent_page.clone(), middle_key, new_parent_sibling_node.clone());
}

coalesce方法

coalesce合并的意思。也就是说,当某个节点页面的size < min size,并且与它相邻的节点也没有多余的节点借个当前节点页面,这个时候就需要执行合并操作,将当前节点页面和它的相邻页面合并成为一个节点页面,如图所示。 image.png

fn coalesce(&mut self, neighbor_page: &mut RcPage, cur_page: &mut RcPage, parent_page: RcPage, index: usize) -> bool {
    let mut key_index = index;

    if index == 0 {
        key_index = 1;
        mem::swap(cur_page, neighbor_page);
    }

    let middle_key = parent_page.borrow().key_at(key_index);
    cur_page.borrow_mut().move_all_to((*neighbor_page).clone(), middle_key);
    (*neighbor_page).borrow_mut().set_next_page(cur_page.borrow().get_next_page());

    parent_page.borrow_mut().remove(key_index);
    return self.coalesce_or_redistribute(parent_page.clone());
}

## B+树迭代器的实现
由于B+树的叶子节点具有next指针指向下一个叶子节点,相当于所有的叶子节点构成了一个链表。因此B+树的叶子节点天然具有迭代属性,于是可以构建一个迭代器来实现叶子节点的迭代。如下所示,`BPlusTreeIter`仅有两个属性,`cur_page_`代表当前迭代器指向的叶子节点,`index_`代表当前迭代器迭代到叶子节点的Vector 中下标为index的键值对。显然,当迭代器的`index_ == cur_page.size`时,需要将cur_page指针指向它的下一个节点,同时index更新为0![image.png](https://cdn.nlark.com/yuque/0/2022/png/22571229/1672327729244-6e8014a3-b894-4c3c-a42e-5f78cbee270d.png#averageHue=%23f5f5f4&clientId=uce36f9f2-b33a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=424&id=ufbdf9090&margin=%5Bobject%20Object%5D&name=image.png&originHeight=847&originWidth=2988&originalType=binary&ratio=1&rotation=0&showTitle=false&size=614703&status=done&style=none&taskId=u80ee889b-ef8c-40d2-88a0-08f73644ce6&title=&width=1494)
```rust
pub struct BPlusTreeIter {
    cur_page_: Page,
    index_: usize
}

B+树迭代器所具有的方法

impl BPlusTreeIter {
    pub fn new(cur_page: Page, index: usize) -> Self {
        BPlusTreeIter {
            cur_page_: cur_page,
            index_: index
        }
    }
}

impl Iterator for BPlusTreeIter {
    type Item = i32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.cur_page_.is_none() {
            return None;
        }

        let cur_page = self.cur_page_.as_ref().unwrap();
        if cur_page.borrow().get_next_page().is_none() && self.index_ == cur_page.borrow().get_size() {
            return None;
        }

        if cur_page.borrow().get_next_page().is_some() && self.index_ == cur_page.borrow().get_size() {
            let next_page = cur_page.borrow().get_next_page();
            match next_page {
                None => {
                    self.cur_page_ = None;
                }
                Some(next_page) => {
                    self.cur_page_ = Some(next_page);
                }
            }
            self.index_ = 0;
        }

        return if let ValueType::Value(value) = self.cur_page_.as_ref().unwrap().borrow().value_at(self.index_) {
            self.index_ += 1;
            value
        } else {
            None
        }
    }
}
You might also like...
Game Boy Emulator written in Rust, as a way to fully grasp the Rust programming language

Flan's Game Boy Emulator Game Boy Emulator written in Rust, as a way to get hands-on with the Rust programming language, and creating a proper project

A minimal version of 'grep' implemented in Rust. Exercise in the "The Rust Programming Language" book.

Minigrep - A simple grep-like tool implemented in Rust This simple CLI tool searches for a given pattern in a specified file and as a result, it print

Nixt is an interpreted programming language written in Rust

Nixt Nixt is an interpreted lisp inspired programming language written in Rust Index About Examples Installation Build About Nixt goal is to provide a

a function programming language for real world applications made in rust

a function programming language for real world applications made in rust

Rust implementation of µKanren, a featherweight relational programming language.

µKanren-rs This is a Rust implementation of µKanren, a featherweight relational programming language. See the original Scheme implementation here for

This repository contains the source of "The Rust Programming Language" book.

The Rust Programming Language This repository contains the source of "The Rust Programming Language" book. The book is available in dead-tree form fro

A short exercise to introduce people to the Rust programming language

Searching primes by brute force This code is ment to be an exercice to teach rust and give a first impression on how to work with the language during

The Rust Programming Language, Chapter 8, Exercise 1

Rust Common Collections - Exercise 1 In the book The Rust Programming Language by Steve Klabnik and Carol Nichols, chapter 8 - Common Collections - pr

The Ribbon Programming Language, made in Rust.

The Ribbon Programming Language (WIP) This language is designed to be quick to write and is heavily inspired by Rust, which is also the language it wa

Owner
yangshijie
yangshijie
Manas project aims to create a modular framework and ecosystem to create robust storage servers adhering to Solid protocol in rust.

मनस् | Manas Solid is a web native protocol to enable interoperable, read-write, collaborative, and decentralized web, truer to web's original vision.

Manomayam 17 Oct 5, 2023
Game development practices with Rust programming language. I want to use different crates for this.

Hazır Oyun Motorlarını Kullanarak Rust Dili Yardımıyla Oyunlar Geliştirmek Rust programlama dilinde oyun geliştirmek için popüler birkaç hazır çatıyı

Burak Selim Senyurt 16 Dec 27, 2022
Small, clean, easy to use programming language

Thistle A modern, simplistic multi-paradigm language supporting object-oriented features Hello World! import IO object Main def main(): unit

null 7 Apr 13, 2022
A repository for showcasing my knowledge of the Rust programming language, and continuing to learn the language.

Learning Rust I started learning the Rust programming language before using GitHub, but increased its usage afterwards. I have found it to be a fast a

Sean P. Myrick V19.1.7.2 2 Nov 8, 2022
Nyah is a programming language runtime built for high performance and comes with a scripting language.

?? Nyah ( Unfinished ) Nyah is a programming language runtime built for high performance and comes with a scripting language. ??️ Status Nyah is not c

Stacker 3 Mar 6, 2022
lelang programming language is a toy language based on LLVM.

lelang leang是一门使用Rust编写,基于LLVM(inkwell llvm safe binding library)实现的编程语言,起初作为课程实验项目,现在为个人长期维护项目。 Target Features 支持8至64位的整形类型和32/64位浮点 基本的函数定义,调用,声明外部

Aya0wind 5 Sep 4, 2022
Embeddable tree-walk interpreter for a "mostly lazy" Lisp-like scripting language.

ceceio Embeddable tree-walk interpreter for a "mostly lazy" Lisp-like scripting language. Just a work-in-progress testbed for now. Sample usage us

Vinícius Miguel 7 Aug 18, 2022
A simpler and 5x faster alternative to HashMap in Rust, which doesn't use hashing and doesn't use heap

At least 5x faster alternative of HashMap, for very small maps. It is also faster than FxHashMap, hashbrown, ArrayMap, and nohash-hasher. The smaller

Yegor Bugayenko 12 Apr 19, 2023
clone of grep cli written in Rust. From Chapter 12 of the Rust Programming Language book

minigrep is a clone of the grep cli in rust Minigrep will find a query string in a file. To test it out, clone the project and run cargo run body poem

Raunak Singh 1 Dec 14, 2021
The Rust Compiler Collection is a collection of compilers for various languages, written with The Rust Programming Language.

rcc The Rust Compiler Collection is a collection of compilers for various languages, written with The Rust Programming Language. Compilers Language Co

null 2 Jan 17, 2022