哈希表

                                                              哈希表

1.什么是哈希表

int[26] freq 就是一个哈希表!每一个字符都和一个索引相对应!

哈希表

将我们关注的值,转化为索引,使用哈希函数。当然在大多数情况下,我们的哈希函数没有那么简单。

在实际中可能索引为字符串,或者为浮点数,日期,我们在使用哈希表的时候,都需要设计一个哈希函数转换为索引。

这是设计hashTable的第一个任务。

哈希表

我们在实际操作中,很难保证每一个“键”通过哈希函数的转换时,可能两个不同的键对应相同的索引,这就是哈希冲突。这也是hash表需要解决的第二个问题。

在设计hashTable时我们需要解决两个问题:

        1.哈希函数的设计

        2.哈希冲突

哈希表充分体现了算法设计领域的金典思想:空间换时间,哈希表是时间和空间之间的平衡

如果我们有 999999999999999999的空间,我们可以用O(1)的时间完成各项操作

如果我们有1的空间,我们只能用O(n)时间完成各项操作(线性表)

“键”通过哈希函数得到的“索引”分布越均匀越好

2哈希函数的设计

对于一些特殊的领域,有特殊领域的哈希函数设计方式,甚至有专门的论文,我们这里只关注哈希函数的一般设计原则。

a.整形(对于所有的类型都可以转换成整形)

哈希表

哈希表

对于上面这个身份证,其实就是取6666,我们知道空间越大那么就是哈希冲突越小。

哈希表

对于身份证我们取后6位,看起来很好其实倒数第6为为日期,日期最大为31,这就会造成分布不均匀。这个问题就是这个问题的陷阱,所以取模的这个数就很重要了,还有一个小的问题,没有利用所有的问题,直接将前面的信息给丢掉了。

哈希表

一个简单的办法:模一个素数(背后有强大的数学理论)

其实如果我们的大整数,都是均匀分布的,那么模任何一个数都是可以的;但是就怕这个大整数规律性特别强。看下面的这个:哈希冲突的规律比较大。

哈希表

关于素数的选择,有专门的人去研究,在这个网站中有详细的说明,可看出在不同的数据范围,我们可以选择合适的素数

http://planetmath.org/goodhashtableprimes

b.浮点型

哈希表

转成整形来处理,然后根据整形来处理就行

哈希表

b.字符串

转成整型处理

哈希表

哈希表

哈希表

哈希表

变形:

哈希表

有可能里面计算会有整形的溢出,我们可以将%M 移到括号的里面

哈希表

代码实现:

   int  hash  = 0;

   for(int i = 0 ; i < s.length(); i ++)

            hash = (hash * B + s.charAt(i)) % M

c.复合类型

比如一个学生有:姓名,学号,年级,日期等等

符合类型:转换成整型处理

哈希表

哈希表

哈希表

转换成整型处理,并不是唯一的方法!不管怎样都符合下面的原则

原则:

1. 一致性: 如果 a==b, 则hash(a) == hash(b)

2. 高效性: 计算高效简便

3. 均匀性:哈希均匀分布

3. 哈希冲突的处理

a. 链地址法(Seperate Chaining)

哈希表

哈希表

此时来了一个k3,k3与k2冲突了,我们将k3也存到1的位置,链表的存储方式,实现方式底层不见得是一个链表,我们也完全可以使用平衡树结构,我们可以存一个TreeMap,相应的我们来一个元素,找到这个索引位置的这个TreeMap,插进这个TreeMap里面就好了。

哈希表     HashMap   就是一个 TreeMap的数组; HashSet 就是一个TreeSet 数组

哈希表哈希表

一定程度:就是说每一个地址的平均冲突达到某一个值,就使用红黑树,其实是一个TreeMap(java中TreeMap就是使用红黑树实现的)当数据规模比较小的时候冲突小,使用链表要快。

b.使用链地址法处理冲突,哈希表的时间度分析

