hash冲突 泊松分布_数据结构之散列(Hash)

一、概述

什么是散列表?散列表也被称为 hash表,它是一种用于以 常数平均时间执行插入、删除和查找的技术。其通过 关键字(key)来实现对数据的直接访问。关键字和数据的映射关系称为 散列函数又称 hash函数,记做 hash(x),我们把散列表的大小记做 tableSize。理想的散列表: 关键字被映射到tableSize个单元内,并且任意两个不同的关键字被映射到不同的单元 。事实上单元的数量有限,而关键字实际上是用不完的。 因此要找到一个散列函数,该函数要在单元之间均匀的分配关键字,这就是散列的基本想法。剩下的还有一个问题就是:当两个关键字散列到同一个单元的时候 ( 这叫做 冲突),应该如何解决这种冲突?
二、散列函数

如果输入的关键字是整数,则一般合理的方法就是直接返回key%tableSize ( hash(x) = x%tableSize ),但如果关键字不是整数,比如说字符串那么以上的散列函数就不适合了,好的解决办法就是将字符串中字符的ASCII 码值加起来生成一个整数,然后在使用以上的散列函数,看过String源码的朋友应该注意到String类提供了一个方法叫做hashCode(),下面我们来看下这段源码

   public int hashCode() {        int h = hash;        if (h == 0 && value.length > 0) {            char val[] = value;            for (int i = 0; i < value.length; i++) {                h = 31 * h + val[i];            }            hash = h;        }        return h;   }
String 类是以字符的形式将数据存储到内存里面。以上计算hashCode的方法就是取出每个字符按照位置乘上31再加上各自的 ASCII 码值,得到一个 int 型的整数,我们可以用这种方式将 String类型的关键字转换成整数关键字,这样就可以用  hash(x) = x%tableSize 这个散列函数,不仅 String类提供了 hashCode方法,而且 Object类也提供了 hashCode方法, Object 类是所有类的父类 ,也就是说 java里面所有的对象都有 hashCode方法,无论关键字是什么类型都可以用 hash(x) = x%tableSize  这个散列函数。我们在设计散列表的时候要 尽可能保证表大小为素数, 这样就可以保证关键字的分配均匀。如下图所示,当 tableSize=10,关键字的个位数始终为0,那么散列函数就不能均匀的分布,当我们将散列表的大小设计为11(素数),可以看到关键字的分布情况要好的多。

839b0985c8094db7b6a556f4a2e2b8f5.png


三、冲突解决策略

上面提到两个关键字被散列到同一个单元,我们必须找到一种策略,该策略可以很好的解决这种冲突 第一种解决冲突的方法通常叫做 分离链接法,其做法是将散列到同一个单元的所有元素保留到一个表中,如下图

128895fb00f183b46f6d4f410f48f062.png

该图展示了散列表大小为10,处理冲突的表为单链表, 考虑到散列表的性能,我们可以得到“ 用于处理冲突的链表不能无限大 ”这种结论。因为我们很容易推测出查找一个元素所耗费的时间是 计算散列函数值的常数时间加上遍历链表所用的时间,用复杂度表示即为 O(1)+O(N) ,从中可以看出 N越大复杂度越高,耗时也就越长。我们记散列表的 填装因子(r)为散列表中 素个数 (n)对该 表大小(tableSize)的比。(即 r=n/tableSzie)从中很容易看出,如果填装因子越大,相对的链表就越大,复杂度越高,最好的办法是 让填装因子始终小于 1,所以我们在设计的时候要考虑到将散列表扩容。

第二种解决冲突的方法可以用线性探测法。当产生冲突的时候该方法会相继探测逐个单元以查找出一个空单元,然后将该冲突值放入这个空的单元里。例如我们在tableSize=10的散列表相继插入【56,89,43,76,23,36】

c036b2b2ea67f87f799d9cfbf2495db2.png

