Java之哈希表

一列键值对数据,存储在一个table中,如何通过数据的关键字快速查找相应值呢?不要告诉我一个个拿出来比较key啊,呵呵。

大家都知道,在所有的线性数据结构中,数组的定位速度最快,因为它可通过数组下标直接定位到相应的数组空间,就不需要一个个查找,一次存取便能得到所查记录。而哈希表就是利用数组这个能够快速定位数据的结构解决以上的问题的。

具体如何做呢?大家是否有注意到前面说的话:“数组可以通过下标直接定位到相应的空间”,对就是这句,哈希表的做法其实很简单,就是把Key通过一个固定的算法函数,既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里,而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位

不知道说到这里,一些不了解的朋友是否大概了解了哈希表的原理,其实就是通过空间换取时间的做法。到这里,可能有的朋友就会问,哈希函数对key进行转换,取余的值一定是唯一的吗?这个当然不能保证,主要是由于hashcode会对数组长度进行取余,因此其结果由于数组长度的限制必然会出现重复,所以就会有“冲突”这一问题,至于解决冲突的办法其实有很多种,比如重复散列的方式,大概就是定位的空间已经存在value且key不同的话就重新进行哈希加一并求模数组元素个数,既 (h(k)+i) mod S , i=1,2,3…… ,直到找到空间为止。还有其他的方式大家如果有兴趣的话可以自己找找资料看看。

Hash表这种数据结构在java中是原生的一个集合对象,在实际中用途极广,主要有这么几个特点:
    访问速度快
    大小不受限制
    按键进行索引,没有重复对象
    用字符串(id:string)检索对象(object)

一、 Hash表概念

    在查找表中我们已经说过,在Hash表中,记录在表中的位置和其关键字之间存在着一种确定的关系。这样我们就能预先知道所查关键字在表中的位置,从而直接通过下标找到记录。使ASL趋近与0.
1)   哈希(Hash)函数是一个映象,即:将关键字的集合映射到某个地址集合上,它的设置很灵活,只要这个地址集合的大小不超出允许范围即可;
2)  由于哈希函数是一个压缩映象,因此,在一般情况下,很容易产生“冲突”现象,即: key1¹ key2,而  f(key1) = f(key2)。
3).  只能尽量减少冲突而不能完全避免冲突,这是因为通常关键字集合比较大,其元素包括所有可能的关键字,而地址集合的元素仅为哈希表中的地址值

哈希方法需要解决以下两个问题:
1.构造好的哈希函数
(1)所选函数尽可能简单,以便提高转换速度。
(2)所选函数对关键码计算出的地址,应在哈希地址集中大致均匀分布,以减少空间
浪费。
2.制定解决冲突的方案。

二、哈希函数构造及适用范围

2.1、直接定址法:
      哈希函数为关键字的线性函数,H(key) = key 或者 H(key) = a ´ key + b
      此法仅适合于:地址集合的大小 等于关键字集合的大小,其中a和b为常数。不会产生冲突,对于较大的关键码集合不适用。

《Java之哈希表》
2.2、数字分析法:
     假设关键字集合中的每个关键字都是由 s 位数字组成 (u1, u2, …, us),分析关键字集中的全体,并从中提取分布均匀的若干位或它们的组合作为地址。
     此法适于:能预先估计出全体关键字的每一位上各种数字出现的频度。

2.3、平方取中法:
     以关键字的平方值的中间几位作为存储地址。求“关键字的平方值” 的目的是“扩大差别” ,同时平方值的中间各位又能受到整个关键字中各位的影响。
    此法适于:关键字中的每一位都有某些数字重复出现频度很高的现象。

2.4、折叠法:
     将关键字分割成若干部分,然后取它们的叠加和为哈希地址。两种叠加处理的方法:
     移位叠加:将分割后的几部分低位对齐相加;
     间界叠加:从一端沿分割界来回折叠,然后对齐相加。
     此法适于:关键字的数字位数特别多。

2.5、除留余数法:
     设定哈希函数为:H(key) = key MOD p   ( p≤m ),其中, m为表长,对p的选择很重要,一般取不大于 m 的素数,或是不含 20 以下的质因子,若p选的不好,容易产生同义词

2.6、随机数法:
     设定哈希函数为:H(key) = Random(key)其中,Random 为伪随机函数
     此法适于:对长度不等的关键字构造哈希函数。

实际造表时,采用何种构造哈希函数的方法取决于建表的关键字集合的情况(包括关键字的范围和形态),以及哈希表长度(哈希地址范围),总的原则是使产生冲突的可能性降到尽可能地小。

那么确定了哈希函数之后,就要解决哈希冲突的问题,常用的方法如下

三、Hash处理冲突方法,各自特征

“处理冲突””的实际含义是:为产生冲突的关键字寻找下一个哈希地址.
3.1、开放定址法:
为产生冲突的关键字地址 H(key) 求得一个地址序列: H0, H1, H2, …, Hs  1≤s≤m-1,Hi = ( H(key) +di  ) MOD m,其中: i=1, 2, …, s,H(key)为哈希函数;m为哈希表长;

