美文网首页算法与数据结构随笔-生活工作点滴
算法与数据结构系列之[哈希表]

算法与数据结构系列之[哈希表]

作者: 秦老厮 | 来源:发表于2019-07-10 15:47 被阅读78次

前言

前面我们介绍线性表的时候,如果要查找某个元素,比如key,就需要从表头开始,逐个比较记录a[i]和key的值是相等还是不相等,知道有相等才算查找成功,返回i。那么我们有没有一种方法不用逐个比较就能查找到要查询的元素呢?答案是肯定的,我们可以通过关键字key得到要查找记录的内存位置,这就是我们这篇要介绍的哈希表,其实哈希表算是线性存储结构的范畴,因为哈希表用到数组作为存储结构。

1.什么是哈希表

哈希表(Hash Table),也叫散列表。哈希表是用数组作为存储结构,具有数组按下表进行随机访问的特性,所以哈希表是数组的一种扩展。哈希表通过哈希函数对我们要存储的元素计算哈希值,将这个哈希值作为数组的下标,然后将数据存储在数组中对应的下标位置。当我们按照键值查询元素时,我们用同样的哈希函数,将键值转换为数组下标,从对应的 数组下标位置取数据。那么哈希表到底有什么用途呢?

当然,哈希表也是用来存储数据的,比如,我们维护一个班级的所有学生的信息,为了方便维护数据,我们可以把一个班级的学生的信息放在一个数组里,假如这个班有40人,学号为1的学生放在数组下标为1的位置,学号为2的学生放在数组下标为2的位置,依次类推,学号为40的学生放在数组下标为40的位置。因为学生学号和下标一一对应,所以当我们需要查询学号为x的学生信息时,只需要将数组下标为x的数组元素取出来 即可,时间复杂度为O(1)。

2.哈希函数

说到哈希表,我们自然绕不开哈希函数,哈希函数是哈希表中非常重要的一个函数,哈希表用这个哈希函数对键值计算哈希值,每次存取都通过计算哈希值找到元素在数组中对应的下标位置。哈希函数设计的好坏直接决定了哈希表冲突的概率大小,也直接决定了哈希表的性能的好坏。那么什么样的哈希函数才算是好的哈希函数呢?首先哈希函数的设计不能太复杂,过于复杂的哈希函数本身就增加了计算时间,影响哈希表的性能。其次,哈希函数生成的值要尽可能随机并且均匀分布,这样才能避免或者最小化哈希冲突,散列到每个槽或桶里的数据才会比较平均,不至于出现某个槽内的数据特别多。

3.哈希冲突

再好的哈希函数也无法避免哈希冲突的,随着数据量的增大,哈希冲突就有可能发生,那么发生了哈希冲突,我们该如何解决呢?

其实,关于哈希冲突的方法早就有人研究好了,解决的方法也有很多种,不过我们常用的有链地址法和开放地址法。

  • 链地址法:顾名思义,链地址法就是利用链表来作为冲突后的存储结构,相比于开放地址法,它更加简单,接下来,我们通过图示方法,介绍下链地址法。

如图,在哈希表中,每个槽或桶会对应一条链表,所有哈希值相同的元素我们都放在相同的槽位对应的链表中。

图一

当插入的时候,我们只需要通过哈希函数计算出对应的哈希槽位,将其插入对应的链表中即可,所以插入的时间复杂度为O(1)。当查找或删除一个元素时,我们同样通过哈希函数计算出对应的槽,然后遍历链表 查找或删除,这连个操作的时间复杂度和链表的长度成k正比,也就是O(k),当链表的长度越长时,性能随之越差,所以,java中的HashMap底层的哈希表解决哈希冲突的方法是链表加红黑树。当链表的长度大于8时,就转化成红黑树,当小于8时又转化为链表。

  • 开放地址法:开放地址法的核心思想是,如果出现哈希冲突,我们就重新探测一个空闲位置,将其插入,探测新位置的常用方法有线性探测,二次探测和多重哈希法。

所谓的线性探测就是当我们往哈希表中插入元素时,如果某个元素经过哈希函数计算哈希值后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,知道找到空闲位置为止。

二次探测和线性探测很像,只是线性探测每次增长的步长为1,二次探测增长的步长为原来的平方,比如下标为1的位置占用了,下一个探测的位置就是2,下标为2的位子被占用了,下次探测的位置就是4,依次类推,直到找到空闲位置为止。

所谓的多重哈希就是哈希函数不止一个,而是有多个哈希函数可以计算哈希值,当然我们先用第一个哈希函数计算哈希值,如果计算得到的存储位置已经被占用,就用第二个哈希函数重新计算哈希值,如果得到的存储位置仍然被占用就用第三个,依次类推,直到找到空闲位置为止。

上面介绍了链地址法和开放地址法两种解决哈希冲突的方法,两种方法各有其优缺点,那么我们在实际开发中到底应该选择哪种冲突解决方法呢?下面对两种方法的优缺点和使用场景做一个介绍。

1.开放地址法

首先我们先看一下开放地址法的优点。

开放地址法不像链地址法需要很多的链表,哈希表中的数据都存储在数组中,在内存中是连续的存储空间,可以有效的利用CPU的缓存加快查询速度。

接下来我们看一下开放地址法的缺点。

在开放地址法中,所有的数据都存储在一个数组中,比起链表来,冲突代价更高,更加浪费存储空间,装载因子(元素个数/存储位置的个数)的上限不能太高。

所以当数据量比较小,装载因子比较小的时候,适合采用开放地址法,Java中的ThreadLocalMap就是采用开放地址法来解决哈希冲突的。

