1. 程式人生 > 其它 >雜湊演算法與雜湊碼

雜湊演算法與雜湊碼

一、引入

 1 /**
 2  * Description:新建一個類作為map的key
 3  */
 4 public class Groundhog
 5 {
 6     protected int number;
 7 
 8     public Groundhog(){
 9     }
10     public Groundhog(int number)
11     {
12         this.number = number;
13     }
14 
15     @Override
16     public String toString()
17     {
18         return "Groundhog{" + "number=" + number + '}';
19     }
20 }
21 
22 /**
23  * Description:新建一個類作為map的value
24  */
25 public class Prediction
26 {
27     private boolean shadow=Math.random() > 0.5;
28 
29     @Override
30     public String toString()
31     {
32         if (shadow) return "Six more weeks of Winter";
33         else return "Early Spring!";
34     }
35 }
36 
37 /**
38  * Description:測試類
39  */
40 public class SpringDetector
41 {
42     public static void detectSpring(Class grondHotClass) throws Exception{
43         Constructor constructor = grondHotClass.getConstructor(new Class[]{int.class});
44         Map map=new HashMap();
45         for (int i=0;i<10;i++){
46             map.put(constructor.newInstance(new Object[]{new Integer(i)}),new Prediction());
47         }
48         System.out.println("map="+map);
49 
50         Groundhog groundhog=(Groundhog)constructor.newInstance(new Object[]{new Integer(3)});
51         System.out.println(groundhog);
52 
53         if (map.containsKey(groundhog)) {//查詢這個key是否存在
54             System.out.println((Prediction)map.get(groundhog));
55         }else {
56             System.out.println("key not find:"+groundhog);
57         }
58 
59     }
60     public static void main(String[] args)
61     {
62         try {
63             detectSpring(Groundhog.class);
64         } catch (Exception e) {
65             e.printStackTrace();
66         }
67     }
68 }

    看這個結果,問題就來了,map中明明存在Groudhog{number=3},為什麼結果顯示的是Key not find呢??問題出在哪裡呢?原來是Groudhog類沒有重寫hashCode()方法,所以這裡是使用Object的hashCode()方法生成雜湊碼,而他預設是使用物件的地址計算雜湊碼。因此,由Groudhog(3)生成的第一個例項的雜湊碼與Groudhog(3)生成的雜湊碼是不同的,所以無法查詢到 key。但是僅僅重寫hashCode()還是不夠的,除非你重寫equals()方法。原因在於不同的物件可能計算出同樣的hashCode的值,hashCode 的值並不是唯一的,當hashCode的值一樣時,就會使用equals()判斷當前的“鍵”是否與表中的存在的鍵“相同”,即“

如果兩個物件相同,那麼他們的hashCode值一定相同。

如果兩個物件的hashCode值相同,他們不一定相同。

    正確的equals()方法必須滿足下列5個條件:

1、自反性: x.equals(x) 一定成立。

2、對稱性: 如果x.equals(y)成立,那麼y.equals(x)也一定成立。

3、傳遞性:如果x.equals(y)=true ,y.equals(z)=true,那麼x.equals(z)=true也成立。

4、一致性:無論呼叫x.equal(y)多少次,返回的結果應該保持一致。

5、對任何不是null的x,x.equals(null)一定返回false。