《Java之哈希表》

3.2、链地址法(拉链法):将所有哈希地址相同的记录都链接在同一链表中。

当存储结构是链表时,多采用拉链法,用拉链法处理冲突的办法是:把具有相同散列地址的关键字(同义词)值放在同一个单链表中,称为同义词链表。有m个散列地址就有m个链表,同时用指针数组T[0..m-1]存放各个链表的头指针,凡是散列地址为i的记录都以结点方式插入到以T[i]为指针的单链表中。T中各分量的初值应为空指针。

《Java之哈希表》
3.3、再哈希法:
方法:构造若干个哈希函数,当发生冲突时,根据另一个哈希函数计算下一个哈希地址,直到冲突不再发 生。即:Hi=Rhi(key)  i=1,2,……k,其中:Rhi——不同的哈希函数,特点:计算时间增加

四、Hash查找过程

对于给定值 K,计算哈希地址 i = H(K),若 r[i] = NULL  则查找不成功,若 r[i].key = K  则查找成功, 否则 “求下一地址 Hi” ,直至r[Hi] = NULL  (查找不成功)  或r[Hi].key = K  (查找成功) 为止。

《Java之哈希表》

五、实现一个使用Hash存数据的场景——-Hash查找算法,插入算法

     假设我们要设计的是一个用来保存中南大学所有在校学生个人信息的数据表。因为在校学生数量也不是特别巨大(8W?),每个学生的学号是唯一的,因此,我们可以简单的应用直接定址法,声明一个10W大小的数组,每个学生的学号作为主键。然后每次要添加或者查找学生,只需要根据需要去操作即可。

      但是,显然这样做是很脑残的。这样做系统的可拓展性和复用性就非常差了,比如有一天人数超过10W了?如果是用来保存别的数据呢?或者我只需要保存20条记录呢?声明大小为10W的数组显然是太浪费了的。

     如果我们是用来保存大数据量(比如银行的用户数,4大的用户数都应该有3-5亿了吧?),这时候我们计算出来的HashCode就很可能会有冲突了, 我们的系统应该有“处理冲突”的能力,此处我们通过挂链法“处理冲突”。

     如果我们的数据量非常巨大,并且还持续在增加,如果我们仅仅只是通过挂链法来处理冲突,可能我们的链上挂了上万个数据后,这个时候再通过静态搜索来查找链表,显然性能也是非常低的。所以我们的系统应该还能实现自动扩容,当容量达到某比例后,即自动扩容,使装载因子保存在一个固定的水平上。

综上所述,我们对这个Hash容器的基本要求应该有如下几点:
满足Hash表的查找要求(废话)
能支持从小数据量到大数据量的自动转变(自动扩容)
使用挂链法解决冲突

 public class MyMap<K, V> {
        private int size;// 当前容量  
        private static int INIT_CAPACITY = 16;// 默认容量  
        private Entry<K, V>[] container;// 实际存储数据的数组对象  
        private static float LOAD_FACTOR = 0.75f;// 装载因子  
        private int max;// 能存的最大的数=capacity*factor  

        // 自己设置容量和装载因子的构造器  
        public MyMap(int init_Capaticy, float load_factor) {
            if (init_Capaticy < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " + init_Capaticy);
            if (load_factor <= 0 || Float.isNaN(load_factor))
                throw new IllegalArgumentException("Illegal load factor: "
                        + load_factor);
            this.LOAD_FACTOR = load_factor;
            max = (int) (init_Capaticy * load_factor);
            container = new Entry[init_Capaticy];
        }

        // 使用默认参数的构造器  
        public MyMap() {
            this(INIT_CAPACITY, LOAD_FACTOR);
        }

        //存
        public boolean put(K k, V v) {
            // 1.计算K的hash值  
            // 因为自己很难写出对不同的类型都适用的Hash算法,故调用JDK给出的hashCode()方法来计算hash值  
            int hash = k.hashCode();
            //将所有信息封装为一个Entry  
            Entry<K,V> temp=new Entry(k,v,hash);
            if(setEntry(temp, container)){
                // 大小加一  
                size++;
                return true;
            }
            return false;
        }
        
        /**
         * 扩容的方法 
         *
         * @param newSize 新的容器大小 
         */
        private void reSize(int newSize) {
            // 1.声明新数组  
            Entry<K, V>[] newTable = new Entry[newSize];
            max = (int) (newSize * LOAD_FACTOR);
            // 2.复制已有元素,即遍历所有元素,每个元素再存一遍  
            for (int j = 0; j < container.length; j++) {
                Entry<K, V> entry = container[j];
                //因为每个数组元素其实为链表,所以…………  
                while (null != entry) {
                    setEntry(entry, newTable);
                }
            }
        }

点赞