數據結構系列(4)之 B 樹

本文將主要講述另外一種樹形結構,B 樹;B 樹是一種多路平衡查找樹,可是能夠將其理解爲是由二叉查找樹合併而來;它主要用於在不一樣存儲介質之間查找數據的時候,減小 I/O 次數(由於一次讀一個節點,能夠讀取多個數據);java

1、結構概述

B 樹,多路平衡查找樹,即有多個分支的查找樹;如圖所示:node

btree

B 樹主要應用於多級存儲介質之間的查找,圖中的藍色節點爲外部節點,表明下一級存儲介質;綠色節點則爲內部節點;同時咱們將B 樹按照其最大分支樹進行分類,好比圖中的則爲4 階B 樹;緩存

對於 m 階 B 樹(m >= 2):this

  • 外部節點的深度統一相等,葉子節點的深度統一相等,樹高等於外部節點深度;
  • 內部節點不超過(m-1)個關鍵碼,不超過 m 路分支,同時很多於(⌈m /2⌉)路分支,可是根節點最少一路分支便可;
  • 因此 m 階 B 樹又稱爲(⌈m /2⌉,m)樹;
public class BTree<E extends Comparable<? super E>> implements Iterable<E> {
  private Node<E> root;
  private final int order;
  private final int MAX_KEYS;
  private final int MIN_KEYS;
  private int height;
  private int totalSize;

  final class Node<T> {
    Object[] values;
    Node<T>[] children;
    Node<T> parent;
    boolean isLeaf;
    int size;

    Node() {
      this.values = new Object[order];      // 實際只有order-1個關鍵碼,超出時會分裂;
      this.children = new Node[order + 1];  // 一樣+1;
      this.isLeaf = true;
      this.size = 0;
    }
  }
}

2、節點修復

由於B 樹節點的在[ ⌈m/2⌉ - 1, m -1 ]之間,因此在動態插入和刪除的過程當中必定會發生不平衡,下面將介紹修復不平衡的幾種方法;指針

1. 分裂

插入時當節點的關鍵碼超過 m-1 ,就將大節點分爲兩個小節點;如圖:code

btreeinsert

分裂時:blog

  • 將第 ⌊m/2⌋ 個關鍵碼移入父節點;
  • 分紅的兩個節點,則成爲新關鍵碼的左右孩子節點;(須要新增節點,並移動節點信息);
  • 再遞歸的檢查其父節點的關鍵碼是否超過;

實現:遞歸

private void split(Node<E> p) {
  Node<E> parent = p.parent;
  if (parent == null) {        // parent爲null,即當前節點爲root,須要上升高度(惟一會致使樹高度增長的操做)
    parent = new Node<E>();
    parent.isLeaf = false;     // 設置爲非葉子節點
    root = parent;             // 更新root節點
    height++;                  // 高度加1
  }
  
  int mid = (p.size - 1) >>> 1; // 須要上一的關鍵碼
  Node<E> left = new Node<E>(); // 分裂,建立一個新的空節點
  Node<E> right = p;            // 右邊節點爲原來的節點
  left.isLeaf = p.isLeaf;       // 節點是否葉子,取決於分裂前是否葉子。

  // 更新孩子節點的parent指針
  if (!p.isLeaf) {
    for (int i = 0; i <= mid; ++i) {  // 左子樹的孩子應該指向左子樹。
      p.children[i].parent = left;
    }
  }
  parent.insertToNonLeaf((E) p.values[mid], left, right); // 把中間節點插入父節點。

  int i, j;
  // 拷貝右子樹信息到左子樹。
  for (i = 0; i < mid; ++i) {
    left.values[i] = right.values[i];
    left.children[i] = right.children[i];
  }
  left.children[i] = right.children[mid];
  left.size = mid;  // 更新左子樹關鍵字數量

  // 刪除右子樹多餘關鍵字和孩子,由於已經拷貝到左孩子中去了。
  for (i = mid + 1, j = 0; i < right.size; ++i, ++j) {
    right.values[j] = right.values[i];
    right.children[j] = right.children[i];
  }
  right.children[j] = right.children[right.size]; // 更新最後一個孩子節點, 注意奇數j == mid,但偶數不是。。
  right.size = right.size - mid - 1;              // 更新右子樹關鍵字數量
  left.parent = parent;                           // 把子樹的父親節點更新
  right.parent = parent;
  if (parent.size > MAX_KEYS)                      // 若是父親節點也達到最大關鍵字數量,須要遞歸分裂。
    split(parent);
}

int insertToNonLeaf(T key, Node<T> left, Node<T> right) {
    int index = insertIndex(key);
    if (index < 0) return index;
  
    for (int i = size; i > index; --i) {
        values[i] = values[i - 1];
        children[i + 1] = children[i];
    }
    children[index] = left;
    children[index + 1] = right;
    values[index] = key;
    size++;
    return index;
}