二、理解hashCode()

     雜湊的價值在於速度:雜湊使得查詢得以快速執行。由於速度的瓶頸是對“鍵”進行查詢,而儲存一組元素最快的資料結構是陣列,所以用它來代表鍵的資訊,注意:陣列並不儲存“鍵”的本身。而通過“鍵”物件生成一個數字,將其作為陣列的下標索引。這個數字就是雜湊碼,由定義在Object的hashCode()生成(或成為雜湊函式)。同時,為了解決陣列容量被固定的問題,不同的“鍵”可以產生相同的下標。那對於陣列來說?怎麼在同一個下標索引儲存多個值呢??原來陣列並不直接儲存“值”,而是儲存“值”的 List。然後對 List中的“值”使用equals()方法進行線性的查詢。這部分的查詢自然會比較慢,但是如果有好的雜湊函式,每個下標索引只儲存少量的值,只對很少的元素進行比較,就會快的多。

    不知道大家有沒有理解我上面在說什麼。不過沒關係,下面會有一個例子幫助大家理解。不過我之前一直被一個問題糾結:為什麼一個hashCode的下標存的會有多個值?因為hashMap裡面只能有唯一的key啊,所以只能有唯一的value在那個下標才對啊。這裡就要提出一個新的概念雜湊衝突的問題,借用網上的一個例子:

    比如:陣列的長度是5。這時有一個數據是6。那麼如何把這個6存放到長度只有5的陣列中呢。按照取模法,計算6%5,結果是1,那麼就把6放到陣列下標是1的位置。那麼,7就應該放到2這個位置。到此位置,雜湊衝突還沒有出現。這時,有個資料是11,按照取模法,11%5=1,也等於1。那麼原來陣列下標是1的地方已經有數了,是6。這時又計算出1這個位置,那麼陣列1這個位置,就必須儲存兩個數了。這時,就叫雜湊衝突。衝突之後就要按照順序來存放了。所以這裡Java中用的解決方法就是在這個hashCode上存一個List,當遇到相同的hashCode時,就往這個List裡add元素就可以了。這才是hash原理的精髓所在啊!哈哈、糾結我一天。

三、HashMap的效能因子

容量(Capacity):散列表中的數量。

初始化容量(Initial capacity):建立散列表時桶的數量。HashMap 和 HashSet都允許你在構造器中制定初始化容量。

尺寸(Size):當前散列表中記錄的數量。

負載因子(Load factor):等於"size/capacity"。負載因子為0,表示空的散列表,0.5表示半滿的散列表,依次類推。輕負載的散列表具有衝突少、適宜插入與適宜查詢的特點(但是使用迭代器遍歷會變慢)。HashMap和hashSet的構造器允許你制定負載因子。這意味著,當負載達到制定值時,容器會自動成倍的增加容量,並將原有的物件重新分配,存入新的容器內(這稱為“重雜湊”rehashing)。HashMap預設的負載因子為0.75,這很好的權衡了時間和空間的成本。

備註:為使雜湊分佈均衡,Java的雜湊函式都使用2的整數次方來作為散列表的理想容量。對現代的處理器來說,除法和求餘是最慢的動作。使用2的整數次方的散列表,可用掩碼代替除法。因為get()是使用最多的操作,求餘數的%操作是其開銷的大部分,而使用2的整數次方可以消除此開銷(也可能對hashCode()有些影響)

四、怎麼重寫hashCode()

現在的IDE工具中,一般都能自動的幫我們重寫了hashCode()和equals()方法,但那或許並不是最優的,重寫hashCode()有兩個原則:

  • 必須速度快,並且必須有意義。也就是說,它必須基於物件的內容生成雜湊碼。
  • 應該產生分佈均勻的雜湊碼。如果雜湊碼都集中在一塊,那麼在某些區域的負載就會變得很重。

下面是怎麼寫出一份像樣的hashCode()的基本指導:

1、給int變數result 賦予某個非零值常量,例如 17。

2、為每個物件內每個有意義的屬性f (即每個可以做equals()的屬性)計算出一個 int 雜湊碼c:

3、合併計算得到的雜湊值:result=37*result+c;

4、返回 result;

5、檢查hashCode()最後生成的結果,確保相同的物件有相同的雜湊碼。

五、自定義HashMap

下面我們將自己寫一個hashMap,便於瞭解底層的原理,大家如果看的懂下面的程式碼,也就很好的理解了hashCode的原理了。

/**
 * Description:首先新建一個類作為map中儲存的物件並重寫了hashCode()和equals()方法
 */
public class MPair implements Map.Entry,Comparable
{
    private Object key,value;

