前言:

   对于Key-Value集合,想象两种极端的情况:如果没有内存的限制,我们可以直接将键作为一个数组的索引,那么所有的查找只需一次就可以完成;另一方面,如果没有时间的限制,那么我们就可以使用无序的数组进行顺序查找,这样只需要很少的内存。然而这两种情况几乎见不到,作为在时间和空间上做出权衡的经典方法,使用散列表可以在大部分情况下实现常数级别的查找和插入操作!

 

一,散列函数

     我们需要有一个将Key转化为数组索引的方法,这个方法就是散列函数。如果我们有一个能保存M个键值对的数组,那么这个散列函数就能够将任意Key转化为【0,M-1】之间的索引。

散列函数和键的类型有关:严格来说:每一种类型的键都有一个对应的散列函数!

Key的常见情况:

正整数:

  常用的方法是除留取余法,选择一个大小为素数M的数组,对于任意正整数K,计算K%M,选择素数的原因是使用素数,散列值的分布会更好!

浮点数:

如果键是0到1之间的小数,可以将他乘以M并四舍五入得到一个【0,M】之间的值,

但是有很大的缺陷,这种情况下键的高位起的作用更大,而最低为几乎没有影响!

比如0.554433*100=55;显然,4433这四位不起任何作用。而在Java里,对这种情况的处理是将键转化为二进制然后再使用除留取余法。

字符串:

除留取余法也可以处理字符串,只要我们将他们当做大整数处理即可。

例如:计算String的哈希值。

 

    

1   int hash=0;
2 
3            for(int i=0;i<s.length();i++)
4 
5            {
6 
7                   hash=(R*hash+s.charAt(i))%M;
8 
9            }

 

charAt()返回一个char,即非负16位整数,如果R足够小,不造成溢出,那摩结果就能落在【0,M】之间。事实上,Java中的String的hashCode()函数就是使用类似的 方法实现的。源码如下:

     

 1 public int hashCode() {
 2 
 3         int h = hash;
 4 
 5         if (h == 0 && value.length > 0) {
 6 
 7             char val[] = value;
 8 
 9  
10 
11             for (int i = 0; i < value.length; i++) {
12 
13                 h = 31 * h + val[i];
14 
15             }
16 
17             hash = h;
18 
19         }
20 
21         return h;
22 
23     }

 

组合键:

 对于组合键的处理,我们可以像处理String那样将他们混合起来,如果键的类型为Date,那么可以采用下列方法:

int hash=(((day*R+month)%M)*R+year)%M;

//R同String中的R

Java中的约定:

 Java要求每种类型都需要相应的散列函数,所以他们都继承了一个能返回32比特整数的hashCode()方法。但是每一种数据类型的hashCode()方法都需要和equals()方法一致,即如果a.equals(b)==true;那么一定会有a.hashCode()==b.hashCode();反之则不然,因为这里存在着哈希碰撞的问题,因此,如果你要自己实现hashCode(),请不要忘记实现equals();

但是因为hashCode默认为32位比特,在实际使用中,我们可以使用除留取余法将他转化为一个合理的整数。例如:

private int hash(K key)

        {

           return (key.hashCode()&0x7fffffff)%M;

  }//这段代码会屏蔽符号位,将其转化为31位非负整数。

自定义hashCode

   其实现在的编译器,如eclipse,IDEA,vscode(需使用插件)等都有自动生成hashCode()的功能,很多时候只需要点两下鼠标就行,如果你真想自己写哈希函数,请记住以下三个原则:

   一致性——等价的键必定产生相等的哈希值

 高效性——计算简便

均匀性——均匀散列所有键

 

 

 

基于拉链法的散列表

 

Java 标准库的 HashMap 基本上就是用 拉链法 实现的。 拉链法 的实现比较简单,将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

 

 

 

 

我们需要使用M条链表来保存N个键,无论键的分布如何,链表的平均长度肯定为N/M;具体实现代码如下:

 

  定义链表,采用顺序搜索:

  

 public class SequentialSearch<K,V>{

  private Node first;

  

   private class Node

   {

      K key;

      V value;

      Node next;

     

      public Node(K k,V v,Node next)

      {

         this.key=k;

         this.value=v;

         this.next=next;

        

      }

   }

  

   public V get(K key)

   {

      for(Node x=first;x!=null;x=x.next)

         if(key.equals(x.key))

             return x.value;

      return null;

   }

  

   public void put(K key,V value)

   {

      for(Node x=first;x!=null;x=x.next)

         if(key.equals(x.key))

         {

            x.value=value;

            return ;

         }

      first=new Node(key,value,first);

   }

}
View Code

 

 

 