上图表示使用线性探测法插入76,23,36时,解决冲突的示意图,我们可以看到当插入76时会产生冲突,该单元已经被56所占据,于是76会探测下一个单元,如果下个单元为空就将76放进去,如果不为空,就继续探测直到找到一个空的单元为止。只要表足够大,总能找到一个空单元,但是这样花费的时间是相当多的。

三、代码实现

接下来我们选择其中一种解决冲突的方法进行代码实现,两种方法相比较,我们选择 分离链接法来进行代码实现。 (Java7HashSetHashMap是用分离链接散列实现的) 首先创建SeparateChainingHashTable类,定义好成员变量,添加构造器,根据上面的分析,我们应当保证散列表的大小始终为素数。我们定义了一个默认的容量为11,还定义一个有参构造器通过方法nextPrime(),将capacity转换为大于capacity的最小素数,如下代码
public class SeparateChainingHashTable {    private List[] entries;    private int currentSize;    // 默认散列表的容量    private static final int DEFAULT_CAPACITY = 11;    SeparateChainingHashTable() {        this(DEFAULT_CAPACITY);    }    SeparateChainingHashTable(int capacity) {        this.entries = new List[nextPrime(capacity)];    }}
然后我们开始添加add()方法, 添加add()方法之前,我们应该考虑:如果添加一个已经存在的元素该如何操作?我们这里就先按照无法重复添加相同元素的逻辑来处理,如下代码,给出了add()方法和contains()方法,在进行添加操作的时候,先判断要添加的元素是否已经存在,存在则不做任何操作,不存在则添加
    /**     * 添加元素     *     * @param t     */    public void add(T t) {        if (contains(t))            return;        // 获取关键字        int key = getKey(t);        if (entries[key] == null)            entries[key] = new LinkedList<>();        entries[key].add(t);        if (++currentSize >= entries.length) {            // 扩容            rehash();        }    }        /**     * 判断是否包含元素     *     * @param t     * @return     */    public boolean contains(T t) {        List entry = entries[getKey(t)];        if (entry == null)            return false;        return entry.contains(t);    }
上面add()方法里面有一个rehash()方法,该方法就是保证散列表的填装因子始终小于1,以确保该散列表的操作效率,以下代码就是该散列的扩容机制
    /**     * 对当前散列表进行扩容     */    private void rehash() {        List[] oldEntries = this.entries;        int oldSize = this.entries.length;        this.entries = new List[nextPrime(oldSize << 1)];        for (List oldEntry : oldEntries) {            if (oldEntry == null)                continue;            for (T t : oldEntry) {                add(t);            }        }        currentSize = 0;    }
最后我们再编写删除remove()方法,同样如果要删除的元素不在散列表里,则不作任何操作,如下代码所示
    /**     * 删除元素     *     * @param t     */    public void remove(T t) {        if (!contains(t))            return;        List entry = entries[getKey(t)];        entry.remove(t);        currentSize--;    }
代码编写到这里,已经基本完成,我们选用的链表是 LinkedList,没有用自己手写的单链表,(之前的 java基础数据结构之链表中已经编写过单链表,感兴趣的读者朋友可以自己将 LinkedList替换成上文提到的单链表) 四、小结

本章主要帮助大家梳理了散列的基本思想,介绍了常规的hash函数,以及解决hash冲突的几种方法,编写了分离链接法实现的散列表。当然散列的实现不止文章中说到的几种方法,例如:布谷鸟散列、跳房子散列等等,关于这些散列感兴趣的朋友可以去找资料详细了解一下。(如果有必要,后续我们会对这两种散列的实现进行分享)

实际工作中我们也经常接触散列这种数据结构,例如标准库中的HashMapHashTable,在分布式系统中我们经常听到的一致性Hash算法等等,都涉及到了散列,由此可见hash的重要性。


感兴趣的读者朋友可以 关注本公众号,和我们一起学习探究。


eb6acf2eaa4cc220386fc804b3798e52.png


本人因所学有限,如有错误之处,望请各位指正!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值