二叉查找樹(Binary Search Tree),也稱爲二叉搜索樹、有序二叉樹(ordered binary tree)或排序二叉樹(sorted binary tree),是指一棵空樹或者具備下列性質的二叉樹:node
二叉查找樹相比於其餘數據結構的優點在於查找、插入的時間複雜度較低,爲O(log n)。中序遍歷二叉查找樹可獲得一個關鍵字的有序序列,一個無序序列能夠經過構造一棵二叉查找樹變成一個有序序列,構造樹的過程即將無序序列中元素逐個插入到二叉查找樹的過程。每次插入的新的結點都是二叉查找樹上新的葉子結點,在進行插入操做時,沒必要移動其它結點,只需改動某個結點的指針,由空變爲非空便可。搜索、插入、刪除的複雜度等於樹高,指望 O(log n),最壞O(n)(數列有序,樹退化成線性表)。數據結構
下面構造的二叉查找樹由於要實現選擇(select)和排名(rank)兩個操做,因此在Node中新增長了一個int類型字段size,表示該結點所做爲根節點所表明的樹當中全部結點的個數(包括其自己),則對任意結點總有size(x)=size(x.left)+size(x.right)+1
(1表明的是x結點自己),當咱們後面在進行插入、刪除相關操做的都要考慮到了對N的進行更新。還設置了size函數來返回size,以下所示less
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; }
//Returns the value associated with the given key. public Value get(Key key) { if (key == null) throw new IllegalArgumentException("calls get() with a null 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) { if (key == null) throw new IllegalArgumentException("calls put() with a null key"); root = put(root, key, val); } private Node put(Node x, Key key, Value val) { if (x == null) return new Node(key, val, 1); int cmp = key.compareTo(x.key); if (cmp < 0) x.left = put(x.left, key, val); else if (cmp > 0) x.right = put(x.right, key, val); else x.val = val; //下面兩行時總會執行的 x.size = 1 + size(x.left) + size(x.right); return x; }
函數功能:在node做爲根節點所表示的二叉查找樹當中,將key和val生成新結點插入進去,已有相同key時則是更新對應的值爲val,而後將根節點返回。其實這個函數當中並不會有下面在刪除操做中存在的根節點被更換的問題,而插入操做自己也不須要像查找(要返回查找到的結點)或者選擇(返回排名爲k的結點)、排名(返回key對應結點在樹中的排名)、取整(要返回一個key取整在樹中可以找到的結點)的要有一個返回值,之因此返回根節點只是爲了更新結點當中的size,在Node不須要size屬性時,徹底能夠將返回值設置成爲void便可代碼留待後面寫下
傳入參數:node、key和val。
返回值:返回一個根節點,該根節點多是隻是根據參數key找到了子樹上某個結點更新了值或者根據key在該根節點子樹上的相應位置插入了一個新結點。this
其中1234主要是邏輯實現,0是主要爲了邏輯實現與完善遞歸。指針
public Key floor(Key key) { Node x = floor(root, key); if (x == null) return null; return x.key; } private Node floor(Node x, Key key) { if (x == null) return null; int cmp = key.compareTo(x.key); if (cmp == 0) return x; if (cmp < 0) return floor(x.left, key); Node t = floor(x.right, key); if (t != null) return t; else return x; }
向下取整是獲得鍵相比給定的key變小了一點的結點,向上取整是獲得鍵相比給定的key變大了一點的結點
向下取整:若是給定鍵的key小於二叉樹的根節點的鍵,那麼小於等於key的最大鍵floor(key)必定在根節點的左子樹當中,這是由於原本就小於了你再變小一點那就只能是根節點左子樹當中的結點了,因此在左子樹當中;若是給定鍵的key大於二叉樹的根節點的鍵,那麼只有當根節點右子樹當中存在小於等於key的結點時,小於等於key的最大鍵纔會出如今右子樹當中,這是由於原本是大於的,若是變小了一點,那麼可能會變成根節點或者根節點左子樹中的結點。而且當小於的時候向下取整可能不存在,而當大於的時候向下取整必定存在。
函數功能:在根節點x所表明的二叉查找樹當中找到 鍵值等於向下取整後的key 的結點,找不到則返回null
傳入參數:二叉查找樹根節點x和要進行向下取整的鍵key
返回值:在根節點x表明的樹中找到的key向下取整的結點或者null(未找到)code
向下取整函數的遞歸邏輯(向上取整相似就不在重複,上下互換、左右互換、大小互換便可):排序
public Key select(int k) { if (k < 0 || k >= size()) { throw new IllegalArgumentException("argument to select() is invalid: " + k); } Node x = select(root, k); return x.key; } // Return key of rank k. private Node select(Node x, int k) { if (x == null) return null; int t = size(x.left); if (t > k) return select(x.left, k); else if (t < k) return select(x.right, k-t-1); else return x; } public int rank(Key key) { if (key == null) throw new IllegalArgumentException("argument to rank() is null"); return rank(key, root); } // Number of keys in the subtree less than key. private int rank(Key key, Node x) { 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); }
函數功能:在根節點所表明的二叉查找樹當中找到名次爲k的結點,而後將其返回,如找不到則返回的是null
傳入參數:排名名次k,二叉查找樹根節點x
返回值:找到的排名爲k的鍵,找不到則返回null
方法思路:假設咱們想找到排名爲k的鍵(即對於該鍵而言在樹中正好有k個小於它的鍵)。若是左子樹中的結點數t大於k,那麼咱們就繼續(遞歸地)在左子樹中查找排名爲k的鍵;若是t等於k,咱們就返回根節中的鍵;若是t小於k,咱們就(遞歸地)在右子樹中查找排名爲(k-t-1,由於要除去本來的根節點及其左子樹上的結點個數之和)的鍵。遞歸
傳入參數:要進行排名的鍵值Key(若是輸入的不是二叉樹中存在的結點的鍵那麼就會返回0,其實改爲-1應該更好些)
返回值:給定鍵的排名
方法思路:實現與select()相似:若是給定鍵和根節點的鍵相等,咱們返回左子樹的結點總數t;若是給定的鍵小於根節點,咱們會返回該鍵在左子樹中的排名(遞歸計算);若是給定的鍵大於根節點,咱們會返回t+1(根節點)加上它在右子樹當中的排名(遞歸計算)。ci
對於delMin()方法(刪除最小鍵所對應的鍵值對),遞歸方法接收一個指向結點的引用,並返回指向一個結點的引用,這樣的話咱們就能將這個返回值賦給被做爲參數傳進來的那個引用變量,就能將對樹結構進行的刪除正確地反映。
什麼意思呢,咱們在刪除的過程當中可能會刪除掉根節點,也就是說根節點換了,但函數外部那個本來的根節點的引用變量仍是指向被刪除的那個根節點,怎麼辦?就是隻能將新的根節點(固然根節點也可能沒被刪掉,仍是舊的,被刪掉的狀況是更少的可是是必須考慮的)返回賦值給那個根節點引用變量,以下面的root = deleteMin(root),這樣就能正確地將刪除操做反映出來了。
//Removes the smallest key and associated value from the symbol table. public void deleteMin() { if (isEmpty()) throw new NoSuchElementException("Symbol table underflow"); root = deleteMin(root); assert check(); } private Node deleteMin(Node x) { if (x.left == null) return x.right; x.left = deleteMin(x.left); x.size = size(x.left) + size(x.right) + 1; return x; }
函數功能:刪除根節點x表明的二叉查找樹中鍵值最小的結點,而後將「新」的根節點x返回
傳入參數:根節點x
返回值:進行刪除鍵值最小的結點以後的"新"的根節點
刪除最大鍵與刪除最小鍵的原理和實現均類似,在此再也不贅述。
對於刪除指定鍵而言,若是指定鍵對應的結點x只有一個子結點或者沒有子結點,咱們就能夠採用和上面刪除最大鍵最小鍵的方式同樣來刪除。但對於有兩個子結點的結點x,要採用在刪除結點x後用它的後繼結點填補它的位置。由於x有一個右子結點,所以它的後繼結點就是其右子樹中的最小結點,這樣的替換仍能保證樹的有序性,由於x.key和它的後繼結點之間不存在其餘的鍵。咱們能用四個簡單的步驟完成將x替換爲它的後繼結點的任務
ps:對於左右子樹任一或者同時不存在的狀況已經用刪除最大最小鍵的方法解決,在這裏不用再考慮了,必定是有右子樹的。
//Removes the specified key and its associated value from this symbol table public void delete(Key key) { if (key == null) throw new IllegalArgumentException("calls delete() with a null key"); root = delete(root, key); assert check(); } 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.size = size(x.left) + size(x.right) + 1; return x; }
找到要刪除的結點後的操做:
以上就是在找到指定鍵值相應節點後的操做,能夠看出是一個順序化流程,並不涉及遞歸,但在找到這個相應結點的過程當中仍是用到了遞歸,並在找到後對結點的N進行了更新,刪除操做整體流程以下:
函數功能:對指定根結點x所表示的樹,刪除指定key在樹中對應的結點,而後將該樹「新」的根節點返回
傳入參數:根節點x,要刪除的結點的指定鍵key
返回值:進行刪除以後的"新"的根節點(也可能並無進行任何刪除操做,在沒找到指定鍵的狀況下)