    public MPair(Object key,Object value)
    {
        this.key = key;
        this.value=value;
    }
    @Override
    public int compareTo(Object o)
    {
        return ((Comparable)key).compareTo(((MPair)o).key);
    }
    
    @Override
    public Object getKey()
    {
        return key;
    }

    @Override
    public Object getValue()
    {
        return value;
    }

     @Override
    public int hashCode()
    {
        int result = key != null ? key.hashCode() : 0;
        result = 31 * result + (value != null ? value.hashCode() : 0);
        return result;
    }

    @Override
    public boolean equals(Object o)
    {
        return key.equals(((MPair)o).key);
    }

    @Override
    public Object setValue(Object v)
    {
        Object result=value;
        this.value=v;
        return result;
    }
    
    @Override
    public String toString()
    {
        return "MPair{" + "key=" + key + ", value=" + value + '}';
    }
public class SimpleHashMap extends AbstractMap
{
    
    private static final int SZ=3;//定一個初始大小的雜湊表容量
    private LinkedList[] linkedLists=new LinkedList[SZ];//建一個hash陣列,用linkedList實現
    public Object put(Object key,Object value){
        Object result=null;
        int index=key.hashCode() % SZ;//對key的值做求模法求出index
        if (index<0) index=-index;
        if (linkedLists[index]==null) linkedLists[index]=new LinkedList();//如果這個index位置沒有物件,就新建一個

        LinkedList linkedList = linkedLists[index];//取出這個index的物件linkedList
        MPair mPair = new MPair(key,value);//新建要儲存的物件mPair
        ListIterator listIterator = linkedList.listIterator();
        boolean found =false;
        while (listIterator.hasNext()){//遍歷這個index位置的List,如果查詢到跟之前一樣的物件(根據equals來比較),則更新那個key對應的value
            Object next = listIterator.next();
            if (next.equals(mPair)){
                result = ((MPair) next).getValue();
                listIterator.set(mPair);//更新動作
                found=true;
                break;
            }
        }
        if (!found) linkedLists[index].add(mPair);//如果沒有找到這個物件,則在這index的List物件上新增一個元素。
        return result;

    }

    public Object get(Object key){
        int index = key.hashCode() % SZ;
        if (index<0) index=-index;
        if (linkedLists[index]==null) return null;
        LinkedList linkedList = linkedLists[index];
        MPair mPair=new MPair(key,null);//新建一個空的物件值,因為equals()的比較是看他們的key是否相等,而在List中的遍歷物件的時候,是通過key來查詢物件的。
        ListIterator listIterator = linkedList.listIterator();
        while (listIterator.hasNext()){
            Object next = listIterator.next();
            if (next.equals(mPair)) return ((MPair)next).getValue();//找到了這個key就返回這個value
        }
        return null;

    }

    @Override
    public Set<Entry> entrySet()
    {
        Set set=new HashSet();
        for (int i=0;i<linkedLists.length;i++){
            if (linkedLists[i]==null) continue;
            Iterator iterator = linkedLists[i].iterator();
            while (iterator.hasNext()){
                set.add(iterator.next());
            }
        }
        return set;
    }

    public static void main(String[] args)
    {
       SimpleHashMap simpleHashMap=new SimpleHashMap();
        simpleHashMap.put("1", "1");
        simpleHashMap.put("2", "2");
        simpleHashMap.put("3","3");
        simpleHashMap.put("4","4");//這裡有四個元素,其中key是1和key是4的index是一樣的,所以index為1的List上面存了兩個元素。
        System.out.println(simpleHashMap);
        Object o = simpleHashMap.get("1");
        System.out.println(o);
        Object o1 = simpleHashMap.get("4");
        System.out.println(o1);

    }
}

六、結語

不知道大家理解了沒?整了我一天,終於還算大概理解了其中的原理了。文筆比較粗糙,大家湊活看吧,畢竟,不會做飯的作家不是好程式設計師啊!哈哈...... 或者,可能我有很多理解的不到位的地方,還請大家不吝指教!