哈希算法和HashMap原理

本文深入探讨了哈希算法的概念,包括其作用、哈希表的实现原理以及哈希冲突的处理。并通过分析常见的哈希算法如MD5、SHA,解释了它们在密码校验中的应用。此外,文章详细讲解了HashMap的内部实现机制,包括存取数据的过程及如何解决哈希冲突。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

哈希算法
  • 什么是哈希算法

    哈希算法不是一个特定的算法,而是一类算法的统称,也叫散列算法。

  • 算法的作用

    哈希算法是为了把任意一个数据,通过计算生成一个定长的key。一个数据只能与一个key对应,通过数据可以得出key值,不能通过key逆推出数据值。

    比较官方的说法:f(data)=key,输入任意长度的data数据,经过哈希算法处理后输出一个定长的数据key。同时这个过程是不可逆的,无法由key逆推出data。

  • 哈希表

    如果是一个data数据集,经过哈希算法处理后得到key的数据集,然后将keys与原始数据进行一一映射就得到了一个哈希表。

    一般来说哈希表M符合M[key]=data这种形式。

    哈希表的好处是当原始数据较大时,我们可以用哈希算法处理得到定长的哈希值key,那么这个key相对原始数据要小得多。我们就可以用这个较小的数据集来做索引,达到快速查找的目的。

  • 哈希冲突

    因为输入数据不定长,而输出的哈希值却是固定长度的,这意味着哈希值是一个有限集合,而输入数据则可以是无穷多个。

    那么建立一对一关系明显是不现实的。所以"碰撞"(不同的输入数据对应了相同的哈希值)是必然会发生的,所以一个成熟的哈希算法会有较好的抗冲突性。

    所以在哈希表实现时候,也要考虑哈希冲突的情况。

  • 常见哈希算法

    密码上常用的MD5,SHA都是哈希算法,因为key的长度(相对大家的密码来说)较大所以碰撞空间较大,有比较好的抗碰撞性,所以常常用作密码校验。

HashMap实现原理
  • 数组与链表

    • 数组:占用连续存储空间,占用内存大,空间复杂度较高。但查找很容易,特点是查找容易,插入和删除困难
    • 链表:存储方式是离散存储,空间复杂度低,但查找的时间复杂度较高,特点是查找麻烦,插入删除容易
  • 哈希表

    是一种综合了数组和链表的特点,做出的一种查找、插入、删除的复杂度都比较低的数据结构。

    哈希表有很多中类型,我们看一种较为常见的类型——拉链型,这种数据结构可以理解为链表的数组

    哈希表图解

    如上图所示,假设通过某个哈希算法,可以把数据转化为0-15中的数字,然后就可以把数据用如上方式存储。

    其中数据对应的hash值代表数组的下标。当同一个hash值有多个数据时,可以在当前下标的链表后面继续添加数据,直到所有的数据归类完成。

    采用哈希表后,数据的查找、删除、插入的平均时间复杂度会有显著的降低,空间的利用也更加高效。

  • HashMap原理简介

    • 概述

      HashMap也可以理解为一个hash表来实现的。

      HashMap有个内部类Entry,其重要属性有key value next,其中key/value用来保存对应的键值对,next作为链表的指针。

      然后内部所有的数据存储在Entry[] table中。

    • 存数据

      public V put(K key, V value) {
          if (key == null)
              return putForNullKey(value); //null总是放在数组的第一个链表中
          int hash = hash(key.hashCode());
          int i = indexFor(hash, table.length);
          //遍历链表
          for (Entry<K,V> e = table[i]; e != null; e = e.next) {
              Object k;
              //如果key在链表中已存在,则替换为新value
              if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                  V oldValue = e.value;
                  e.value = value;
                  e.recordAccess(this);
                  return oldValue;
              }
          }
          modCount++;
          addEntry(hash, key, value, i);
          return null;
      }
      

      首先会生成hash值,用来对应Entry数组下标,然后会遍历当前下标对应链表的所有数据,如果对应的key已经存在,则替换,否则在当前数据末尾添加数据。

      void addEntry(int hash, K key, V value, int bucketIndex) {
          Entry<K,V> e = table[bucketIndex];
          table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //参数e, 是Entry.next
          //如果size超过threshold,则扩充table大小。再散列
          if (size++ >= threshold)
                  resize(2 * table.length);
      }
      

      当随着数据的不断增多,为了保证性能的优越性,会扩充Entry数组的长度。此时通过hash值转数组下标的方法也会改变,有兴趣的可以查看hashmap源码。

    • 取数据

      public V get(Object key) {
          if (key == null)
              return getForNullKey();
          int hash = hash(key.hashCode());
          //先定位到数组元素,再遍历该元素处的链表
          for (Entry<K,V> e = table[indexFor(hash, table.length)];
               e != null;
               e = e.next) {
              Object k;
              if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                  return e.value;
          }
          return null;
      }
      

      取数据对应的原理通过代码可以很好理解,就不专门介绍了。

总结

hashMap是具有超快查询速度的数据存储方式,查找的时间复杂度基本是O(1)级别,唯一的缺点可能就是需要进行一次额外的hash值计算。所以在日常使用中,对键值对的存储可以优先考虑hashMap。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值