學習數據結構 二叉查找樹(binary search tree)

爲學習 LLVM 的 ImmutableSet,其底層的實現選擇爲 AVL 樹(平衡二叉搜索樹),我不很熟悉該樹,雖然大體知道但畢竟不精,所以仍是先學習學習二叉搜索樹吧。java

二叉搜索樹或叫作二叉查找樹,能夠用於實現Dictionary辭典,辭典是K,V 鍵值的對的集合。通常 Dictionary 不含重複的 K。一個 Dictionary 的抽象數據類型規範可定義爲:node

interface Dictionary<K extends Comparable<K>, V> {
	// 當且僅當辭典爲空的時候返回真。
	public boolean isEmpty();
	// 若是存在鍵值爲 k 的 <K,V> 對,則返回它們。 
	public Entry<K, V> get(K k); 
	// 插入給出的 k,v 對,若是鍵值已經存在則更新相關元素 v。
	public void insert(K k, V v);
	// 刪除特定鍵值的 k,v 對。
	public void remove(K k);
}

二叉查找樹的定義有4條,參見書便可。咱們最需關注的是在二叉查找樹中位於左子樹的鍵小於根的鍵,右子樹的鍵大於根的鍵。下面具體學習如何實現 isEmpty, get, insert, remove 等方法。小程序

假設節點與樹的結構以下(方法實如今 BSTree 類中):學習

// 表示一個樹節點。
class TreeNode<K, V> {
	TreeNode left;  // 左子樹
	TreeNode right; // 右子樹
	K key;	       // 鍵
	V value;        // 元素值
}

// 表示一個二叉查找樹。
class BSTree<K, V> implements Dictionary {
	TreeNode<K, V> root;  // 根節點。
}

方法 isEmpty 比較簡單,判斷 root 是否爲空便可。不用多探討了。this

方法 get 以下:3d

 

// 在 BSTree 類中.
  public TreeNode get(K k) {
    if (this.root == null) return null; // 沒有任何節點
    return this.root.find(k);
  }

  // 在 TreeNode 類中。
  public TreeNode find(K k) {
    TreeNode<K, V> node = this;
    while (node != null) {
      int c = k.compareTo(node.getKey());
      if (c < 0) 
        node = node.getLeft();	// 查找左子樹。
       else if (c > 0)
        node = node.getRight();	// 查找右子樹。
       else
      return node;	// 找到了。(c == 0)
    }
    return null;	// 沒有找到。
  }

在這裏使用了二叉查找樹的核心性質,根據 compare 結果,若是 k 小於當前節點的鍵則查找左子樹,大於則查找右子樹,若是相等則就是本身。也能夠使用遞歸方法寫查找,可是效率上可能不如這樣寫好。code

insert 方法實現以下:遞歸

public void insert(K k, V v) {
    if (k == null) throw new NullPointerException("k");
    TreeNode<K, V> new_node = new TreeNode<K, V>(k, v);
    if (this.root == null) {
      // 特殊狀況,沒有任何節點的時候。
      this.root = new_node;
      return;
    }
    
    // 在樹中搜索 k 的節點 p, pp 表示 p 的父節點。
    TreeNode<K, V> p = this.root, pp = null;
    while (p != null) {
      pp = p;    // 若是 p 走向了子節點,則 pp 剛好是 p 的父節點。
      int c = k.compareTo(p.getKey());
      if (c < 0)   // 走向左節點。
        p = p.getLeft();
      else if (c > 0)    // 走向右節點。
        p = p.getRight();
      else {    // p 的 key 就是 k,找到同鍵值的,則更新 v 返回。
        p.setValue(v);
        return;
      }
    }
    
    // 若是走到這裏,則 p == null, pp 是前一個節點,新的節點應插入到 pp 下面(左、右取決於鍵值的比較)
    if (k.compareTo(pp.getKey()) < 0)  // k < pp.key 表示應插入到 pp 的左子樹
      pp.left = new_node;
    else
      pp.right = new_node;
  }

 remove 實現以下:rem

private TreeNode<K, V> internal_remove(K k) {
    if (this.root == null) return null;    // 沒有任何節點的特殊狀況。
    TreeNode<K, V> p = this.root, pp = null;
    
    // 查找要刪除的鍵值爲 k 的節點 p,以及其父節點 pp。
    // 若是 pp 爲 null 表示 p 是根節點 root。
    while (p != null) {
      int c = k.compareTo(p.getKey());
      if (c == 0) break;
      pp = p;
      p = (c < 0) ? p.left : p.right;
    }
    if (p == null) return null;     // 沒有找到要刪除的元素。
    
    // 如今 p 是要刪除的節點,pp 是其父節點(可能爲 null)。
    if (p.left == null || p.right == null) {
      // 這裏 p 至多隻有一個子樹,則就用這一個子樹替代 p 便可。
      set_pp_child(pp, p, p.left != null ? p.left : p.right);
    } else {
      // p 有兩個子樹。能夠用 left.max or right.min 來替代 p。咱們選用右邊的最左子樹(min)。
      TreeNode<K, V> pR = p.right;
      if (pR.left == null) {
        // 右邊的子樹沒有左分支,則用 p_right 替代 p 便可。
        pR.left = p.left;
        set_pp_child(pp, p, pR);
        return p;
      }
      
      // p 的右子樹 pR 有左子樹,則從中刪除最左的節點 -- rmin。
      TreeNode<K, V> rmin = internal_remove_rmin(pR);
      rmin.left = p.left;    // 用 rmin 替代 p,所以設置 rmin.left, .right
      rmin.right = pR;
      set_pp_child(pp, p, rmin);
    }
    return p;
  }
  
  
  // 移除指定節點的最小子節點(pR.min),也即最左子節點。
  private TreeNode<K, V> internal_remove_rmin(TreeNode<K, V> pR) {
    if (pR.left == null) throw new java.lang.RuntimeException("pR.left is null");
    
    TreeNode<K, V> rmin = pR.left, rmin_pp = pR;  // rmin_pp 是 rMin 的父節點。
    while (rmin.left != null) {
      rmin_pp = rmin;
      rmin = rmin.left;    // 向左走到最左子節點,其就是 pR.min。
    }
    
    // 如今 rmin = pR.min, rmin_pp = rmin.parent。
    assert (rmin.left == null);    // 如今 rmin 必然沒有左子樹了。
    rmin_pp.left = rmin.right;
    rmin.right = null;
    return rmin;
  }
  
  private void set_pp_child(TreeNode<K, V> pp, TreeNode<K, V> p, TreeNode<K, V> child) {
    if (pp == null)
      this.root = child;    // 刪除的就是根,設置根爲 child
    else if (pp.left == p)
      pp.left = child;    // p 是 pp 的左子樹,替代爲 child
    else 
      pp.right = child;    // p 是 pp 的右子樹,替代爲 child
  }

remove 方法的實現顯得笨拙一些。get

爲了方便研究 binary search tree,我編寫了一個 java 小程序,位於這裏:http://vdisk.weibo.com/s/3dT-U/1331863473

該程序能夠經過輸入命令 find, insert, delete, dump 等構造和查看二叉查找樹。幫助爲 help, 退出爲 exit。

下一步準備在二叉查找樹的基礎上實現/學習 AVL 樹。

相關文章
相關標籤/搜索