Hashing

理想中的hash table仅仅是包含元素的固定大小的数组,有用来查询的key和数据字段value,我们假设表的大小为TableSize。
每一个key被映射到0到TableSize-1区间,并替换数组适当的单元格。这种映射方法叫hash函数,我们希望这个映射方法应该简单,并且能够让不同的key获得不同的hash值,然而由于我们有有限的单元格,但却需要支持无尽的key,显然是不可能的,因此我们需要找到一种hash函数,能够将key映射均匀的在这些单元格中。最后,当不同的key映射到相同的值时(通常称之为冲突),我们如何处理?

Hash Function

假如key是整型数值时,没有特殊情况的话,直接返回key mod TableSize即可。比如如果大小为10,但key又都是以0结尾的,那么这种方式显然不合理了。为了防止这种情况发生,我们通常设置TableSize为一个素数。
当key为字符串时,就需要多多考虑这个函数应该如何设计了。一种方式是将每一个字符的ASCII码相加:

public class Hashing {
    public static int hash(String key, int tableSize) {
        int hashVal = 0;
        
        for(int i = 0; i<key.length(); i++) {
            hashVal += key.charAt(i);
        }
        
        return hashVal % tableSize;
    }
}

这种方式简单快捷,但是当TableSize很大的时候,这个方法无法均匀的分布key。例如当大小为10007(素数)。假设key的长度为8或者更小,由于ASCII最大只有127因此key最多分布在1到1016之间。
另一种方式如下:

    public static int hash(String key, int tableSize) {
        return (key.charAt(0) + key.charAt(1) * 27 +key.charAt(2) * 729) % tableSize;
    }

假设key至少有三个字符,27代表英文的字母和一个空格,729是27的平方。这个方法仅仅考虑key的前3个字符,如上所述,虽然263=17576,但英文单词并不是完全随机的,通过字典可以查到常用的单词也就2851个,即使这些完全没有碰撞,但只有28%的区间被映射到,因此这个方法也是不太适合的。
那么我们可以将所有的字符都考虑进来,并生成一个和质数相关的多项式,同时也要考虑可能存在溢出的情况:

    public static int hash(String key, int tableSize) {
        int hashVal = 0;
        for( int i = 0; i<key.length(); i++) {
            hashVal = 37 * hashVal + key.charAt(i);
        }
        
        hashVal = hashVal % tableSize;
        if( hashVal < 0) {
            hashVal += tableSize;
        }
        
        return hashVal;
    }

当然这种方式也不一定是最优的,并且如果key特别长的话将会耗费大量的时间,我们可以看到JDK中字符串的hashcode的实现方式是:

    public static int hashCode(byte[] value) {
        int h = 0;
        int length = value.length >> 1;

        for(int i = 0; i < length; ++i) {
            h = 31 * h + getChar(value, i);
        }

        return h;
    }

剩下的就是需要解决冲突问题了,当两个key有相同的hash结果时,将会发生冲突。有很多种方法来解决这个问题,最主要的两种是独立链表和开放式寻址。

独立链表

第一种方式是独立链表(separate chaining),将所有相同hash值存放在一个链表中。通过使用hash函数来决定在哪个链表中进行遍历,来实现查找。当插入值时,我们hash函数计算后,如果已经有值,就在该值得头部插入,因为通常最近插入的使用会更频繁。

import java.util.LinkedList;
import java.util.List;

/**
 * @author dalu
 * @create 2020-04-12 22:01
 */
public class SeparateChainingHashTable <AnyType> {
    public SeparateChainingHashTable() {
        this(DEFAULT_TABLE_SIZE);
    }

    public SeparateChainingHashTable(int size) {
        theLists = new LinkedList[nextPrime(size)];
        for(int i = 0; i < theLists.length; i++) {
            theLists[i] = new LinkedList<>();
        }
    }

    public void insert(AnyType x) {
        List<AnyType> whichList = theLists[myhash(x)];
        if( !whichList.contains(x)) {
            whichList.add(x);

            if(++currentSize > theLists.length) {
                rehash();
            }
        }

    }

    public void remove(AnyType x) {
        List<AnyType> whichList = theLists[myhash(x)];
        if(whichList.contains(x)) {
            whichList.remove(x);
            currentSize--;
        }

    }

    public boolean contains(AnyType x) {
        List<AnyType> whichList = theLists[myhash(x)];
        return whichList.contains(x);
    }

    public void makeEmpty() {
        for(int i = 0; i < theLists.length; i++) {
            theLists[i].clear();
        }
        currentSize = 0;
    }

    private static final int DEFAULT_TABLE_SIZE = 101;

    private List<AnyType> [] theLists;

    private int currentSize;

    private void rehash() {
        List<AnyType> [] oldLists = theLists;
        
        theLists = new List[nextPrime(2 * theLists.length)];
        for(int j = 0; j < theLists.length; j++) {
            theLists[j] = new LinkedList<>();
        }
        currentSize = 0;
        for(int i = 0; i < oldLists.length; i++) {
            for(AnyType item : oldLists[i]) {
                insert(item);
            }
        }

    }

    private int myhash(AnyType x) {
        int hashVal = x.hashCode();

        hashVal %= theLists.length;
        if(hashVal < 0) {
            hashVal += theLists.length;
        }
        return hashVal;

    }

    private static int nextPrime(int n) {
        if(n % 2 == 0) {
            n++;
        }
        for(; !isPrime(n); n += 2) {

        }
        return n;

    }

    private static boolean isPrime(int n) {
        if(n == 2 || n == 3) {
            return true;
        }

        if(n == 1 || n % 2 == 0) {
            return false;
        }

        for(int i = 3; i * i <= n; i += 2) {
            if(n % i == 0) {
                return  false;
            }
        }
        return  true;

    }
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容