一、定義
一棵二叉查找樹是一棵二叉樹,每個節點都含有一個Comparable的鍵(以及對應的值)。
每個節點的鍵都大於左子樹中任意節點的鍵而小於右子樹中任意節點的鍵。
每個節點都有兩個鏈接,左鏈接、右鏈接,分別指向自己的左子節點和右子節點,鏈接也可以指向null。
盡管鏈接指向的是節點,可以將每個鏈接看做指向了另一棵二叉樹。這個思路能幫助理解二叉查找樹的遞歸方法。
二、基本實現
1、數據表示
private class Node { private Key key; private Value val; private Node left, right; private int size; public Node(Key key, Value val, int n) { this.key = key; this.val = val; this.size = n; left = right = null; } }
2、測試所有方法的用例
在實現方法后,需要一個用例來測試方法是否正常工作。
以下是用例的代碼:

package com.qiusongde; import edu.princeton.cs.algs4.StdOut; public class BSTTest { public static void main(String[] args) { String test = "S E A R C H E X A M P L E"; String[] keys = test.split("\\s+"); int n = keys.length; //test put StdOut.println("Testing put(Key,Value)"); BST<String, Integer> st = new BST<String, Integer>(); StdOut.println(st); for (int i = 0; i < n; i++) { st.put(keys[i], i); StdOut.println("put(" + keys[i] + ", " + i +")"); StdOut.println(st); } //test search hit StdOut.println("Testing keys() and get(Key)"); StdOut.println("--------------------------------"); for (String s : st.keys()) StdOut.println(s + " " + st.get(s)); //test search miss StdOut.println("I" + " " + st.get("I")); StdOut.println(); //test delete StdOut.println("Testing delete(Key)"); StdOut.println(st); StdOut.println("delete E"); st.delete("E");//not root, has two subtree StdOut.println(st); StdOut.println("delete A"); st.delete("A");//not root, has right subtree StdOut.println(st); StdOut.println("delete P"); st.delete("P");//not root, has no subtree StdOut.println(st); StdOut.println("delete L"); st.delete("L");//not root, has left subtree StdOut.println(st); StdOut.println("delete S"); st.delete("S");//root, has two subtree StdOut.println(st); StdOut.println("delete X"); st.delete("X"); StdOut.println(st);//root, has subtree for (int i = 0; i < n; i++) { StdOut.println("delete " + keys[i]); st.delete(keys[i]); StdOut.println(st); } //insert back StdOut.println("insert back"); for (int i = 0; i < n; i++) { st.put(keys[i], i); } StdOut.println(st); StdOut.println("size = " + st.size()); StdOut.println("min = " + st.min()); StdOut.println("max = " + st.max()); StdOut.println(); // print keys in order using select StdOut.println("Testing select"); StdOut.println("--------------------------------"); for (int i = 0; i <= st.size(); i++) StdOut.println(i + " " + st.select(i)); StdOut.println(); // test rank, floor, ceiling StdOut.println("key rank floor ceil"); StdOut.println("-------------------"); for (char i = 'A' - 1; i <= 'Z'; i++) { String s = i + ""; StdOut.printf("%2s %4d %4s %4s\n", s, st.rank(s), st.floor(s), st.ceiling(s)); } StdOut.println(); // test range search and range count String[] from = { "A", "Z", "X", "0", "B", "C" }; String[] to = { "Z", "A", "X", "Z", "G", "L" }; StdOut.println("range search"); StdOut.println("-------------------"); for (int i = 0; i < from.length; i++) { StdOut.printf("%s-%s (%2d) : ", from[i], to[i], st.size(from[i], to[i])); for (String s : st.keys(from[i], to[i])) StdOut.print(s + " "); StdOut.println(); } StdOut.println(); // delete the smallest keys StdOut.println(st); StdOut.println("Test deleteMin"); for (int i = 0; i < 3; i++) { st.deleteMin(); StdOut.println(st); } // delete all the remaining keys, using deleteMax StdOut.println("Test deleteMax"); while (!st.isEmpty()) { st.deleteMax(); StdOut.println(st); } // under empty, test again StdOut.println("under empty, test again"); StdOut.println("size = " + st.size()); StdOut.println("min = " + st.min()); StdOut.println("max = " + st.max()); StdOut.println(); // print keys in order using keys() StdOut.println("Testing keys()"); StdOut.println("--------------------------------"); for (String s : st.keys()) StdOut.println(s + " " + st.get(s)); StdOut.println(); // print keys in order using select StdOut.println("Testing select"); StdOut.println("--------------------------------"); for (int i = 0; i <= st.size(); i++) StdOut.println(i + " " + st.select(i)); StdOut.println(); // test rank, floor, ceiling StdOut.println("key rank floor ceil"); StdOut.println("-------------------"); for (char i = 'A'; i <= 'Z'; i++) { String s = i + ""; StdOut.printf("%2s %4d %4s %4s\n", s, st.rank(s), st.floor(s), st.ceiling(s)); } StdOut.println(); // test range search and range count StdOut.println("range search"); StdOut.println("-------------------"); for (int i = 0; i < from.length; i++) { StdOut.printf("%s-%s (%2d) : ", from[i], to[i], st.size(from[i], to[i])); for (String s : st.keys(from[i], to[i])) StdOut.print(s + " "); StdOut.println(); } StdOut.println(); // delete the smallest keys StdOut.println(st); StdOut.println("Test deleteMin"); for (int i = 0; i < 3; i++) { st.deleteMin(); StdOut.println(st); } // delete all the remaining keys, using deleteMax StdOut.println("Test deleteMax"); while (!st.isEmpty()) { st.deleteMax(); StdOut.println(st); } StdOut.println(); StdOut.println("get(S) under empty"); StdOut.println(st.get("S")); StdOut.println(); StdOut.println("delete(S) under empty"); StdOut.println(st); st.delete("S"); StdOut.println(st); } }
需要實現toString來配合測試用例:
private String BSTString(Node x) { if(x == null) return ""; String s = ""; s += BSTString(x.left); s += x.key + " " + x.val + " " + x.size + "(s)\n"; s += BSTString(x.right); return s; }
3、查找實現(search)
思路:
A、如果二叉查找樹為空,查找失敗(search miss),返回null;
B、如果根節點的鍵等於要查找的鍵,返回根節點的值(search hit)。
C、否則,繼續在相應的子樹中查找。如果要查找的鍵小於根節點的鍵,在左子樹中查找;如果要查找的鍵大於根節點的鍵,在右子樹中查找。
D、重復ABC步驟,直至search miss或者search hit。
遞歸實現:
/** * Returns the value associated with the given key. * * @param key the key * @return the value associated with the given key if the key is in the symbol table * and {@code null} if the key is not in the symbol table * @throws IllegalArgumentException if {@code key} is {@code null} */ public Value get(Key key) { if(key == null) throw new IllegalArgumentException("key is null"); return get(root, key);//work when BST is empty } private Value get(Node x, Key key) { if(x == null) return null;//serach miss 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;//serach hit }
非遞歸實現:
/** * Returns the value associated with the given key. * * @param key the key * @return the value associated with the given key if the key is in the symbol table * and {@code null} if the key is not in the symbol table * @throws IllegalArgumentException if {@code key} is {@code null} */ public Value get(Key key) { if(key == null) throw new IllegalArgumentException("key is null"); Node cur = root; while(cur != null) { int cmp = key.compareTo(cur.key); if(cmp < 0) cur = cur.left; else if(cmp > 0) cur = cur.right; else return cur.val;//search hit } return null;//search miss }
4、插入實現
思路:
A、如果二叉查找樹是空的,生成一個新節點,並返回該節點,相當於插入新節點后的二叉樹根節點。
B、如果根節點鍵和要插入的鍵相等,更新根節點的值。
C、如果要插入的鍵小於根節點的鍵,在左子樹插入,並將根節點的左鏈接指向插入后的左子樹。
D、如果要插入的鍵小於根節點的鍵,在右子樹插入,並將根節點的右鏈接指向插入后的右子樹。
E、更新根節點的size,並返回根節點,作為插入新節點后的二叉樹根節點。
F、重復ABCD,直至插入或者更新成功。
遞歸實現:
/** * Inserts the specified key-value pair into the symbol table, overwriting the old * value with the new value if the symbol table already contains the specified key. * Deletes the specified key (and its associated value) from this symbol table * if the specified value is {@code null}. * * @param key the key * @param val the value * @throws IllegalArgumentException if {@code key} is {@code null} */ public void put(Key key, Value val) { if(key == null) throw new IllegalArgumentException("key is null"); if(val == null) { delete(key); return; } 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 = size(x.left) + size(x.right) + 1; return x; }
遞歸實現的,要在插入之后更新節點的size。
非遞歸實現:
/** * Inserts the specified key-value pair into the symbol table, overwriting the old * value with the new value if the symbol table already contains the specified key. * Deletes the specified key (and its associated value) from this symbol table * if the specified value is {@code null}. * * @param key the key * @param val the value * @throws IllegalArgumentException if {@code key} is {@code null} */ public void put(Key key, Value val) { if(key == null) throw new IllegalArgumentException("key is null"); if(val == null) { delete(key); return; } if(root == null) { root = new Node(key, val); return; } boolean alreadyin = contains(key);//see if it needs to update the counts Node parent = null; Node cur = root; while(cur != null) { parent = cur; cur.size = alreadyin ? cur.size : cur.size + 1;//change size of cur int cmp = key.compareTo(cur.key); if(cmp < 0) cur = cur.left; else if(cmp > 0) cur = cur.right; else { cur.val = val; return; } } if(key.compareTo(parent.key) < 0) parent.left = new Node(key, val); else parent.right = new Node(key, val); }
非遞歸實現的比較麻煩,需要處理一些特殊情況:
A、需要記錄父節點,用於更新父節點的鏈接。
B、還要處理一個特殊情況,就是根節點就是要插入的位置。
C、還要多調用一次get,用於判斷要插入的鍵值對是否已經在二叉搜索樹中。如果在就不用更新size,如果不在就需要更新size。
D、非遞歸實現更新size跟遞歸實現的順序相反,要邊搜索邊更改size。
注:后邊只要改變二叉搜索樹結構的,非遞歸實現都需要考慮這些特殊情況,如insert或delete等。
5、min實現和max實現
以min為例:
思路:
A、如果根節點的左鏈接是null,返回根節點。
B、否則繼續在左子樹中查找。
C、重復AB,直至找到一個根節點的左鏈接是null。
遞歸實現:
/** * Returns the smallest key in the symbol table. * * If the symbol table is empty, return {@code null}. * * @return the smallest key in the symbol table */ public Key min() { if(isEmpty()) return null; return min(root).key; } private Node min(Node x) { if(x.left == null) return x; return min(x.left); }
非遞歸實現:
/** * Returns the smallest key in the symbol table. * * If the symbole table is empty, return {@code null}. * * @return the smallest key in the symbol table */ public Key min() { if(isEmpty()) return null; return min(root).key; } private Node min(Node x) {//x must not be null if(x == null) throw new IllegalArgumentException("Node x must not be null"); Node cur = x; while(cur.left != null) { cur = cur.left; } return cur; }
這里private的min函數,在后邊delete需要調用到。
max的思路和min思路基本差不多,只是左右相反即可。
遞歸實現:
/** * Return the largest key in the symbol table. * * If the symbol table is empty, return {@code null} * * @return the largest key in the symbol table */ public Key max() { if(isEmpty()) return null; return max(root).key; } private Node max(Node x) { if(x.right == null) return x; return max(x.right); }
非遞歸實現:
/** * Return the largest key in the symbol table. * * If the symbol table is empty, return {@code null} * * @return the largest key in the symbol table */ public Key max() { if(isEmpty()) return null; Node cur = root; while(cur.right != null) { cur = cur.right; } return cur.key; }
6、floor和ceiling實現
floor是求the largest key in the BST less than or equal to key
思路:
A、如果根節點是null,則直接返回null。
B、如果根節點鍵值大於key,則繼續floor(key)在左子樹中,所以繼續在左子樹中查找。
C、如果根節點鍵值剛好等於key,則根節點的鍵值即為floor(key),直接返回該鍵。
D、如果根節點鍵值小於key,那么根節點的key有可能就是floor(key),只要右子樹中不存在節點的key小於等於輸入的key。
E、重復ABCD
遞歸實現:
/** * Return the largest key in the symbol table less than or equal to {@code key}. * * If the symbol table is empty, return {@code null} * * @param key the key * @return the largest key in the symbol table less than or equal to {@code key} * * @throws IllegalArgumentException if {@code key} is {@code null} */ public Key floor(Key key) { if(key == null) throw new IllegalArgumentException("key is null"); Node x = floor(root, key);//work when BST is empty 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);//in left subtree Node t = floor(x.right, key);//see if right subtree has a key is the floor(key) if(t != null) return t;//yes, return t else return x;//no, return x }
非遞歸實現:
/** * Return the largest key in the symbol table less than or equal to {@code key}. * * If the symbol table is empty, return {@code null} * * @param key the key * @return the largest key in the symbol table less than or equal to {@code key} * * @throws IllegalArgumentException if {@code key} is {@code null} */ public Key floor(Key key) { if(key == null) throw new IllegalArgumentException("key is null"); Node cur = root; Key result = null; while(cur != null) {//it works when BST is empty int cmp = key.compareTo(cur.key); if(cmp < 0) { cur = cur.left; } else if(cmp > 0) { result = cur.key;//may be updated cur = cur.right;//see if the right subtree has a key smaller than or equal to key } else { return cur.key;//final result } } return result; }
這里用result來緩存,用於解決思路中D的情況。
ceiling和floor思路差不多,只不過左右對調過來。
遞歸實現:
/** * Return the smallest key in the symbol table larger than or equal to {@code key}. * * If the symbol table is empty, return {@code null}. * * @param key the key * @return the smallest key in the symbol table larger than or equal to {@code key} * * @throws IllegalArgumentException if {@code key} is {@code null} */ public Key ceiling(Key key) { if(key == null) throw new IllegalArgumentException("key is null"); Node x = ceiling(root, key);//work when BST is empty if(x == null) return null; return x.key; } private Node ceiling(Node x, Key key) { if(x == null) return null; int cmp = key.compareTo(x.key); if(cmp == 0) return x; if(cmp > 0) return ceiling(x.right, key); Node t = ceiling(x.left, key); if(t != null) return t; else return x; }
非遞歸實現:
/** * Return the smallest key in the symbol table larger than or equal to {@code key}. * * If the symbol table is empty, return {@code null}. * * @param key the key * @return the smallest key in the symbol table larger than or equal to {@code key} * * @throws IllegalArgumentException if {@code key} is {@code null} */ public Key ceiling(Key key) { if(key == null) throw new IllegalArgumentException("key is null"); Node cur = root; Key result = null; while(cur != null) {//it works when BST is empty int cmp = key.compareTo(cur.key); if(cmp < 0) { result = cur.key;//may be updated cur = cur.left; } else if(cmp > 0) { cur = cur.right; } else { return cur.key;//final result } } return result; }
7、select實現
size是專門為select和rank等函數准備的。
思路:
A、如果根節點為null,直接返回null。
B、如果左子樹的節點個數為t,select(k),如果t=k,那么直接返回根節點的值。
C、如果t>k,根節點的排序太大,需要在左子樹中繼續select(k)。
D、如果t<k,根節點的排序太小,需要在右子樹中繼續select(k-t-1)。
E、重復ABCD
遞歸實現:
/** * Return the kth smallest key in the symbol table. * * When k is 0, return the smallest key. When k is <em>N</em> − 1, return the largest key. * * @param k the order statistic * @return the kth smallest key in the symbol table * * @throws IllegalArgumentException unless {@code k} is between 0 and * <em>N</em> − 1 */ public Key select(int k) { if(k < 0 || k >= size()) return null; Node x = select(root, k);//work when BST is empty if(x == null) return null; return x.key; } 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; }
非遞歸實現:
/** * Return the kth smallest key in the symbol table. * * When k is 0, return the smallest key. When k is <em>N</em> − 1, return the largest key. * * @param k the order statistic * @return the kth smallest key in the symbol table * * @throws IllegalArgumentException unless {@code k} is between 0 and * <em>N</em> − 1 */ public Key select(int k) { if(k < 0 || k >= size())//include the empty situation return null; Node cur = root; while(cur != null) { int less = size(cur.left); if(less < k) { cur = cur.right; k = k - less - 1; } else if(less > k) { cur = cur.left; } else { return cur.key; } } return null; }
8、rank實現
思路:
A、如果根節點為null,返回0。
B、如果根節點的鍵剛好等於key,返回左子樹的節點個數(剛好只有左子樹的所有鍵小於key)。
C、如果根節點的鍵大於key,則在左子樹中rank(key)(只有左子樹才有小於key的鍵)。
D、如果根節點的鍵小於key,則除了左子樹所有鍵都小於key外,右子樹也可能有小於key的鍵,則返回左子樹的節點個數+rank(key)在右子樹的值+1。
E、重復ABCD。
遞歸實現:
/** * * Return the number of keys in the symbol table strictly less than {@code key}. * * @param key the key * @return the number of keys in the symbol table strictly less than {@code key} * * @throws IllegalArgumentException if key is {@code null} */ public int rank(Key key) { if(key == null) throw new IllegalArgumentException("key is null"); return rank(root, key);//work when BST is empty } private int rank(Node x, Key key) { if(x == null) return 0; int cmp = key.compareTo(x.key); if(cmp < 0) return rank(x.left, key); else if(cmp > 0) return size(x.left) + 1 + rank(x.right, key); else return size(x.left); }
非遞歸實現:
/** * * Return the number of keys in the symbol table strictly less than {@code key}. * * @param key the key * @return the number of keys in the symbol table strictly less than {@code key} * * @throws IllegalArgumentException if key is {@code null} */ public int rank(Key key) { if(key == null) throw new IllegalArgumentException("key is null"); Node cur = root; int result = 0; while(cur != null) {//work when BST is empty int cmp = key.compareTo(cur.key); if(cmp < 0) { cur = cur.left; } else if(cmp > 0) { result += size(cur.left) + 1; cur = cur.right; } else { return result + size(cur.left); } } return result; }
9、刪除最小值和最大值
思路:
一直往左走,直到到達左鏈接為null的節點x(最小值),然后將指向該節點x的鏈接替換為x.right。
遞歸實現:
/** * Removes the smallest key and its associated value from the symbol table * * If the symbol table is empty, do nothing. * */ public void deleteMin() { if(isEmpty()) return;//do nothing root = deleteMin(root); } 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; }
非遞歸實現:
/** * Removes the smallest key and its associated value from the symbol table * * If the symbol table is empty, do nothing. * */ public void deleteMin() { if(isEmpty()) return;//do nothing root = deleteMin(root); } private Node deleteMin(Node x) {//x must not be null if(x.left == null) { return x.right; } Node cur = x; Node parent = null; while(cur.left != null) { cur.size--; parent = cur; cur = cur.left; } parent.left = cur.right; return x; }
非遞歸:
A、需要記錄父節點,用於更新父節點的鏈接。
B、還要處理一個特殊情況,就是根節點就是要刪除的節點。
C、非遞歸實現更新size跟遞歸實現的順序相反,要邊搜索邊更改size。
刪除最大值思路差不多,左右相反而已。
遞歸實現:
/** * Removes the largest key and associated value from the symbol table. * * If the symbol table is empty, do nothing. */ public void deleteMax() { if(isEmpty()) return; //do nothing root = deleteMax(root); } private Node deleteMax(Node x) { if(x.right == null) return x.left; x.right = deleteMax(x.right); x.size = size(x.left) + size(x.right) + 1; return x; }
非遞歸實現:
/** * Removes the largest key and associated value from the symbol table. * * If the symbol table is empty, do nothing. */ public void deleteMax() { if(isEmpty()) return;//do nothing if(root.right == null) { root = root.left; return; } Node cur = root; Node parent = null; while(cur.right != null) { cur.size--; parent = cur; cur = cur.right; } parent.right = cur.left; }
非遞歸:
A、需要記錄父節點,用於更新父節點的鏈接。
B、還要處理一個特殊情況,就是根節點就是要刪除的節點。
C、非遞歸實現更新size跟遞歸實現的順序相反,要邊搜索邊更改size。
10、刪除節點
如果要刪除的節點x只有子節點或者沒有子節點,那么可以效仿刪除最小值和最大值的做法。
如果有兩個子節點呢?應該在右子樹中找繼承者來替換x,然后再返回x。
步驟:
A、用t保存即將刪除的節點x。
B、將x指向右子樹的最小鍵節點,也就是繼承者。
C、x.right = deleteMin(t.right)。
D、x.left = t.left。
遞歸實現:
/** * Removes the specified key and its associated value from this symbol table * If the key is not in this symbol table or this symbol table is empty, do nothing * * @param key the key * @throws IllegalArgumentException if {@code key} if {@code null} */ public void delete(Key key) { if(key == null) throw new IllegalArgumentException("key is null"); if(isEmpty()) return; //do nothing root = delete(root, key); } 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);//left subtree else if(cmp > 0) x.right = delete(x.right, key);//right subtree else { if(x.right == null) return x.left; if(x.left == null) return x.right; Node temp = x;//save the node to be deleted x = min(temp.right);//set x to point to its successor min(temp.right) x.right = deleteMin(temp.right);//set the right link of the successor to deleteMin(temp.right) x.left = temp.left;//set the left link of the successor to t.left } x.size = size(x.left) + size(x.right) + 1; return x; }
非遞歸實現:
/** * Removes the specified key and its associated value from this symbol table * If the key is not in this symbol table or this symbol talbe is empty, do nothing * * @param key the key * @throws IllegalArgumentException if {@code key} if {@code null} */ public void delete(Key key) { if(key == null) throw new IllegalArgumentException("key is null"); if(!contains(key) || isEmpty()) { return;//do nothing } if(key.compareTo(root.key) == 0) { deleteRoot(); return; } Node cur = root; Node parent = null; while(cur != null) { int cmp = key.compareTo(cur.key); if(cmp < 0) { cur.size--; parent = cur;//record parent cur = cur.left; } else if(cmp > 0){ cur.size--; parent = cur;//record parent cur = cur.right; } else { int parentcmp = key.compareTo(parent.key); if(cur.left == null) {//special case if(parentcmp < 0) { parent.left = cur.right; } else { parent.right = cur.right; } return; } if(cur.right == null) {//special case if(parentcmp < 0) { parent.left = cur.left; } else { parent.right = cur.left; } return; } Node temp = cur; cur = min(temp.right);//temp.right will not be null cur.right = deleteMin(temp.right); cur.left = temp.left; cur.size = size(cur.left) + size(cur.right) + 1; if(parentcmp < 0) { parent.left = cur; } else { parent.right = cur; } return; } } }
這個非遞歸的實現太麻煩了,有很多特殊情況需要處理。
A、需要記錄父節點,用於更新父節點的鏈接。
B、還要處理一個特殊情況,就是根節點就是要刪除的節點。
C、還要多調用一次get,用於判斷要刪除的節點是否在二叉搜索樹中。
D、非遞歸實現更新size跟遞歸實現的順序相反,要邊搜索邊更改size。
11、其他操作
/** * Returns all keys in the symbol table as an {@code Iterable}. * To iterate over all of the keys in the symbol table named {@code st}, * use the foreach notation: {@code for (Key key : st.keys())}. * * @return all keys in the symbol table */ public Iterable<Key> keys() { return keys(min(), max()); } /** * Returns all keys in the symbol table in the given range, * as an {@code Iterable}. * * @param lo minimum endpoint * @param hi maximum endpoint * @return all keys in the symbol table between {@code lo} * (inclusive) and {@code hi} (inclusive) * * @throws IllegalArgumentException if either {@code lo} or {@code hi} * is {@code null} */ public Iterable<Key> keys(Key lo, Key hi) { LinkedList<Key> queue = new LinkedList<>(); keys(root, queue, lo, hi); return queue; } private void keys(Node x, LinkedList<Key> queue, Key lo, Key hi) { if(x == null) return; int cmplo = lo.compareTo(x.key); int cmphi = hi.compareTo(x.key); if(cmplo < 0) keys(x.left, queue, lo, hi); if(cmplo <= 0 && cmphi >= 0) queue.add(x.key); if(cmphi > 0) keys(x.right, queue, lo, hi); } /** * 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); } /** * Returns the number of keys in the symbol table in the given range. * * @param lo minimum endpoint * @param hi maximum endpoint * @return the number of keys in the symbol table between {@code lo} * (inclusive) and {@code hi} (inclusive) * * @throws IllegalArgumentException if either {@code lo} or {@code hi} * is {@code null} */ public int size(Key lo, Key hi) { if(lo == null) throw new IllegalArgumentException("lo is null"); if(hi == null) throw new IllegalArgumentException("hi is null"); if(hi.compareTo(lo) < 0) return 0; else if(contains(hi)) return rank(hi) - rank(lo) + 1; else return rank(hi) - rank(lo); } private int size(Node x) { if(x == null) return 0; else return x.size; } /** * Returns true if this symbol table is empty. * * @return {@code true} if this symbol table is empty; {@code false} otherwise */ public boolean isEmpty() { return size() == 0; } /** * Does this symbol table contain the given key? * * @param key the key * @return {@code true} if this symbol table contains {@code key} and * {@code false} otherwise * * @throws IllegalArgumentException if {@code key} is {@code null} */ public boolean contains(Key key) { if(key == null) throw new IllegalArgumentException("key is null"); return get(key) != null; }
三、性能分析
假設鍵的順序是隨機的,也就是插入的順序是隨機的。
這個的分析其實和快排是差不多的。
結論1:在N個隨機鍵構造的二叉查找樹中,查找命中平均需要的比較次數為~2lnN ~1.39log2N。
證明:
定義內部路徑長度為所有節點的深度的和。
令IN為N個隨機排序的不同鍵構造而成的二叉查找樹的內部路徑長度,其中I0=I1=0。
則IN = (N-1) + (I0+IN-1)/N + (I1+CN-2)/N + …… + (IN-1+I0)/N。
根節點使兩個子樹所有節點的深度都加1,也就是N-1。
這個的分析跟快排的分析差不多,IN~2NlnN。
平均比較次數為:1+IN/N~2lnN。
結論2:在N個隨機鍵構造的二叉查找樹中,插入和查找失敗平均需要的比較次數為~2lnN ~1.39log2N
證明:
定義外部路徑長度為根節點到所有null節點的所有路徑的節點總和。
令EN為N個隨機排序的不同鍵構造而成的二叉查找樹的外部路徑長度,其中E0=0,E1=2。
則EN = (N+1) + (E0+EN-1)/N + (E1+EN-2)/N + …… + (EN-1+E0)/N。
其中N+1為經過根節點的總路徑的總和為N+1。這里還需要證明有N個節點的二叉樹,其null鏈接為N+1。所以總路徑數為N+1。
N個節點,總共有2N個鏈接,其中除了根節點外,每個節點都有一個鏈接指向該節點,故總共有N-1個鏈接指向節點,剩下2N-(N-1)=N+1個鏈接指向null。
EN的分析和IN差不多。
結論3:EN=IN+2N
用歸納法,即可證明。略。
結論4:插入和查找失敗平均比查找成功多一次比較。
證明:
插入和查找失敗平均比較次數為EN/N,查找成功平均比較次數為IN/N+1。
EN/N-(IN/N+1)=(EN-IN)/N - 1= 2N/N - 1 = 1。
結論5:在一棵二叉查找樹中,所有操作在最壞情況下所需的時間都和樹的高度成正比。
樹的高度將會比平均內部路徑長度要大。對於足夠大的N,高度趨近於2.99logN。
但是構造樹的鍵不是隨機的話,最壞情況將會變得不可接受。比如用例按照順序或者逆序插入符號表。
這種情況還是有可能出現的,因為用例控制着插入和查找等操作的順序。
平衡二叉查找樹(紅黑樹)可以解決這個問題。