浅析hashMap原理及简单实现

HashMap是一种基于哈希表的数据结构,用于存储键值对,其中键是唯一的。哈希函数用于确定元素的存储位置,但可能导致哈希冲突。解决冲突的策略包括线性探测、二次探测和链地址法(开散列)。Java中的HashMap采用了数组+链表+红黑树的方式,当链表长度超过8时,会转为红黑树提高效率。

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

知识补充

hashMapMap的具体实现类。Map是一个接口类,该类没有继承自Collection,该类中存储的是键值对,并且键唯一,不能重复。

键值对就是一个唯一的key对应一个值。举个例子:就像学生的学号,每个学生的学号对应一个同学的姓名,学号是唯一的,学生的姓名可以重复。也就是说,键值对中键唯一,一个键对应一个值,但是也可以有多个键对应相同的值。就像学校有多个叫王五的同学,他们的学号唯一,但是对应的姓名都是王五。

在这里插入图片描述

注意

  • Map是一个接口,不能实例化对象。如果要实例化对象只能实例化其实现类
  • Map中的键是唯一的,value是可以重复的
  • 在Map中插入键值对,键不能为空,值可以为空
  • Map中键值对的key不能直接修改,value可以直接修改,如果要修改key,只能先将key删除掉再插入

hashMap概念

在顺序结构和二叉搜索树中,元素与其位置没有一一对应的关系,因此在查找一个元素时,必须要经过比较遍历。顺序查找的时间复杂度为O(n),二叉搜索树和二分查找为O(logn),搜索的效率取决于元素比较的次数。

理想的搜索方法就是:不经过任何比较,依次从表中找到想要的元素。如果能构造一种存储结构,通过某种函数使元素的存储位置和元素之间能够建立起对应的关系,那么在查找时,就可以很快找到该元素。

该方法即为哈希(散列)方法,哈希方法中使用的转换函数为称为哈希函数,构造出来的结构称为哈希表

存储方式:

  • 插入元素

    根据插入元素的关键字,使用哈希函数来计算出该元素的存储位置,并按此位置存放

  • 查找元素

    对元素的关键字进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置比较,若关键字相等,则找到该元素

例如集合:{1,8,4,9,2,3,5}

哈希函数设置为:hash(key) = key % capacity,capacity为存储元素底层空间的大小

在这里插入图片描述

假设哈希表的默认存储容量为10,那么我们按照哈希函数hash(key) = key % capacity来得出每一个关键字存储的位置,并将关键字放进去。同样取数据时,我们知道key,就可以用哈希函数找到存储key的位置,然后找到该元素。然而,按照如上方式继续插入19时,就会发现9号地址已经有关键字。此时该怎么做?扩容?然后放到下标为19的地址吗?

哈希冲突

我们发现,对于两个不同的关键字key,使用哈希函数计算之后得到了相同的地址,这种现象称为哈希冲突。这种冲突如何避免呢?单纯的扩容肯定是不行的,假如集合元素为{9,19,29,39,49,59}难道为了存储这几个数据去开60的空间吗?

冲突避免–哈希函数设计

我们需要明确的是,哈希表底层数组的容量往往是小于我们实际要存储的关键字的数量的,意思就是哈希冲突的发生是必然的,我们要想办法尽量降低冲突率

引起哈希冲突原因:哈希函数设计不合理

哈希函数的设计原则

  • 哈希函数的定义域必须要包括全部的关键字,如果哈希表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出的地址能均匀分布在整个存储空间
  • 哈希函数应该比较简单

常见哈希函数

  1. 直接定制法

    取关键字的某个线性函数为散列地址:hash(key) = A*key + B

    优点:简单、均匀 缺点:需要事先知道关键字的分布情况

    例如:字符串中的第一个唯一字符

    使用哈希表的思想,我们知道字母有26个,创建大小为26的字符数组,通过哈希函数char - 'a'得到key的地址(下标),遍历过程中,每出现一次相同的key就让chars[char - 'a']地址的值加一。然后我们得到了第i个字母在字符串中出现的次数即chars[i],最后遍历字符串,找chars[char - 'a']位置的值是否为1,遇到的第一个为1的即返回结果。

        public int firstUniqChar(String s) {
            char[] chars = new char[26];
            for(char c : s.toCharArray()) {
                chars[c - 'a']++;
            }
    
            for(int i = 0;i < s.length();i++) {
                char c = s.charAt(i);
                if(chars[c - 'a'] == 1) {
                    return i;
                }
            }
            return -1;
        }
    
  2. 除留余数法

    设哈希表中允许的地址数为m,取一个不大于m,但是最接近或者等于m的质数n作为除数,按照哈希函数hash(key) = key % n,将关键字转成地址。

