1. 程式人生 > >路漫漫其修遠兮,吾將上下而求索!

路漫漫其修遠兮,吾將上下而求索!

B樹是為磁碟和其他直接存取的輔助儲存裝置而設計的一種平衡搜尋樹。B樹類似於紅黑樹,但它們在降低I/O運算元方面要更好一些。許多資料庫系統使用B樹或者B樹的變種來儲存資訊。

B樹與紅黑樹的不同之處在於B樹的結點可以有很多孩子,從數個到樹千個。也就是說,一個B樹的“分支因子”可以相當大,儘管它通常依賴於所使用的磁碟單元的特性。B樹類似於紅黑樹,就是每棵含有n個結點的B數高度為O(lgn)。然而,一棵B樹的嚴格高度可能比一棵紅黑樹的高度要小許多,這是因為它的分支因子。

1 B樹的定義

如圖所示就是一棵B樹,一棵B樹T是具有以下性質的有根樹(根為T.root):

  1. 每個結點x有下面屬性:
    1. x.n當前儲存在結點x中的關鍵字個數
    2. x.n個關鍵字本身x.key1,x.key2,…,x.keyn,以非降序存放,使得x.key1 ≤ x.key2 ≤ ... ≤ x.keyn
    3. x.leaf,一個布林值,如果x是葉結點,則為true;如果x是內部結點,則為false
  2. 每個內部結點x還包含x.n+1個指向其孩子的指標x.c1,x.c2,…,x.cn+1。葉結點沒有孩子,所以它們的Ci屬性沒有定義。
  3. 關鍵字x.key1對儲存在各子樹中的關鍵字範圍加以分割:如果ki為任意一個儲存在x.ci為根的子樹中的關鍵字,那麼k1 ≤ x.key1 ≤ k2 ≤ x.key2 ≤ ... ≤ x.keyn ≤ kn+1
  4. 每個葉結點具有相同的深度,即樹的高度h。
  5. 每個結點所包含的關鍵字個數有上限和下限。用一個被稱為B樹的最小度數的固定整數t > 1來表示這些界限:
    1. 除了根結點以外每個結點必須至少有t-1個關鍵字。因此,除了根結點以外的每個內部結點至少有t個孩子。如果樹非空,根結點至少有一個關鍵字。
    2. 每個結點至多包含2t-1個關鍵字。因此,一個內部結點至多可有結點恰好有2t-1個關鍵字時,稱該結點是滿的

B樹的高度

如果n>0,那麼對任意一棵包含n個關鍵字、高度h、最小度數t>1的B樹T有

2 B樹設計

2.1 B樹的結點YJBTreeNode

根據B樹對於結點的定義設計結點YJBTreeNode。

//
//  YJBTreeNode.swift
// BTree // // CSDN:http://blog.csdn.net/y550918116j // GitHub:https://github.com/937447974/Blog // // Created by yangjun on 15/11/23. // Copyright © 2015年 陽君. All rights reserved. // import Cocoa /// B樹結點 class YJBTreeNode: NSObject { /// 度數,除根結點的其他結點最少t-1,最多2t-2個子結點 var t :Int /// 樹高 var h :Int = 1 /// 是否葉結點,true是,false不是 var leaf :Bool = true /// 關鍵字 var key :[Int] = [] /// 子結點,子結點個數最少t,最多2t-1個子結點 var child :[YJBTreeNode] = [] // MARK: - 初始化 /// 初始化 /// /// - parameter t : 度數,除根結點的其他結點最少t-1,最多2t-2個子結點 /// /// - returns: void init(t :Int) { self.t = t } }

2.2 B樹YJBTree

根據對B樹的定義,我們這裡設計類YJBTree,並可以指定最小度數t

//
//  YJBTree.swift
//  BTree
//
//  CSDN:http://blog.csdn.net/y550918116j
//  GitHub:https://github.com/937447974/Blog
//
//  Created by yangjun on 15/11/23.
//  Copyright © 2015年 陽君. All rights reserved.
//

import Cocoa

/// B樹
class YJBTree {

    /// 根結點
    private var root: YJBTreeNode
    /// 度數,除根結點的其他結點最少t-1,最多2t-2個子結點,預設t最小為3
    private let t: Int

    // MARK: - 初始化B樹
    /// 初始化B樹
    ///
    /// - parameter t : 度數,除根結點的其他結點最少t-1,
    ///                 最多2t-2個子結點,預設t最小為3
    /// - returns: void
    init(t :Int) {
        self.t = t < 3 ? 3 : t
        root = YJBTreeNode(t: self.t)
    }

}

3 B樹上的基本操作

3.1 排序

B樹的資料和二叉搜尋樹相類似,我們可以使用中序遍歷的方式實現,即左-中-右。

// MARK: - 排序
/// 排序輸出
///
/// - returns: [Int]
func sort() -> [Int] {
    return self.sort(self.root)
}

private func sort(node: YJBTreeNode) -> [Int] {
    // 當前為葉子結點
    if node.leaf {
        return node.key
    }
    var list:[Int] = []
    let count = node.key.count
    for i in 0..<count {
        list += self.sort(node.child[i])
        list.append(node.key[i])
    }
    list += self.sort(node.child[count])
    return list
}

