查詢與二叉樹
查詢與二叉樹
我家園子有幾棵樹系列
- 查詢與二叉樹
- 我家園子有幾棵樹系列
- Preface
- 查詢
- 二叉查詢樹的實現
- 定義資料結構
- 中序遍歷
- 查詢操作
- 插入
- 刪除
- 刪除最小值
- 複製(拷貝)刪除
- 步驟
- Rank
- 2-3查詢樹
- 總結
Preface
前面我們學習了基於線性表的資料結構,如陣列,連結串列,佇列,棧等。現在我們要開始學習一種非線性的資料結構--樹(tree),是不是很興奮呢!讓我們開始新的系列吧!
查詢
先讓我們回憶一下線性表的查詢,首先最暴力的方法就是做一個線性掃描,一一對比是不是要找的值。這麼做的時間複雜度顯而易見的是 O(N),如表格第一行;更機智一點,我們採用二分法,首先將線性表排好順序,然後每次對比中間的值就好了,這樣做的時間複雜度就是 O(logN),如表格第二行。但上面的做法都是利用的線性資料結構,而它有致命的缺點;那就是進行動態的操作時,比如插入,刪除;無法同時實現迅速的查詢,只能等重新排序以後再查,效率就低了很多,無法滿足日常需求(如下表)。這個時候我們的主角就閃亮登場了——二叉查詢樹。
圖源
表格
二叉查詢樹的實現
首先我放幾張圖說明一下什麼是二叉樹,樹的高度,深度等等,詳細的介紹我已經放在這裡,有興趣的話也可以看看別人的部落格。
圖源 轉載學習,如侵權則聯絡我刪除!
深度
廢話不多說我們開始實現一顆二叉查詢樹(BST)吧!
[注] 為了方便理解大部分程式碼都提供了遞迴實現!
定義資料結構
public class BST<Key extends Comparable<Key>, Value> { private Node root; // root of BST private class Node { private Key key; // sorted by key private Value val; // associated data private Node left, right; // left and right subtrees private int size; // number of nodes in subtree public Node(Key key, Value val, int size) { this.key = key; this.val = val; this.size = size; } } /** * Initializes an empty symbol table. */ public BST() {} /** * Returns the number of key-value pairs in this symbol table. * @return the number of key-value pairs in this symbol table */ public int size() { return size(root); } // return number of key-value pairs in BST rooted at x private int size(Node x) { if (x == null) return 0; else return x.size; } }
中序遍歷
我們知道二叉查詢樹的任一個節點,他的左子結點比他小,右子節點比他大,哈,那麼我們只要進行一波中序遍歷就可以完成資料的排序啦!
/*************************************************************************** * 中序遍歷,非遞迴版本 ***************************************************************************/ public Iterable<Key> keys() { Stack<Node> stack = new Stack<Node>(); Queue<Key> queue = new Queue<Key>(); Node x = root; while (x != null || !stack.isEmpty()) { if (x != null) { stack.push(x); x = x.left; } else { x = stack.pop(); queue.enqueue(x.key); x = x.right; } } return queue; } /************************************************************************ * 中序遍歷,遞迴列印 ************************************************************************/ public void inOrder(Node* root) { if (root == null) return; inOrder(root.left); print root // 此處為虛擬碼,表示列印 root 節點 inOrder(root.right); }
查詢操作
/************************************************************************
* 非遞迴
************************************************************************/
Value get(Key key){
Node x = root;
while(x != null){
int cmp = key.compareTo(x.key);
if(cmp<0)
x = x.left;
else if(cmp>0)
x = x.right;
else return
x.value;
}
return null;
}
/************************************************************************
* 遞迴
************************************************************************/
public Value get(Key key) {
return get(root, key);
}
private Value get(Node x, Key key) {
if (x == null) return null;
int cmp = key.compareTo(x.key);
if (cmp < 0) return get(x.left, key);
else if (cmp > 0) return get(x.right, key);
else return x.val;
}
插入
/************************************************************************
* 非遞迴
************************************************************************/
public void put(Key key, Value val) {
Node z = new Node(key, val);
if (root == null) {
root = z;
return;
}
Node parent = null, x = root;
while (x != null) {
parent = x;
int cmp = key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else if (cmp > 0)
x = x.right;
else {
x.val = val;
return;
}
}
int cmp = key.compareTo(parent.key);
if (cmp < 0)
parent.left = z;
else
parent.right = z;
}
/************************************************************************
* 遞迴版本
************************************************************************/
public void put(Key key, Value value) {
root = put(root, key, value);
}
private Node put(Node x, Key key, Value value) {
if (x == null)
return new Node(key, value, 1);
int cmp = key.compareTo(x.key);
if (cmp < 0)
x.left = put(x.left, key, value);
else if (cmp > 0)
x.right = put(x.right, key, value);
else
x.value = value;
x.size = 1 + size(x.left) + size(x.right);
return x;
}
刪除
刪除有兩種方式,一種是合併刪除
,另一種是複製刪除
,這裡我主要講第二種,想了解第一種可以點這裡
刪除最小值
在正式的刪除之前讓我們先熱身一下,看看怎麼刪除一棵樹的最小值(如圖)。
步驟
- 我們先找到最小值,即不斷查詢節點的左子節點,若無左節點,那他就是最小值。
- 找到最小的節點後,返回他的右子節點給上一層,最小節點會被GC機制回收
- 因為用的是遞迴方法,所以依次更新節點數量
public void deleteMin(){
root = deleteMin(root);
}
private Node deleteMin(Node x){
if (x.left == null) return x.right;
x.left = deleteMin(x.left);
x.N = size(x.left) + size(x.right) + 1;
return x;
}
複製(拷貝)刪除
在說複製刪除之前,我們需要先熟悉二叉查詢樹的前驅和後繼(根據中序遍歷衍生出來的概念)。
- 前驅:A節點的前驅是其左子樹中最右側節點。
- 後繼:A節點的後繼是其右子樹中最左側節點。
BSTcopyDelete
上圖是複製刪除的原理,我們既可以用前驅節點 14 代替,又可以用後繼節點 18 代替。
步驟
BSTcDelete
如圖所示,我們分為四個步驟
- 將指向即將被刪除的節點的連結儲存為t;
- 將 x 指向它的後繼節點
min(t.right)
; - 將 x 的右連結(原本指向一顆所有節點都大於 x.key 的二叉查詢樹) 指向
deleteMin(t.right)
,也就是在刪除後所有節點仍然都大於 x.key 的子二叉查詢樹。 - 將 x 的左連結(本為空) 設為 t.left
public void delete(Key key){
root = delete(root,key);
}
private Node min(Node x){
if(x.left == null) return x;
else return min(x.left);
}
private Node delete(Node x, Key key){
if(x==null) return null;
int cmp = key.compareTo(x.key);
if(cmp < 0) x.left = delete(x.left, key);
else if(cmp > 0) x.right = delete(x.right, key);
else{
if(x.right == null) return x.left;
if(x.left == null) return x.right;
Node t = x;
x = min(t.right);
x.right = deleteMin(t.right);
x.left = t.left;
}
x.N = size(x.left) + size(x.right) + 1;
return x;
}
在前面的程式碼中,我們總是刪除node中的後繼結點,這樣必然會降低右子樹的高度,在前面中我們知道,我們也可以使用前驅結點來代替被刪除的結點。所以我們可以交替的使用前驅和後繼來代替被刪除的結點。
J.Culberson從理論證實了使用非對稱刪除, IPL(內部路徑長度)的期望值是 O(n√n), 平均查詢時間為 O(√n),而使用對稱刪除, IPL的期望值為 O(nlgn),平均查詢時間為 O(lgn)。
Rank
查詢節點 x 的排名
public int rank(Key key){
return rank(key, root);
}
private int rank(Key key, Node x){
// 返回以 x 為根節點的子樹中小於x.key的數量
if(x == null) return 0;
int cmp = key.compareTo(x.key);
if(cmp<0) return rank(key,x.left);
else if(cmp>0) return 1 + size(x.left) + rank(key,x.right);
else return size(x.left);
}
2-3查詢樹
通過前面的分析我們知道,一般情況下二叉查詢樹的查詢,插入,刪除都是 O(lgn)的時間複雜度,但是二叉查詢樹的時間複雜度是和樹的高度是密切相關的,如果我們以升序的元素進行二叉樹的插入,我們會發現,此時的二叉樹已經退化成連結串列了,查詢的時間複雜度變成了 O(n),這在效能上是不可容忍的退化!那麼我們該怎麼解決這個問題呢?
答案相信大家都知道了,那就是構建一顆始終平衡的二叉查詢樹。那麼有哪些平衡二叉查詢樹呢?如何實現?
這些我們留到下節再講。
總結
這一節我們學會了使用非線性的資料結構--二叉查詢樹來高效的實現查詢,插入,刪除操作。分析了它的效能,在隨機插入的情況下,二叉查詢樹的高度趨近於 2.99lgN
,平均查詢時間複雜度為 1.39lgN(2lnN)
,而且在升序插入的情況下,樹會退化成連結串列。這些知識為我們後面學習2-3查詢樹和紅黑樹打下了基礎。