冲突避免–负载因子调节

哈希表的负载因子定义为:α = 表中已经存储的元素个数 / 表的长度

负载因子和冲突率的关系(粗略演示)

在这里插入图片描述

闭散列

闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明哈希表还有空间,那么可以将key放到冲突位置的下一个空位置去。寻找空位置的方法有两个:

1、线性探测

线性探测即对于key先通过哈希函数计算出地址,如果计算出的地址已经存在元素,那么就说明在该位置发生了哈希冲突。就从冲突位置开始,依次向后探测,直到寻找到下一个空位为止。

  • 插入
    • 通过哈希函数获取待插入key在哈希表中的位置
    • 如果该位置没有元素则直接插入,如果存在元素则使用线性探测找到下一个空位再插入新元素

例如:在刚才的基础上,再插入24,33

在这里插入图片描述

原本24,33应该插入的位置为4和3,但是因为4和3已经有元素存在。因此将向后探测找下一个空位为6,插入24,同理在7号位置插入33。

缺点:产生冲突的数据堆积在一起。

注意采用闭散列处理哈希冲突时不能随便删除哈希表中已有的元素,若直接删除元素会影响其它元素的搜索。例如:删除元素4,我们要查找24号元素,用哈希函数算出位置在4,但是4被删除了,那么24这个元素就不会被找到了。因为当4存在时,我们通过哈希函数计算出4,然后去4找发现与我们要的数字不匹配,那么我们就从4往后找24。但是4被删除后,我们找4位置发现没有元素,就以为整个哈希表中没有24。

2、二次探测

线性探测发生冲突后找洗一个空位置,导致产生冲突的数据堆积在一起。为了避免该问题,二次探测找下一个空位的方法为:hash(key) = (hash1(key) + i^2 ) % m 或者 hash(key) = (hash1(key) - i^2 ) % m。其中hash1(key)为通过哈希函数计算得到的key的地址,i 为探测过程中发生的冲突次数,m为哈希表的大小。

在这里插入图片描述

闭散列最大的缺陷就是空间利用率低

开散列

开散列又叫链地址法,首先根据关键字计算散列地址,具有相同地址的关键码归为同一子集合,每一个子集合称为一个桶,每个桶中的元素通过单链表连接起来,链表的头节点存储在哈希表中。jdk1.8的hashMap底层实现采用的是数组+链表+红黑树,当链表长度超过8时,将链表自动转化为红黑树,以提高效率。

在这里插入图片描述

开散列中每个桶中放的都是发生哈希冲突的元素,开散列可以认为是把一个在大集合中的搜索问题,转化在一个小集合中的搜索。搜索时,通过哈希函数找到在大集合中的位置,然后在该位置的桶中依次查找。当然桶中的元素并不是越多越好,桶中元素越多,查找时也会费时间。这时,我们可以把桶设计为搜索树。

哈希表实现

class BolgHashBuck{
    static class Buck {
        public int key;
        public int val;
        public Buck next;

        public Buck(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }

        public Buck[] array;
        public int usedsize;
        public BolgHashBuck() {
        this.array = new Buck[10];
        this.usedsize = 0;
        }

        //插入
        public void put(int key,int val) {
        Buck buck = new Buck(key,val);
        int index = key % this.array.length;
        //先找一遍,有没有同样key的元素,有就替换
        for (Buck cur = this.array[index]; cur != null;cur = cur.next) {
            if (cur.key == key) {
                cur.val = val;
                return;
            }
        }
        //头插
            buck.next = this.array[index];
            this.array[index] = buck;
            this.usedsize++;
            //大于负载因子时扩容
            if (loadFactor() > 0.75) {
                resize();
            }

        }

        //扩容,重新哈希
        public void resize() {
        Buck[] newArray = new Buck[this.array.length * 2];
            for (int i = 0; i < this.array.length; i++) {
                Buck curNext = null;
                for (Buck cur = this.array[i]; cur != null;cur = curNext) {
                    curNext = cur.next;

                    int index = cur.key % newArray.length;
                    cur.next = newArray[index];
                    newArray[index] = cur;

                }
            }
            this.array = newArray;
        }

        //获取key的值
        public int getValue(int key) {
        int index = key % this.array.length;
        for(Buck cur = this.array[index];cur != null; cur = cur.next) {
            if (cur.key == key) {
                return cur.val;
            }
        }
        return -1;
        }

        //负载因子
    public float loadFactor() {
            return this.usedsize * 1.0f / this.array.length;
    }
 }

在这里插入图片描述

如图,key=21和key=1发生冲突,然后将21头插到1号位置

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值