3.2 搜尋

在B數中搜索一個key時,需要注意的是key是在node.key中還是在node.child中。

// MARK: - 根據key搜尋所在的樹以及位置
func search(key: Int) ->(node: YJBTreeNode, keyLocation: Int)? {
    return self.search(self.root, key: key)
}

private func search(node: YJBTreeNode, key :Int) ->(node: YJBTreeNode, keyLocation: Int)? {
    var i:Int = 0
    while (i < node.key.count && key > node.key[i]) {
        i++
    }
    if (i < node.key.count && key == node.key[i]) {
        return (node, i)
    } else if node.leaf {
        return nil
    } else {
        // 讀磁碟node.child[i]物件
        return self.search(node.child[i], key: key)
    }
}

3.3 插入key

在B數中插入一個key,其實質是在其葉子結點插入,然後葉子結點裂變生成一棵B樹。

插入key的具體思想如下。

  1. 根結點為葉子結點執行步驟2,為內部結點執行步驟3。
  2. 插入葉子結點,直接插入,葉子滿時,分裂並向上拋數。
  3. 插入內部結點,向下傳遞找到要插入的葉子結點執行步驟2。
  4. 回撥時,遇到向上拋的資料後,重新排列結點。如果結點滿,分裂結點繼續向上拋。
// MARK: - 插入
/// 插入key
///
/// - parameter key : 要插入的key值
///
/// - returns: void
func insert(key: Int) {
    /**
     插入三情況結點
     1. 根結點為葉子結點執行步驟2,為內部結點執行步驟3。
    2. 插入葉子結點,直接插入,葉子滿時,分裂並向上拋數。
    3. 插入內部結點,向下傳遞找到要插入的葉子結點執行步驟2。
    4. 回撥時,遇到向上拋的資料後,重新排列結點。如果結點滿,分裂結點繼續向上拋。
     */
    // 情況1:樹高為1,直接新增葉子結點
    if root.h == 1 {
        root = self.insertLeaf(self.root, key: key)
    } else {
        if let root = self.insert(self.root, key: key) {
            // 情況3,重定向根結點
            self.root = root
        }
    }
}

// MARK: node中插入資料key
private func insert(node: YJBTreeNode, key :Int) -> YJBTreeNode? {
    // 插入演算法:每次插入都是插入葉子結點,分裂時,依次向上插入分裂
    // 當前為葉子結點
    if node.leaf {// 情況2:
        let leaf = self.insertLeaf(node, key: key)
        // 有向上分裂
        if leaf.h == 2 {
            return leaf
        }
        return nil
    } else {// 情況3:
        let index = self.insertIndex(node, key: key)
        // 向下繼續插入
        if let child = self.insert(node.child[index], key: key) { // 遇到拋數
            // 插入key
            node.key.insert(child.key[0], atIndex: index)
            // 替換並插入child
            node.child[index] = child.child[1]
            node.child.insert(child.child[0], atIndex: index)
            // 如果結點滿,分裂結點繼續向上拋
            if node.key.count >= 2 * t - 1{
                return self.splitChild(node)
            }
        }
        return nil
    }
}

// MARK: 葉子結點插入資料
private func insertLeaf(node: YJBTreeNode, key :Int) -> YJBTreeNode {
    let index = self.insertIndex(node, key: key)
    node.key.insert(key, atIndex: index)
    // 判斷是否需要分割
    if node.key.count == 2 * t - 1{
        return self.splitChild(node)
    }
    return node
}

// MARK: 插入輔助方法,尋找要插入的位置
private func insertIndex(node: YJBTreeNode, key :Int) ->Int {
    // key的個數
    let count = node.key.count
    // 有資料
    if count != 0 {
        if key <= node.key[0] {// 最小
            return 0
        } else if node.key[count-1] <= key {// 最大
            return count
        }
        // 中間
        for i in 1 ..< node.key.count {
            if (node.key[i-1] <= key && key <= node.key[i]) {
                return i;
            }
        }
    }
    return 0
}

// MARK: - 分裂結點成為一棵B樹
private func splitChild(node: YJBTreeNode) -> YJBTreeNode {
    // 分割點
    let split = node.key.count / 2
    // 初始化
    let r = YJBTreeNode(t: t)
    r.leaf = false
    let rLeft = YJBTreeNode(t: t)
    let rRight = YJBTreeNode(t: t)
    // 樹高
    r.h = node.h + 1
    rLeft.h = node.h
    rRight.h = node.h
    // 整理r.key
    r.key.append(node.key[split])
    // 整理r.child
    for i in 0..<split {
        rLeft.key.append(node.key[i]) // 左孩子key
    }
    r.child.append(rLeft)
    for i in split+1..<node.key.count {
        rRight.key.append(node.key[i]) // 右孩子key
    }
    r.child.append(rRight)
    // 整理child
    if node.child.count >=  self.t {
        // 有孩子代表是內部結點
        rLeft.leaf = false
        rRight.leaf = false
        // 整理孩子結點的孩子結點
        for i in 0...split {
            rLeft.child.append(node.child[i])
        }
        for i in split+1..<node.child.count {
            rRight.child.append(node.child[i])
        }
    }
    // 返回的r為一棵B樹
    return r
}