2. 旋轉

刪除節點時,可能會致使節點的關鍵碼數量小於 ⌊m/2⌋,此時能夠向他的左孩子或者右孩子,借一個關鍵碼;如圖:ci

btreeremove1

圖中:rem

  • 首先檢查發現沒有左兄弟,而且右兄弟能夠借出
  • 而後左旋轉父節點,父節點的關鍵碼進入補齊,右兄弟的關鍵碼進入父節點;

右孩子富裕時左旋:

private void leftRotate(Node<E> p) {
  Node<E> right = rightSibling(p);   // 獲取右兄弟
  int myRank = rankInChildren(p);    // 獲取在父節點中的秩
  Object oldSeparator = p.parent.values[myRank];
  p.values[p.size] = oldSeparator;
  p.size++;
  Object newSeparator = right.values[0];
  Node<E> child = right.isLeaf ? null : right.children[0];  // 獲取右兄弟中最小的關鍵碼
  int i;
  for (i = 0; i < right.size - 1; ++i) {
    right.values[i] = right.values[i + 1];
    if (!right.isLeaf)
      right.children[i] = right.children[i + 1];
  }
  if (!right.isLeaf) {
    right.children[right.size - 1] = right.children[right.size];
    child.parent = p;
    p.children[p.size] = child;
  }
  right.size--;
  p.parent.values[myRank] = newSeparator;
}

private Node<E> rightSibling(Node<E> p) {
  if (p == null || p.parent == null) // 根節點無兄弟節點
    return null;
  Node<E> parent = p.parent;
  int i = rankInChildren(p);
  if (i >= 0 && i < parent.size) {
    return parent.children[i + 1];
  }
  return null;
}

左孩子富裕時右旋:

private void rightRotate(Node<E> p) {
  Node<E> left = leftSibling(p);
  int myRank = rankInChildren(p);
  Object oldSeparator = p.parent.values[myRank - 1];
  Node<E> child = null;
  if (!left.isLeaf) {
    child = left.children[left.size];
    p.children[p.size + 1] = p.children[p.size];
  }
  
  for (int i = p.size; i >= 1; --i) {
    p.values[i] = p.values[i - 1];  
    if (!p.isLeaf)
      p.children[i] = p.children[i - 1];
  }
  
  if (!left.isLeaf) {
    child.parent = p;
    p.children[0] = child;
  }
  p.values[0] = oldSeparator;
  p.size++;
  Object newSeparator = left.values[left.size - 1];
  left.size--;
  p.parent.values[myRank - 1] = newSeparator;
}

private Node<E> leftSibling(Node<E> p) {
  if (p == null || p.parent == null) return null;
  Node<E> parent = p.parent;
  int i = rankInChildren(p);
  
  if (i >= 1) return parent.children[i - 1];
  return null;
}

3. 合併

當左右孩子的關鍵碼都不足以借出時,則將兩個孩子合併,如圖:

btreeremove2

圖中:

  • 首先左右兄弟都不足以借出
  • 從父節點借得一個關鍵碼;
  • 而後以借得的關鍵碼爲粘合左右兄弟節點;
  • 最後須要檢查父節點是否平衡;

實現:

private void merge(Node<E> p) {
  Node<E> parent = p.parent;
  assert (parent != null);
  Node<E> left = p; // left node 或者是當前節點,即貧困節點,或者是當前節點的左兄弟節點。
  Node<E> right = rightSibling(p);
  if (right == null) {
    left = leftSibling(p);
    right = p;
  }
  int myRank = rankInChildren(left);
  // 把父親節點的Separator下移到須要合併的節點left
  Object separator = parent.values[myRank];
  left.values[left.size] = separator;
  left.size++;
  // 從父親節點中刪除Separator
  for (int i = myRank; i < parent.size - 1; i++) {
    parent.values[i] = parent.values[i + 1];
    parent.children[i + 1] = parent.children[i + 2];

  }
  //FIXME
  parent.values[parent.size - 1] = null;
  parent.children[parent.size] = null;
  parent.size--;
  // 拷貝右節點到左節點
  for (int i = 0; i < right.size; ++i) {
    left.size++;
    left.values[left.size - 1] = right.values[i];
    if (!left.isLeaf) {
      right.children[i].parent = left; // donot forget it.
      left.children[left.size - 1] = right.children[i];
    }
  }
  // 不要忘記最後一個孩子更新。
  if (!left.isLeaf) {
    right.children[right.size].parent = left;
    left.children[left.size] = right.children[right.size];
  }
  // 若是父親節點也貧困了,須要從父親節點從新調整,直到知足平衡或者父親節點就是root節點
  if (parent.size < MIN_KEYS) {
    if (parent.size == 0 && parent == root) {
      root = left;
      root.parent = null;
      height--;
    } else {
      rebalancingAfterDeletion(parent);
    }
  }
}

