哈希表想知道的都在这啦

笔记 专栏收录该内容
69 篇文章 1 订阅

引言

前面所见到的查找方式:

查找方式时间复杂度
顺序查找O(N)
二分查找O(log2N)
二叉搜索树O(N)

他们的共同点都要进行比较

因此哈希(大佬)就提出了一种解决方式,该方式不用进行比较就能找到元素所处的位置。具体步骤如下所述~~

哈希概念

通过某种方式(哈希函数),将元素与其存储位置建立一一对应的映射关系,从而提高查找的效率;

例如

将元素{1,7,6,4,9}放入数组如下数组中:

方法:

  • 计算元素key在数组中的位置为index

  • key插入到index位置即可

    index=key%array.length

在这里插入图片描述
查找元素时

  • 计算元素x可能在数组中的位置index(有可能查找的元素不存在)
  • index位置取元素即可

将上面的计算公式就称为哈希函数,将计算出来的位置编号就称为哈希地址如下所示

在这里插入图片描述

常见的哈希函数

  • 直接定制法

取关键字的某个线性函数作为散列(哈希)地址,Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况

  • 除留余数法(常用)

Hash(key) = key% pp一般选用质数;

  • 平方取中法

比如关键字为1234,平方就是1522756,抽取中间的3227作为哈希地址;当关键字为4321,对
它平方就是18671041,抽取中间的3671(或710)作为哈希地址;
适用场景:不清楚关键字的分布,而位数又不是很多的情况

  • 折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址;
适用场景:不清楚关键字的分布且位数比较多的情况

  • 随机数法

选择一个随机函数,取关键字的随机函数值为它的哈希地址,
H(key) = random(key),其中random为随机数函数。
适用场景:通常应用于关键字长度不等时

  • 数学分析法

根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址;
适用场景:适合处理关键字位数比较大的情况,

但无论哈希函数设计的多精妙,也不能完全避免产生哈希冲突,只能减少发生冲突的概率

哈希冲突

多个不同的元素经过相同的哈希函数就会出现相同的哈希地址,将该情况就称为哈希冲突,将发生哈希冲突的元素称为同义词;

当发生哈希冲突时,首先需要检查哈希函数设计是否合理,哈希函数设计原则有

  • 哈希函数的定义域必须包括需要存储的全部关键码;
  • 哈希函数计算出来的地址能均匀分布在整个空间中;
  • 哈希函数设计应该尽比较简单;

解决冲突的两种主要解决方式

方式一:闭散列

闭散列:也叫开放定址法,是指当发生哈希冲突时,如果哈希表中元素并没有满的情况下,那么,就可以把关键字key按照某种方式存放到发生冲突位置的“下一个” 空位置中去;

找空位置的某种方式

  • 线性探测
  • 二次探测

线性探测