這裡使用了方法splitChild,該方法的作用就是key滿足分裂條件時,分裂結點成為一個新的B樹。

3.4 刪除

在B中刪除key時,需要考慮刪除的key是在葉子上,還是在樹的內部。

刪除的核心思想就是:

  1. 找到key,合併其左右子結點成新的B樹。
  2. 判斷B能否替代該key。能夠替代,替換key、left、right;不能替換,替換left,刪除key和right
  3. 巢狀回撥時,遇到key.count==t-2時,做合併當前child、對應的key和相鄰的child;否則不執行任何操作。
// MARK: - 刪除key
func delete(key: Int) {
    if self.root.key.count == 0 { // 樹為空時,直接返回
        return
    }
    // 根據返回的結點判斷是否需要降key操作
    let newRoot = self.delete(self.root, key: key)
    self.root = newRoot.key.count != 0 || newRoot.leaf ? newRoot : newRoot.child.first!
}

private func delete(node: YJBTreeNode, key: Int) -> YJBTreeNode {
    /**刪除思路:
    1. 找到key,合併其左右子結點成新的B樹B
    2. 判斷B能否替代該key。能夠替代,替換keyleftright;不能替換,替換left,刪除keyright
    3. 巢狀回撥時,遇到key.count==t-2時,做合併當前child對應的key和相鄰的child;否則不執行任何操作。
    */
    if node.leaf { // 葉子結點
        for (index, value) in node.key.enumerate() {
            if value == key { // 找到則刪除
                node.key.removeAtIndex(index)
                break
            }
        }
    } else { // 內部結點
        // 巢狀向下
        var index = -1
        let count = node.key.count
        // 尋找key或child
        if key > node.key.last! { // 這裡用到了快速跳躍,先判斷是否跳過
            index = count
        } else {
            for i in 0..<count {
                if node.key[i] == key { // 找到key
                    let bTree = self.mergeChild(node.child[i], right: node.child[i+1])
                    // 先刪除後插入
                    self.replaceNode(node, index: i, bTree: bTree)
                    break
                } else if key < node.key[i] { // 找到child
                    index = i
                    break
                }
            }
        }
        // 巢狀返回
        if index != -1 {
            var bTree = self.delete(node.child[index], key: key)
            if bTree.key.count == self.t-2 { // 降key操作
                let keyIndex = index == count ? index-1 : index
                bTree = self.mergeKeyChild(node.child[keyIndex], key: node.key[keyIndex], right: node.child[keyIndex+1])
                self.replaceNode(node, index: keyIndex, bTree: bTree)
            }
        }
    }
    return node
}

// MARK: B樹替換結點中rightkeyleft
private func replaceNode(var node: YJBTreeNode, index: Int, bTree:YJBTreeNode) {
    if node.h == bTree.h { // 樹高相同,替換keyleftright
        node.key[index] = bTree.key[0]
        node.child[index] = bTree.child[0]
        node.child[index+1] = bTree.child[1]
    } else { // 樹高不同,替換left,刪除keyright
        node.key.removeAtIndex(index)
        node.child[index] = bTree
        node.child.removeAtIndex(index+1)
    }
    // 是否需要合併
    if node.key.count >= 2 * t - 1{
        node = self.splitChild(node)
    }
}

// MARK: 合併左孩子、key、右孩子
private func mergeKeyChild(left: YJBTreeNode, key:Int, right: YJBTreeNode) -> YJBTreeNode {
    // key
    left.key.append(key)
    left.key += right.key
    // child
    left.child += right.child
    // 是否需要合併
    if left.key.count >= 2 * t - 1{
        return self.splitChild(left)
    }
    return left
}

// MARK: 合併左右子結點
private func mergeChild(left: YJBTreeNode, right: YJBTreeNode) -> YJBTreeNode {
    if left.leaf { // 葉子結點
        left.key += right.key
    } else {
        // 合併首位結點
        let bTree = self.mergeChild(left.child.last!, right: right.child.first!)
        // 去掉左child尾,右child首
        left.child.removeLast()
        right.child.removeAtIndex(0)
        if bTree.h == left.h { // 樹高不變時,連key、child
            left.key.append(bTree.key[0])
            left.child += bTree.child
        } else { // 樹高變時,新生成的樹是當前結點的child
            left.child.append(bTree)
        }
        // 連線right.child
        left.key += right.key
        left.child += right.child
    }
    // 是否需要合併
    if left.key.count >= 2 * t - 1{
        return self.splitChild(left)
    }
    return left
}

5 小結

本篇博文講解了B樹的定義、查詢、增加和刪除等功能。B樹是以一種自然的方式推廣了二叉搜尋樹,可以保證在最壞情況下基本動態集合操作的時間複雜度為O(lgn)。
 

其他

原始碼

參考資料

演算法導論

文件修改記錄

時間 描述
2015-11-23 完成B樹的查詢和增加結點的研發
2015-11-24 完成B樹刪除結點的研發,完成《B樹》博文

版權所有