3、查找

查找時採起逐層查找:

  • 查找不大於目標關鍵碼的最大值;
  • 精確對比是否命中,若沒有命中則深刻孩子節點

實現:

public Node<E> search(E e) {
  Node<E> v = root;
  while (v != null) {       // 逐層查找
    int r = v.search(e);     // 在當前節點中,找到不大於e的最大關鍵碼
    if (r >= 0 && cmp(e, v.values[r]) == 0) {
      return v;
    }
    v = v.children[r + 1];     // 轉入對應子樹——需作I/O,最費時間
  }
  return null;
}

int search(T key) {
  int low = 0;
  int high = size - 1;

  do {
    int mi = (low + high) >> 1;
    if (cmp(key, values[mi]) < 0) {
      high = mi;
    } else {
      low = mi + 1;
    }
  } while (low < high);

  return --low;
}

4、插入

public boolean add(E key) {
  if (key == null) {
    return false;
  }
  if (root == null) {
    root = new Node<E>();
    this.height = 1;
    this.totalSize = 0;
  }
  boolean inserted = insert(key, root);
  if (inserted) {
    ++totalSize;
    ++modCount;
    return true;
  } else {
    return false;
  }
}
  
private boolean insert(E key, Node<E> p) {
  assert (p != null);
  if (!p.isLeaf) { // 老是插入到葉子中,不可能直接插入到內部節點
    int index = p.insertIndex(key); // 獲取插入位置,若是 < 0說明已存在
    if (index < 0) // index < 0 說明key已存在
      return false;
    return insert(key, p.children[index]); // 插入的位置就是孩子的位置
  }
  boolean inserted = p.insertToLeaf(key) >= 0; // p是葉子節點,直接插入。

  if (p.size > MAX_KEYS) { // 若是關鍵字多於最大關鍵字數量,須要分裂節點。
    split(p);
  }
  return inserted;
}
  
int insertToLeaf(T key) {
  int index = insertIndex(key);
  if (index < 0)
    return index;
  for (int i = size; i > index; --i) {// 移動向右key
    values[i] = values[i - 1];
  }
  values[index] = key;
  ++size;
  return index;
}

5、刪除

public boolean remove(E e) {
  if (root == null) {
    return false;
  }
  boolean isRemoved = remove(e, root);
  if (isRemoved) {
    --totalSize;
    ++modCount;
  }
  return isRemoved;
}

private boolean remove(E e, Node<E> p) {
  if (p.isLeaf) { // 刪除的關鍵字在葉子節點中,直接刪除,而後從新調整
    boolean isRemoved = p.deleteFromLeaf(e);
    if (p.size < MIN_KEYS) {
      rebalancingAfterDeletion(p); // rebalances the tree
    }
    return isRemoved;
  }
  int index = p.binarySearch(e);
  if (index < 0) { // 不在吃節點中,遞歸從子樹中查找。
    return remove(e, p.children[-index - 1]); // -index - 1就是插入位置,即孩子節點位置。
  }
  // 刪除的是內部節點,須要尋找左子樹最大節點(或者右子樹中最小節點)做爲新分隔符替換刪除的關鍵字。
  Node<E> leftLeaf = leftLeaf(p, index);// 尋找左子樹最右節點。
  Object candidate = leftLeaf.values[leftLeaf.size - 1];
  //從葉子節點中移除候選節點
  leftLeaf.values[leftLeaf.size - 1] = null;
  leftLeaf.size--;
  //候選節點做爲分隔符替代刪除的節點。
  p.values[index] = candidate;
  //從新調整樹使其平衡。
  if (leftLeaf.size < MIN_KEYS) {
    rebalancingAfterDeletion(leftLeaf);
  }
  return true;
}

boolean deleteFromLeaf(T key) {
  int index = binarySearch(key);
  if (index < 0)
    return false;
  for (int i = index; i < size; ++i) {
    values[i] = values[i + 1];
  }
  this.size--;
  return true;
}
  
private void rebalancingAfterDeletion(Node<E> p) {
  if (p == root) { // 說明p是root節點,不須要處理
    return;
  }

  Node<E> left = leftSibling(p); // 獲取左兄弟
  if (left != null && left.size > MIN_KEYS) { // 左兄弟很富裕, 右旋轉。
    rightRotate(p);
    return;
  }

  Node<E> right = rightSibling(p); // 右兄弟
  if (right != null && right.size > MIN_KEYS) { // 若是右兄弟節點富裕,左旋轉。
    leftRotate(p);
    return;
  }

  merge(p);
}

總結

  • 一般狀況下B 樹節點的大小設置會和緩存頁至關,以保證一次可以獲取更多的關鍵碼,以減小 I/O;
  • B 樹仍然還有不少的變種,甚至紅黑樹也和(2,4)B 樹息息相關,後面的章節會繼續講到;
相關文章
相關標籤/搜索