时间消耗,其实就是在链表的时间消耗上

哈希表

平均来看,非常差,全部发生地址冲突。那么链地址法就需要O(N)的时间复杂度。如果是平衡树:O(log(N)),这里如果是一个静态数组的话,我们的N无穷大的时候,那么N/M就无穷大了,所以我们的地址固定为M个显然不太合理。

改进的方案,空间应该随着N的改变自适应。

哈希表

哈希表的动态空间处理:

哈希表

我们同动态数组的均摊复杂度分析     平均复杂度为O(1)

哈希表

哈希表

哈希表

4. 哈希冲突的处理

前面我们讲到的链地址法,其实就是一个封闭地址法!

a.开放地址法( 每一个地址只存元素,如果冲突了就放到它的下一个地址中)

哈希表

开放地址里面的线性探测法

哈希表

线性探测法的性能比较低,消耗比较高,基于此有一些改进法,像平法探测法,先是直接+1,然后+4 +9,+16 等等

哈希表

哈希表

对于开发地址法,也有可能达到最大容积,此时也需要扩容,通常对于开放地址法有一个指标叫做负载率(存储总数/开放地址大小)对于开发地址法,只要负载率选择合适也是O(1)的时间复杂度。

使用TreeMap的数组来实现hashTable,代码如下:

public class HashTable<K, V> {
    private final int[] capacity
                         ={53,97,193,389,769,1543,3079,6151,12289,24593,
                           49157,98317,196613,393241,786433,1572869,3145739,
                           6291469,12582917,25165843,50331653,100663319,201326611,
                           402653189, 805306457, 1610612741};
    private static final int upperTol = 10;
    private static final int lowerTol = 2;
    private  int capacityIndex = 0;
    private TreeMap<K, V>[] hashtable;
    private int size;
    private int M;  // 长度

    public HashTable(){
        this.M = capacity[capacityIndex];
        size = 0;
        hashtable = new TreeMap[M];
        
        for(int i = 0 ; i < M ; i ++)
            hashtable[i] = new TreeMap<>();
        
    }

    
    // key可以是任意的类型,先转成整形
    private int hash(K key){
        return (key.hashCode() & 0x7fffffff) % M;
    }

    public void add(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];
        // if(!hashtable[hash(key)].containsKey(key)){
        if(!map.containsKey(key)){
            map.put(key, value);
            size ++;
            
            // N/M >= upperTol 防止整形到浮点转化
            if(size >= upperTol * M && capacityIndex + 1 < capacity.length)
                capacityIndex ++;
                resize(capacity[capacityIndex]);
        }
    }

    public V remove(K key){
        V ret = null;
        TreeMap<K, V> map = hashtable[hash(key)];
        if(map.containsKey(key)){
            ret = map.remove(key);
            size --;
            
            // M > initCapacity 至少要多少
            if(size <= lowerTol * M && capacityIndex - 1 >= 0)
                capacityIndex --;
                resize(capacity[capacityIndex]);
        }
        return ret;
    }

    public void set(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];
        if(!map.containsKey(key))
            throw new IllegalArgumentException(key + " doesn't exist!");

        map.put(key, value);
    }

    public boolean contains(K key){
        //hashtable[hash(key)] 找到TreeMap
        return hashtable[hash(key)].containsKey(key);
    }

    public V get(K key){
        return hashtable[hash(key)].get(key);
    }

    private void resize(int newM){
        TreeMap<K, V>[] newHashTable = new TreeMap[newM];
        for(int i = 0 ; i < newM ; i ++)
            newHashTable[i] = new TreeMap<>();
        
        // 将原先hashTable中放到新的hashTable中
        int oloM = M;
        this.M = newM;
        for(int i = 0 ; i < oloM ; i ++)
            for(K key: hashtable[i].keySet())
                newHashTable[hash(key)].put(key, hashtable[i].get(key));

        //this.M = newM;
        this.hashtable = newHashTable;
    }
}