热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JDK常用数据结构(二)—散列表

散列表,又叫哈希表,是能够通过给定关键字的值直接访问到具体对应的值的一种数据结构。也就是说,把关键字映射到一个表中的位置来直接访问记录

像散列表这么优秀的数据结构没理由不单独挑出来独自咀嚼啊!因此,这部分会重点分析散列表。

散列表

散列表,又叫哈希表,是能够通过给定关键字的值直接访问到具体对应的值的一种数据结构。也就是说,把关键字映射到一个表中的位置来直接访问记录,以加快访问的速度。通常,我们把关键字叫做key,把对应的记录叫做value,也就是说通过key访问一个映射关系来得到value的访问地址。我们称这个映射关系叫做散列函数或者哈希函数,存放记录的数组叫做散列表。某些特殊情况下,通过不同的key,可能访问到同一个地址,这种现象叫做冲突(Collision)。根据上述描述,散列由两个重要部分组成: 散列函数解决寻址问题;散列表解决存储问题。常用的散列函数包括

  • 直接寻址法

    取关键字或关键字的某个线性函数的值作为散列地址。设关键字为x,那么散列地址可表示为:

    p(x) = x 或 H(x) = ax + b (a、b为常数)

  • 保留余数法

    取关键字被某个不大于散列表的表长 n 的数 m 除后所得的余数 p 为散列地址,关键字 x 的数据元素的散列地址为:

    p(x) = x mod m

    在保留余数法中,选取合适的m很重要,如果选取不当,则导致大量的碰撞。一般情况下,m为素数或者直接用 n时,散列表的分布比较均匀。

  • 数字分析法

    通过对数据的分析,发现数据中冲突较少的部分,并构造散列地址。

  • 平方取中法

    当无法确定关键字里哪几位的分布相对比较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为散列地址。

  • 取随机数法

    使用一个随机函数,取关键字的随机值作为散列地址,这种方式通常用于关键字长度不同的场合。

不存在一种万能的散列函数,在任何情况下都是出色的。但是大部分情况下,保留余数法比较好。

