拉链法和线性探测法

散列函数

  1. 正整数
    除留余数法,选择大小为素数M的数组,对于任意正整数k ,计算k除以M的余数。
    如果M不是素数,我们可能无法利用键中包含的所有信息,这可能导致我们无法均匀地散列散列值

  2. 浮点数
    第一,如果键是0-1的实数,我们可以将它乘 M 并四舍五入得到一个0~M-1 之间的索引,有缺陷,高位起的作用更大,最低位对散列值得结果没影响。
    第二,将键表示为二进制,然后试用版除留余数法。

  3. 字符串
    基本原理也是除留余数法,Horner方法
    int hash = 0;
    for(int i = 0 ; i<s.length(); i++){
    hash = (R * hash +s.charAt(i)) % M;
    }
    //R代表进制

  4. 组合键
    eg ,年月日
    int hash = (((day * R +month) % M) * R +year) % M;

  5. Java约定
    第一,所有数据类型都集成了一个能返回一个32bit整数的hashCode()方法。
    第二,每一种数据类型的hashCode()都必须和equals()一致
    第三,a.equals(b) 为true a.hashCode()= b.hashCode(); a,b的hashCode相同,并不一样是一个对象,需要用equal判断是否为同一对象。

  6. hashCode()的返回值转化为索引值
    hashCode()返回32位整数,而我们需要的是 0~M-1索引。转化方法如下

private int hash(Key x){
    return (x.hashCode() & 0x7fffffff) % M;
}
//x.hashCode() & 0xfffffff将符号位屏蔽,然后除留余数法计算。

  1. hashCode()
public class Transaction{
    private final String who;
    private final Date when;
    private final double amount;
    
    public int hashCode(){
        int hash  = 17;
        hash = 31 * hash + who.hashCode();
        hash = 31 * hash + when.hashCode();
       hash = 31 * hash + ammount.hashCode();
       return hash;
    }
}
  1. 软缓存
    如果散列值得计算很耗时,那么我们或许可以将每个键的散列值保存起来,即在每个键中使用一个hash变量保存它的hashCode()返回值
  2. 优秀的散列方法的三个条件
    第一,一致性 等价的键必然产生相等的散列值
    第二,高效性,计算简便
    第三,均匀地散列所有键

基于拉链法的散列表

定义:碰撞处理是指处理两个或多个键的散列值相同的情况。一种直接的办法是将大小为M的数组中的每个元素指向一条链表,链表中每个结点都存储了
散列值为该元素索引的键值对。这种方法称为拉链法。
让M足够大,这样每个链表的长度就越短。
查找分两步:根据散列值找到链表,然后沿着链表查找相应的键。
在一张含有M条链表 和N个键的散列表中,未命中查找和插入操作所需比较的次数为 ~N/M

基于线性探测法的散列表

另外一种,用大小M的数组保存N个键值对,其中 M > N。我们需要依靠数组中的空位解决碰撞冲突。基于这种策略的所有方法统称为 开放地址散列表
开放地址散列表最简单的方法叫做 线性探测法
定义:当碰撞发生时,我们直接检查散列表中的下一个位置。这样的线性探测有三种结果
第一,命中,该位置的键和被查找的键相同
第二,未命中,键为空(该位置没有键)
第三,继续查找,该位置的键和被查找的键不同

基于线性探测法的散列表:
插入思路:
第一:计算key的散列表,找到相应的位置
第二:查看这个位置的值是否为要插入的key。不是的话比较下一个位置 ,直到当前位置的键值为空。
第三:如果在遇到空键值前遇到相等的键,则进行更新操作;如果直到遇到空值也没遇到相同的键,在空键值位置插入相应键值。

public class LinearProbingHashST<Key, Value> {
    private static final int INIT_CAPACITY = 4;

    private int n;           // number of key-value pairs in the symbol table
    private int m;           // size of linear probing table
    private Key[] keys;      // the keys
    private Value[] vals;    // the values

    public LinearProbingHashST() {
        this(INIT_CAPACITY);
    }
    public LinearProbingHashST(int capacity) {
        m = capacity;
        n = 0;
        keys = (Key[])   new Object[m];
        vals = (Value[]) new Object[m];
    }

    private int hash(Key key) {
        return (key.hashCode() & 0x7fffffff) % m;
    }

    // resizes the hash table to the given capacity by re-hashing all of the keys
    private void resize(int capacity) {
        LinearProbingHashST<Key, Value> temp = new LinearProbingHashST<Key, Value>(capacity);
        for (int i = 0; i < m; i++) {
            if (keys[i] != null) {
                temp.put(keys[i], vals[i]);
            }
        }
        keys = temp.keys;
        vals = temp.vals;
        m    = temp.m;
    }
    public void put(Key key, Value val) {
        if (key == null) throw new IllegalArgumentException("first argument to put() is null");
        if (val == null) {
            delete(key);
            return;
        }
        // double table size if 50% full
        if (n >= m/2) resize(2*m);

        int i;
        for (i = hash(key); keys[i] != null; i = (i + 1) % m) {
            if (keys[i].equals(key)) {
                vals[i] = val;
                return;
            }
        }
        keys[i] = key;
        vals[i] = val;
        n++;
    }
    public Value get(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to get() is null");
        for (int i = hash(key); keys[i] != null; i = (i + 1) % m)
            if (keys[i].equals(key))
                return vals[i];
        return null;
    }
}

删除操作
键簇: 元素在插入数组后集聚而成的一组连续的条目。
我们需要将簇中被删除键的右侧的所有键重新插入到散列表

    public void delete(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to delete() is null");
        if (!contains(key)) return;

        // find position i of key
        int i = hash(key);
        while (!key.equals(keys[i])) {
            i = (i + 1) % m;
        }

        // delete key and associated value
        keys[i] = null;
        vals[i] = null;

        // rehash all keys in same cluster
        i = (i + 1) % m;
        while (keys[i] != null) {
            // delete keys[i] an vals[i] and reinsert
            Key   keyToRehash = keys[i];
            Value valToRehash = vals[i];
            keys[i] = null;
            vals[i] = null;
            n--;
            put(keyToRehash, valToRehash);
            i = (i + 1) % m;
        }

        n--;

        // halves size of array if it's 12.5% full or less
        if (n > 0 && n <= m/8) resize(m/2);

        assert check();
    }

当 散列表快满的时候查找所需要的探测次数是巨大的,但当使用率 a小于 1/2 是探测的预计次数在1.5~2.5之间。

调整数组的大小
第一,基于线性探测法的数组大小调整
把原表中所有的键重新散列并插入到新表

    private void resize(int capacity) {
        LinearProbingHashST<Key, Value> temp = new LinearProbingHashST<Key, Value>(capacity);
        for (int i = 0; i < m; i++) {
            if (keys[i] != null) {
                temp.put(keys[i], vals[i]);
            }
        }
        keys = temp.keys;
        vals = temp.vals;
        m    = temp.m;
    }

第二,拉链法的数组大小调整

内存使用
除了存储键和值所需要的空间外,我们实现拉链法 SeparateChainingHashST 保存了M个SeparateChainingHashST
对象和它们的引用。每个对象需要16字节,每个引用需要8字节。另外还有N个对象,每个对象需要24字节以及三个引用。
在使用率 1/81/2情况下,线性探测使用4N16N个引用。
方法 N个元素所需的内存
拉链法 48N + 32M
线性探测法 32N ~ 128N
二叉查找树 56N

posted @ 2019-07-24 20:47  0ffff  阅读(1268)  评论(0编辑  收藏  举报