基于拉链法的散列表:

 

 1 public class ChainHash<K,V> {
 2 
 3   
 4 
 5   private int N;
 6 
 7   private int M;
 8 
 9   private SequentialSearch<K,V>[] array;
10 
11  
12 
13   public ChainHash()
14 
15   {
16 
17     
18 
19   }
20 
21   @SuppressWarnings("unchecked")
22 
23   public ChainHash(int size)
24 
25   {
26 
27      this.M=size;
28 
29      array= (SequentialSearch<K,V>[])new SequentialSearch[M];
30 
31 //Java不允许创建泛型数组,需要强制转换
32 
33      for(int i=0;i<M;i++)
34 
35      {
36 
37         array[i]=new SequentialSearch();
38 
39      }
40 
41   }
42 
43  
44 
45   private int hash(K key)
46 
47   {
48 
49      return (key.hashCode()&0x7fffffff)%M;
50 
51   }
52 
53  
54 
55   public V get(K key)
56 
57   {
58 
59     return  (V)array[hash(key)].get(key);
60 
61   }
62 
63  
64 
65   public void put(K key,V value)
66 
67   {
68 
69        array[hash(key)].put(key, value);
70 
71   }
72 
73 }
74 
75  
View Code

 

 

线性探测法

   用大小为M的数组保存N个键值对

通过散列函数hash(key),找到关键字key在线性序列中的位置,如果当前位置已经有了一个关键字,就长生了哈希冲突,我们直接检查散列表的下一位置, 此时会产生三种结果:

  • *命中:该位置的键和被查找的键相同(更新value)
  • *未命中:该位置没有键(插入相关Key-Value)
  • *继续查找:该位置的键和被查找的键不同

 

 

 

 

 

代码实现:

    这里使用了并行数组,一条保存键,一条保存值

     

  1 public class LineHash<K,V> {
  2 
  3  
  4 
  5    private int N;
  6 
  7    private int M=16;
  8 
  9    private K[] keys;
 10 
 11    private V[] values;
 12 
 13   
 14 
 15    public LineHash()
 16 
 17    {
 18 
 19       keys=(K[])new Object[M];
 20 
 21       values=(V[]) new Object[M];
 22 
 23    }
 24 
 25   
 26 
 27    private int hash(K key)
 28 
 29    {
 30 
 31       return (key.hashCode()&0x7fffffff)%M;
 32 
 33    }
 34 
 35    private void resize()
 36 
 37    {
 38 
 39       //TODO
 40 
 41    }
 42 
 43   
 44 
 45    public void put(K key,V value)
 46 
 47    {
 48 
 49       int i;
 50 
 51       for(i=hash(key);keys[i]!=null;i=(i+1)%M)
 52 
 53       {
 54 
 55          if(keys[i].equals(key))
 56 
 57          {
 58 
 59             values[i]=value;
 60 
 61             return;
 62 
 63          }
 64 
 65       }
 66 
 67      
 68 
 69       keys[i]=key;
 70 
 71       values[i]=value;
 72 
 73       N++;
 74 
 75    }
 76 
 77   
 78 
 79    public V get(K key)
 80 
 81    {
 82 
 83       int i;
 84 
 85       for(i=hash(key);keys[i]!=null;i=(i+1)%M)
 86 
 87       {
 88 
 89          if(keys[i].equals(key))
 90 
 91             return values[i];
 92 
 93       }
 94 
 95       return null;
 96 
 97    }
 98 
 99   
100 
101    @Override
102 
103    public String toString()
104 
105    {
106 
107       StringBuilder sb=new StringBuilder();
108 
109       for(int i=0;i<M;i++)
110 
111       {
112 
113          sb.append(i+"  ").append(keys[i]).append("= ").append(values[i]).append("\n");
114 
115       }
116 
117       return sb.toString();
118 
119    }
120 
121   
122 
123 }
View Code