当发生冲突时,常见的处理冲突的方法有两种

  1. 将溢出的数据元素存放到散列表中没有使用的单元中

    常见的处理方法有

  • 开发地址法 :在数组中从映射到的位置开始顺序查找空位置,如果需要,从最后一个位置绕回第一个位置。这种方法碰撞可能引起连锁反应,使表中形成一些较长的连续被占用的单元,如:从1---10的地址全部被使用。从而使散列表的性能降低。

  • 二次探测发 :不直接检查下一个单元,而是检查远离初始探测点的某一单元,以消除线性探测法中的初始聚集的问题。

  • 再散列法 :有两个散列函数H1和H2。H1用来计算探测序列的起始地址,H2用来计算下一个位置的步长。

  • 将映射到同一地址的数据元素分别保存到其他数据结构中

    由于地址相同的数据元素个数变化比较大,因此通常采用链表的方式。散列表本身只保存一个指向各自链表中第一个节点的指针。通常使用的方法是链地址法。

  • 散列表在JDK中具体实现有HashMap和ConcurrentHashMap,这里我们简要分析一下HashMap中的实现,ConcurrentHashMap到红黑树的时候分析。那么,HashMap的哈希函数是什么呢?下面我们从源码层面分析,

    ...
    static class Node implements Map.Entry {
          final int hash;
          final K key;
          V value;
          Node next;
          Node(int hash, K key, V value, Node next) {
              this.hash = hash;
              this.key = key;
              this.value = value;
              this.next = next;
          }
    ...

    结点数据结构中,包含了结点本身的hash值,结点的key,结点key对应的value和下一个界面的指针。

    ...
    1)
    static final int hash(Object key) {
          int h;
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    ...
    2)
    public V put(K key, V value) {
          return putVal(hash(key), key, value, false, true);
    }
    ...
    3)
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                      boolean evict) {
          Node[] tab; Node p; int n, i;
          if ((tab = table) == null || (n = tab.length) == 0)
              n = (tab = resize()).length;
          if ((p = tab[i = (n - 1) & hash]) == null)
              tab[i] = newNode(hash, key, value, null);
          else {
              Node e; K k;
              if (p.hash == hash &&
    ...
    4)
    Node newNode(int hash, K key, V value, Node next) {
          return new Node<>(hash, key, value, next);
    }
    ...

    我们以2)put函数为切入点,直接可以看出首先调用了1)hash函数计算key值的hash值,然后再委托3)putVal函数将键值对存储到散列表中。对3)putVal函数的分析可知,1)并不是HashMap的哈希函数,那么,它的哈希函数式什么呢?事实上,只要锁定3)putVal函数中的数组下表i就可以了,

    i = (n - 1) & hash

    因此,HashMap的散列函数为(n - 1) & hash。上述公式中,hash通过1)hash函数计算出来;n是多少呢?打开resize方法,

    /**
    * The default initial capacity - MUST be a power of two.
    */
    static final int DEFAULT_INITIAL_CAPACITY = 1 <<4; // aka 16
    ...
    final Node[] resize() {
          Node[] oldTab = table;
          int oldCap = (oldTab == null) ? 0 : oldTab.length;
          int oldThr = threshold;
          int newCap, newThr = 0;
          if (oldCap > 0) {
              if (oldCap >= MAXIMUM_CAPACITY) {
                  threshold = Integer.MAX_VALUE;
                  return oldTab;
              }
              else if ((newCap = oldCap <<1)
                        oldCap >= DEFAULT_INITIAL_CAPACITY)
                  newThr = oldThr <<1; // double threshold
          }
          else if (oldThr > 0) // initial capacity was placed in threshold
              newCap = oldThr;
          else {               // zero initial threshold signifies using defaults
              newCap = DEFAULT_INITIAL_CAPACITY;
              newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
          }
          if (newThr == 0) {
              float ft = (float)newCap * loadFactor;
              newThr = (newCap
                        (int)ft : Integer.MAX_VALUE);
          }
          threshold = newThr;
          @SuppressWarnings({"rawtypes","unchecked"})
              Node[] newTab = (Node[])new Node[newCap];
          table = newTab;
    ...

    从源码可以看出,table的长度newCap默认值为DEFAULT_INITIAL_CAPACITY。DEFAULT_INITIAL_CAPACITY默认值为16。所以,n默认值为16。

    通过哈希函数找到数组下标之后,如果出现冲突怎么解决呢?HashMap采用的策略是:如果冲突元素8个以内采用单向链表存储;否则,将Node转换为TreeNode采用红黑树存储。代码片段如下

    ...
    static final int TREEIFY_THRESHOLD = 8;
    ...
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                      boolean evict) {
          Node[] tab; Node p; int n, i;
          if ((tab = table) == null || (n = tab.length) == 0)
              n = (tab = resize()).length;
          if ((p = tab[i = (n - 1) & hash]) == null)
              tab[i] = newNode(hash, key, value, null);
          else {
              ...
                  for (int binCount = 0; ; ++binCount) {
                      if ((e = p.next) == null) {
                          p.next = newNode(hash, key, value, null);
                          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                              treeifyBin(tab, hash);
                          break;
                      }
                      if (e.hash == hash &&
                          ((k = e.key) == key || (key != null && key.equals(k))))
                          break;
                      p = e;
                  }
              }
    ...

    首先p被赋值为数组中下标为哈希函数映射值的结点,也就是链表的头结点,然后执行for循环,e赋值为p结点的下一个结点,如果p结点为尾结点,则新生成一个结点,追加到p结点之后,跳出循环;如果p结点不是尾结点,则将p指向p结点的下一个结点(p = e),直到p指向了链表的尾结点,同时,生成新结点追加到p之后,此时,如果结点数量已经大于TREEIFY_THRESHOLD,则执行treeifyBin()函数,

    ...
    final void treeifyBin(Node[] tab, int hash) {
          int n, index; Node e;
          if (tab == null || (n = tab.length)
              resize();
          else if ((e = tab[index = (n - 1) & hash]) != null) {
              TreeNode hd = null, tl = null;
              do {
                  TreeNode p = replacementTreeNode(e, null);
                  if (tl == null)
                      hd = p;
                  else {
                      p.prev = tl;
                      tl.next = p;
                  }
                  tl = p;
              } while ((e = e.next) != null);
              if ((tab[index] = hd) != null)
                  hd.treeify(tab);
          }
      }
    ...

    函数中,首先将单向链表转换为双向链表(do...while),然后调用根节点的treeify()方法将双向链表转换为红黑树,具体转换过程,在红黑树部分分析。

    优点

    • 不论哈希表中有多少数据,查找、插入、删除(有时包括删除)只需要接近常量的时间即0(1)的时间级。

    缺点

    • 它是基于数组的,数组创建后难于扩展。

    适用范围

    散列表比较适合无序、需要快速访问的情况。此外,哈希表也可以用来查重处理。



    推荐阅读
    • Java学习笔记之面向对象编程(OOP)
      本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
    • HashMap的相关问题及其底层数据结构和操作流程
      本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
    • CSS3选择器的使用方法详解,提高Web开发效率和精准度
      本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
    • Java容器中的compareto方法排序原理解析
      本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
    • C语言注释工具及快捷键,删除C语言注释工具的实现思路
      本文介绍了C语言中注释的两种方式以及注释的作用,提供了删除C语言注释的工具实现思路,并分享了C语言中注释的快捷键操作方法。 ... [详细]
    • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
    • r2dbc配置多数据源
      R2dbc配置多数据源问题根据官网配置r2dbc连接mysql多数据源所遇到的问题pom配置可以参考官网,不过我这样配置会报错我并没有这样配置将以下内容添加到pom.xml文件d ... [详细]
    • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
    • 本文概述了JNI的原理以及常用方法。JNI提供了一种Java字节码调用C/C++的解决方案,但引用类型不能直接在Native层使用,需要进行类型转化。多维数组(包括二维数组)都是引用类型,需要使用jobjectArray类型来存取其值。此外,由于Java支持函数重载,根据函数名无法找到对应的JNI函数,因此介绍了JNI函数签名信息的解决方案。 ... [详细]
    • HashMap的扩容知识详解
      本文详细介绍了HashMap的扩容知识,包括扩容的概述、扩容条件以及1.7版本中的扩容方法。通过学习本文,读者可以全面了解HashMap的扩容机制,提升对HashMap的理解和应用能力。 ... [详细]
    • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
    • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
    • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
    • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
    • 本文介绍了关于Java异常的八大常见问题,包括异常管理的最佳做法、在try块中定义的变量不能用于catch或finally的原因以及为什么Double.parseDouble(null)和Integer.parseInt(null)会抛出不同的异常。同时指出这些问题是由于不同的开发人员开发所导致的,不值得过多思考。 ... [详细]
    author-avatar
    手浪用户2602933263
    这个家伙很懒,什么也没留下!
    PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
    Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有