目錄
哈希樹的理論基礎
質數分辨定律
這個定理可以簡單的表述爲:n個不同的質數可以“分辨”的連續整數的個數和他們的乘積相等。“分辨”就是指這些連續的整數不可能有完全相同的餘數序列。這個爲哈希樹的分辨方式奠定了理論基礎。
顯然,這個定理的一個特殊情況就是爲從2起的連續質數。我們可以記爲前個連續質數的乘積。連續10個質數就可以分辨大約個數,已經超過計算機中常用整數(32bit)的表達範圍。
而按照目前的CPU水平,100次取餘的整數除法操作幾乎不算什麼難事。在實際應用中,整體的操作速度往往取決於節點將關鍵字裝載內存的次數和時間。一般來說,裝載的時間是由關鍵字的大小和硬件來決定的;在相同類型關鍵字和相同硬件條件下,實際的整體操作時間就主要取決於裝載的次數。他們之間是一個成正比的關係。
餘數分辨定理
這個定理可以簡單的表述爲:n個不同的數可以“分辨”的連續整數的個數不超過他們的最小公倍數。超過這個範圍就意味着衝突的概率會增加。定理1是定理2的一個特例。
哈希樹簡介
從2起的連續質數,連續10個質數就可以分辨大約M(10) =2*3*5*7*11*13*17*19*23*29= 6464693230 個數(64億),已經超過計算機中常用整數(32bit)的表達範圍(int的範圍爲正負20個億)。連續100個質數就可以分辨大約M(100) = 4.711930 乘以10的219次方。
我們選擇質數分辨算法來建立一顆哈希樹。選擇從2開始的連續質數來建立一個十層的哈希樹(因爲已經超過了計算機中常用整數的表達範圍)。第一層節點爲根節點,根節點下有2個節點;第二層的每個節點下有3個節點;依此類推,即每層節點的子節點數目爲連續的質數。到了第十層,每個節點下有29個節點。
同一結點中的子結點,從左到右代表不同的餘數結果。
例如:第二層結點下有三個子節點。那麼從左到右分別代表:除3餘0,除3餘1,除3餘2.
對質數進行取餘操作得到的餘數決定了處理的路徑。
也就是說如果有21億個數字的話,我們查找的哪怕是最底層的也僅僅需要計算10次就能找到對應的數字。
所以hash樹是一棵爲查找而生的樹。
查找
哈希樹的節點查找過程和節點插入過程類似,就是對關鍵字用質數序列取餘,根據餘數確定下一節點的分叉路徑,直到找到目標節點。
如上圖,最小”哈希樹(HashTree)在從4G個對象中找出所匹配的對象,比較次數不超過10次。也就是說:最多屬於O(10)。在實際應用中,調整了質數的範圍,使得比較次數一般不超過5次。也就是說:最多屬於O(5)。因此可以根據自身需要在時間和空間上尋求一個平衡點。
刪除
哈希樹的節點刪除過程也很簡單,哈希樹在刪除的時候,並不做任何結構調整。
只是先查到到要刪除的節點,然後把此節點的“佔位標記”置爲false即可(即表示此節點爲空節點,但並不進行物理刪除)。
優點
結構簡單
從哈希樹的結構來說,非常的簡單。每層節點的子節點個數爲連續的質數。子節點可以隨時創建。因此哈希樹的結構是動態的,也不像某些哈希算法那樣需要長時間的初始化過程。哈希樹也沒有必要爲不存在的關鍵字提前分配空間。
需要注意的是哈希樹是一個單向增加的結構,即隨着所需要存儲的數據量增加而增大。即使數據量減少到原來的數量,但是哈希樹的總節點數不會減少。這樣做的目的是爲了避免結構的調整帶來的額外消耗。
查找迅速
從算法過程我們可以看出,對於整數,哈希樹層級最多能增加到10。因此最多只需要十次取餘和比較操作,就可以知道這個對象是否存在。這個在算法邏輯上決定了哈希樹的優越性。
一般的樹狀結構,往往隨着層次和層次中節點數的增加而導致更多的比較操作。操作次數可以說無法準確確定上限。而哈希樹的查找次數和元素個數沒有關係。如果元素的連續關鍵字總個數在計算機的整數(32bit)所能表達的最大範圍內,那麼比較次數就最多不會超過10次,通常低於這個數值。
結構不變
從刪除算法中可以看出,哈希樹在刪除的時候,並不做任何結構調整。這個也是它的一個非常好的優點。常規樹結構在增加元素和刪除元素的時候都要做一定的結構調整,否則他們將可能退化爲鏈表結構,而導致查找效率的降低。哈希樹採取的是一種“見縫插針”的算法,從來不用擔心退化的問題,也不必爲優化結構而採取額外的操作,因此大大節約了操作時間。
缺點
非排序性
哈希樹不支持排序,沒有順序特性。如果在此基礎上不做任何改進的話並試圖通過遍歷來實現排序,那麼操作效率將遠遠低於其他類型的數據結構。
哈希樹的java實現
節點
package datastructure.tree.hashtree;
import java.util.Arrays;
public class Node {
//node的下一層的節點
public Node[] next;
//節點的值
public int value;
//節點是否已被刪除
public boolean isDel;
public Node(int value,int nextNum){
this.value=value;
this.next=new Node[nextNum];
this.isDel=false;
}
@Override
public String toString() {
return "Node [next=" + Arrays.toString(next) + ", value=" + value + ", isDel=" + isDel + "]";
}
}
哈希樹
如何操作都在註釋中
基本思路就是先處理root,然後處理下一層的節點,然後再跳到下一層去
package datastructure.tree.hashtree;
public class HashTree {
public static final int[] primeNumber=new int[]{2,3,5,7,11,13,17,19,23,29};//連續11個質數能表示6464693230個數字
Node root;
/**HashTree初始化
* 對根節點的值設置成0,但是被刪除的節點
*/
public HashTree(){
root=new Node(0, primeNumber[0]);
root.isDel=true;
}
/** 在HashTree中插入一個節點
* @param value 節點的值
*/
public void insertNode(int value){
//如果HashTree中已經有這個節點,就不插入
if(searchNode(value)){
return;
}
//排除root節點被刪除或者初始化的情況
if(root.isDel==true){
root.value=value;
root.isDel=false;
return;
}
//層級,在每一層,primeNumber[level]爲下一層的節點數
int level=0;
Node nowNode=root;
while(true){
//得到下個節點的位置(因爲已經考慮了root,可以直接考慮下一層的情況)
int index=value%(primeNumber[level]);
if(nowNode.next[index]==null){
//在第n層,primeNumber[level]爲n+1層的節點數,primeNumber[level+1]爲n+2層的節點數
//在第n層nowNode.next[index]爲第n+1層的節點,它的next的數量爲n+2層的節點數
nowNode.next[index]=new Node(value, primeNumber[level+1]);
break;
}
//將被刪除的節點更新爲當前值
if(nowNode.next[index].isDel==true){
nowNode.next[index].value=value;
nowNode.next[index].isDel=false;
break;
}
//到下一個對應節點
nowNode=nowNode.next[index];
level++;
}
}
/**在HashTree中查詢節點是否存在
* @param value 節點的值
* @return 存在,返回true 不存在,返回false
*/
public boolean searchNode(int value){
//考慮root是查找節點
if(root.value==value&&root.isDel==false){
return true;
}
//層級,在每一層,primeNumber[level]爲下一層的節點數
int level=0;
Node nowNode=root;
while(true){
//得到下個節點的位置(因爲已經考慮了root,可以直接考慮下一層的情況)
int index=value%(primeNumber[level]);
//如果對應節點爲空,直接返回false
if(nowNode.next[index]==null){
return false;
}
//如果對應節點沒有被刪除而且值相同,直接返回false
if(nowNode.next[index].isDel==false&&nowNode.next[index].value==value){
return true;
}
//到下一個對應節點
nowNode=nowNode.next[index];
level++;
}
}
/**在HashTree中刪除值爲value的節點
* @param value 節點的值
* @return 如果刪除成功,返回true 如果HashTree中沒有這個節點或者已經被刪除,返回false
*/
public boolean deleteNode(int value){
//考慮root是被刪除節點
if(root.value==value&&root.isDel==false){
root.isDel=true;
return true;
}
//層級,在每一層,primeNumber[level]爲下一層的節點數
int level=0;
Node nowNode=root;
while(true){
//得到下個節點的位置(因爲已經考慮了root,可以直接考慮下一層的情況)
int index=value%(primeNumber[level]);
//如果對應節點爲空,直接返回false
if(nowNode.next[index]==null){
return false;
}
//如果對應節點沒有被刪除而且值相同,進行刪除,返回true
if(nowNode.next[index].isDel==false&&nowNode.next[index].value==value){
nowNode.next[index].isDel=true;
return true;
}
//到下一個對應節點
nowNode=nowNode.next[index];
level++;
}
}
}
測試
package datastructure.tree.hashtree;
public class Main {
public static void main(String[] args) {
HashTree tree=new HashTree();
System.out.println(tree.root);
tree.insertNode(2);
tree.insertNode(3);
tree.insertNode(4);
tree.insertNode(4);
System.out.println(tree.root);
System.out.println(tree.searchNode(3));
tree.deleteNode(3);
System.out.println(tree.root);
System.out.println(tree.searchNode(3));
System.out.println(tree.searchNode(2));
System.out.println(tree.searchNode(4));
}
}
哈希樹的應用
從上面的分析可以看出哈希樹是一種可以自適應的樹。通過給出足夠多的不同質數,我們總可以將所有已經出現的關鍵字進行區別。而質數本身就是無窮無盡的。這種方式使得關鍵字空間和地址空間不再是壓縮對應方式,而是完全可以等價的。
哈希樹可以廣泛應用於那些需要對大容量數據進行快速匹配操作的地方。例如:數據庫索引系統、短信息中的收條匹配、大量號碼路由匹配、信息過濾匹配。程序員可以利用各種代碼來實現哈希樹結構。它可以爲程序員提供一種使用起來更加方便,更加簡單的快速數據存儲方式。