2.链地址法

首先,链表法对内存的利用率比开放寻址法要高。因为链表结点可以在需要的时候再创建,并不需要像开放寻址法那样事先申请好。其次, 链表法比起开放寻址法,对大装载因子的容忍度更高。

但是链表要存储指针,所以对于比较小的对象的存储,是比较消耗内存的,还有可能会让内存的消耗翻倍。而且,因为链表中的结点是零散分布在内存中的,不是连续的,所以对CPU缓存是不友好的,这方面对于执行效率也有一定的影响。

基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。

4.代码实现

public class HashTable<K,V> {
   private final int[] capacity=
           {53,97,193,389,769,1543,3079,6151,12289,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 = capacity[0]; //初始化容量数组下标

   private TreeMap<K,V>[] hashtable;
   private int M; //hash表中有M个位置
   private int size; //hash表中元素的个数

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

   //哈希函数
   private int hash(K key){
       return (key.hashCode() & 0x7fffffff) % M; //key.hashCode() & 0x7fffffff将key的hashcode值的符号去掉
   }

   //获取哈希表的元素个数
   public int getSize(){
       return size;
   }

   //向哈希表中添加元素
   public void add(K key,V value){
       TreeMap<K,V> map = hashtable[hash(key)];
       if(map.containsKey(key))
           map.put(key,value);
       else{
           map.put(key,value);
           size++;
           //判断是否需要扩容
           if(size >= upperTol*M && capacityIndex+1 < capacity.length){
               capacityIndex++;
               resize(capacity[capacityIndex]);
           }
       }
   }

   //从哈希表中删除元素
   public V remove(K key){
       TreeMap<K,V> map = hashtable[hash(key)];
       V ret = null;
       if(map.containsKey(key)){
           ret = map.remove(key);
           size--;
           //判断是否需要缩容
           if(size < lowerTol * M && capacityIndex-1 >= 0){
               capacityIndex--;
               resize(capacity[capacityIndex]);
           }
       }
       return ret;
   }

   //扩容
   private void resize(int newM) {
       TreeMap<K,V>[] newHashTable = new  TreeMap[newM];
       for (int i = 0; i < newM; i++) {
           newHashTable[i] = new TreeMap<>();
       }
       int oldM = M;
       this.M = newM;
       for (int i = 0; i < oldM; i++) {
           for (K key : hashtable[i].keySet()) {
               newHashTable[hash(key)].put(key,hashtable[i].get(key));
           }
       }
       this.hashtable = newHashTable;
   }

   //将哈希表中键为key的元素值修改为value
   public void set(K key,V value){
       TreeMap<K,V> map = hashtable[hash(key)];
       if(!map.containsKey(key))
           throw new IllegalArgumentException("要修改的键不存在");
       map.put(key,value);
   }


   //判断哈希表中是否存在key
   public boolean contains(K key){
       return hashtable[hash(key)].containsKey(key);
   }

   //获取哈希表中键为key的value值
   public V get(K key){
       return hashtable[hash(key)].get(key);
   }

   public static void main(String[] args) {
       HashTable<String,Integer> hashTable = new HashTable();
       hashTable.add("黎明",100);
       hashTable.add("赵薇",98);
       hashTable.add("周凯",79);
       System.out.println(hashTable.get("周凯"));
   }
}

5.总结

这篇我们介绍了哈希表的相关概念,哈希函数,哈希冲突以及解决哈希冲突的方法,最后还贴出了哈希表的代码实现以供参考,关于哈希表的实现,可以参看Java中的HashMap的底层实现。

本人微信公众号,点关注,不迷路

相关文章

  • 九、哈希表

    这一节记录一下哈希表的学习 持续更新中...数据结构与算法系列博客:一、数据结构与算法概述二、数组及LeetCod...

  • 数据结构和算法

    1.哈希表哈希算法详解(附带 iOS 开发中实际应用) 2.链表iOS 数据结构之链表

  • 数据结构与算法 - 查找

    数据结构与算法系列文章数据结构与算法 - 时间复杂度数据结构与算法 - 线性表数据结构与算法 - 树形结构数据结构...

  • 算法与数据结构系列之[哈希表]

    前言 前面我们介绍线性表的时候,如果要查找某个元素,比如key,就需要从表头开始,逐个比较记录a[i]和key的值...

  • 数据结构与算法 - 树形结构

    数据结构与算法系列文章数据结构与算法 - 时间复杂度数据结构与算法 - 线性表数据结构与算法 - 树形结构 目录 ...

  • 「Redis源码解读」—数据结构(二)哈希表

    Redis的字典使用哈希表作为底层实现 知识点 1.数据结构 哈希节点 哈希表 类型处理函数 2.哈希 哈希算法 ...

  • MySQL索引简述--哈希索引

    哈希算法 哈希算法时间复杂度为O(1),且不只存在于索引中,每个数据库应用中都存在该数据结构。 哈希表 哈希表也为...

  • 使用JavaScript实现哈希表

    关于哈希表的原理详见我的上一篇文章简单易懂数据结构之哈希表 前言 JavaScript中是有哈希类型的数据结构的,...

  • 重温:数据结构与算法 - 03数组

    数据结构与算法之美 - 数组 数据结构与算法之美-学习大纲 什么数组? 数组是一种 线性表 数据结构。它用一组 连...

  • 2.6 数据结构 --1.5 哈希表

    数据结构子目录https://www.jianshu.com/p/a344fa483655 哈希表 在了解哈希表之...

网友评论

    本文标题:算法与数据结构系列之[哈希表]

    本文链接:https://www.haomeiwen.com/subject/xaphkctx.html