从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止
如下,当要插入的元素为44时,通过哈希函数 **index=key%array.length**计算出所要插入的位置为4,而该位置已经放入元素4了,因此就需要借助线性探测依次往后找空位置,将元素44插入进去。
在这里插入图片描述
但存在问题:我们怎么知道该位置到底有元素还是没有元素呢
方法:因此就需要借助标志位来帮助我们检测,初始状态下,将所有位置设为空(empty)状态,当插入元素时,就将该状态设为exist状态,表示已经存在元素,删除时,需要注意不能直接将元素删除完就将标志位设为空,这样的话,就会导致后面存放发生冲突的元素无法找到(原因:状态为空就不会往后差找了,因此就需要再借助一个标志位delete来表示是删除的元素,这样当碰到这样的标志时,就继续往后查找发生冲突的元素。

线性探测的缺点

容易出现元素的堆积

当存放的元素为{11,21,32,43,44,65}时,就会出现元素堆积到一起了

在这里插入图片描述
研究表明:该方法的载荷因子一般为0.7~0.8,在Java中,规定载荷因子为0.75

为了解决线性探测存在的元素堆积的问题,引出了二次探测

通俗的说:二次探测找空位置的方式原理与上面的线性探测是相同的,只不过就是隔着元素去找的(取决于i的平方);
在这里插入图片描述
H0为通过哈希函数计算出的散列地址,m为哈希表的容量大小,i为自然数:1,2,3…

从一定程度上解决了元素出现堆积的情况,但当哈希表中所存元素不断增多时,二次探测找元素的次数也就随之增加了;

研究表明:二次探测的载荷因子不超过0.5

载荷因子

载荷因子=哈希表中所存有效元素的个数/哈希表的长度
负载因子与冲突率之间的关系图:
在这里插入图片描述

因此,载荷因子越小,所存的元素就越少,发生冲突的概率就越小,但空间利用率就越低

方式二:开散列

开散列:又称为链地址法(开链法),首先对关键码集合用散列函数(哈希函数)计算散列地址,具有相同地址的关键码归于同一个子集合中,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中;

本质哈希表(数组)中存放链表

在这里插入图片描述
开散列中的每个桶都用来存放产生哈希冲突的元素;这样就可以认为是把一个在大集合中的搜索问题转化为小集合中的搜索问题了。

简单实现哈希桶

package day20211128;

public class MyHashBucket {
      //定义结点
    public static class  ListNode{
        Integer key;
        Integer value;
        ListNode next;  //下一个引用

          public ListNode(Integer key,Integer value){
              this.key=key;
              this.value=value;

          }
        }
        ListNode[] table;   //用来存放链表的数组
        int size; //哈希桶中有效元素的个数

       public MyHashBucket(int initCapacity){
           initCapacity= initCapacity <= 0?16:initCapacity;
           table=new ListNode[initCapacity];
       }

       //插入---->put
      public Integer put(Integer key,Integer value){
           checkCapacity();
           //1.根据哈希函数计算对应的桶号
          int bucketNum=hashFunc(key);
          //检测所插入元素的key在哈希桶中是否已经存在----key是唯一的
          //相当于在单链表中查找一个元素是否存在
          ListNode cur=table[bucketNum];
          while(cur!=null){
              if(key.equals(cur.key)){
                  //key存在时,将旧值进行替换
                  Integer oldValue=cur.value;
                  cur.value=value;
                  return oldValue;
              }
              cur=cur.next;
          }
           //key不存在时
          //2.将元素插入到哈希桶(bucketNum)中
          ListNode newNode=new ListNode(key, value);
          newNode.next=table[bucketNum];
          table[bucketNum]=newNode;
          size++;
          return value;
      }

      public Integer get (Integer key){
           //1.根据哈希函数计算对应的桶号
         int bucketNum=hashFunc(key);

          //2.到桶中找key
          ListNode cur=table[bucketNum];
          while(cur!=null){
              if(key.equals(cur.key)){
                  return cur.value;
              }
          }
          return null;
      }

      public Integer getOrDefault(Integer key,Integer value){
          Integer ret=get(key);
          if(ret!=null){
              return ret;
          }
          return value;
      }

      //删除--->remove
       public  Integer remove(Integer key){
           //通过哈希函数计算对应的桶号
           int bucketNum=hashFunc(key);

           //在桶(bucketNum)中找要删除的结点
           ListNode cur=table[bucketNum];
           ListNode prev=null;
           while(cur!=null){
               if(key.equals(cur.key)){
                   Integer oldValue=cur.value;
                   //删除的结点刚好是第一个
                   if(table[bucketNum]==cur){
                       table[bucketNum]=cur.next;
                       //cur.next=null;
                   }else{
                       //删除其他结点
                       prev.next=cur.next;
                       //cur.next=null;
                   }
                   cur.next=null;
                   size--;
                   return  oldValue;
               }

               cur=cur.next;

           }
           return  null;
       }

       public boolean containsKey(Integer key){
           //根据哈希的特性来找
           return  null!=get(key);
       }

       public boolean containsValue(Integer value){
           //每个桶进行遍历找
           for(int bucketNum=0;bucketNum<table.length;bucketNum++){
               ListNode cur=table[bucketNum];
               while (cur!=null){
                   if(value.equals(cur.value)){
                       return  true;
                   }
                   cur=cur.next;
               }
           }
           return false;
       }
       public int size(){
           return size;
       }

       private void checkCapacity(){
           if(size>=table.length){
               int newCapacity=table.length*2;
               ListNode[] newTable=new ListNode[newCapacity];

               //将就数组table中的元素搬移到扩容之后的newTable中
               //每个结点都需要搬移
               for(int i=0;i<table.length;i++){
                   //逐个桶进行搬移
                   ListNode cur=table[i];
                   while (cur!=null){
                       //将cur先从tablet中移除
                       table[i]=cur.next;

                       //将移除的cur往newTablet中插入
                       int bucketNum=cur.key % newTable.length;
                       cur.next=newTable[bucketNum];
                       newTable[bucketNum]=cur;
                       cur=table[i];
                   }
               }
                       table=newTable;
           }

       }
      private Integer hashFunc(Integer key){
           return key % table.length;

      }
   }

上面代码存在的问题

  • 哈希函数采用的是除留余数法,但只是针对key为整形的情况,当key不是整形时,就不能用了;
  • 扩容时,没有考虑负载因子的调节,直接将负载因子设为1了;
  • 虽然通过扩容可以将一部分较长的链表缓解,但随着插入元素的不断增多,查找的效率就下降了;

和Java类集的关系

  • Java中利用哈希表实现MapSet
  • Java中使用的是哈希桶方式解决冲突的
  • Java 中,当发生冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
  • Java中计算哈希地址值实际上是调用的是类的 hashCode() 方法,进行 key 的相等的比较调用的是 equals 方法

带你看标准库中关于HashMap的设计

1.HashMap的默认初始容量是16

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

2.HashMap的最大容量为2的30次方

/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

3.HashMap的默认负载因子是0.75

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

4.链表与红黑树相互转化

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */若链表中节点个数超过8时,会将链表转化为红黑树
static final int TREEIFY_THRESHOLD = 8;


/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 /// 若红黑树中节点小于6时,红黑树退还为链表
static final int UNTREEIFY_THRESHOLD = 6;
/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 */如果哈希桶中某条链表的个数超过8,并且桶的个数超过64时才会将链表转换为红黑树,否则直接扩容
static final int MIN_TREEIFY_CAPACITY = 64;

5.哈希表的容量转换为2的幂次方

static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

6.将key转换为整形数字

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
重写了hashCode方法

Java8对于数组table的初始化,并没有直接放在构造器中完成,而是将table数组的构造延迟到了(resize)扩容中完成;

性能分析

虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突概率是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是O(1);因此性能还是很优的~~

HashMap常考问题

  • HashMap何时开辟bucket数组占用内存?

第一次插入元素时

  • HashMap为什么要重写HashCode与equals?

HashMap是泛型的,里面存放的是K-V键值对,而它的哈希函数实质采用的是除留余数法,因此,当key不是整形时,就需要用户提供HashCode方法,将key转为整形;
重写equals主要是默认情况下,当没有重写时,会调用基类object中的equals方法,而object中的equals方法是用对象的地址来比较的;当要比较对象的值时,就需要重写。

  • HashMap何时扩容?

有效元素的个数大于哈希表的容量*负载因子时,就要考虑扩容;

  • new HashMap(19),bucket数组多大?

在Java1.8中,new一个HashMap对象时,并没有给其开辟空间,在第一次插入时,会将容量调整到比当前容量大的最接近的2的幂次方;也就是32

  • 3
    点赞
  • 0
    评论
  • 4
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:数字20 设计师:CSDN官方博客 返回首页

打赏作者

Java小菜鸟~

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值