HashMap和HashSet

本文深入探讨了哈希表的基本概念,包括哈希函数的设计原则如直接定制法和除留余数法,以及解决哈希冲突的闭散列(开放定址法)和开散列(链地址法)。同时,阐述了HashMap和HashSet在Java中的实现,它们都基于哈希表,HashSet依赖于HashMap。文章还介绍了如何在冲突发生时通过负载因子调整哈希表大小,并讨论了哈希表的动态扩容策略。
摘要由CSDN通过智能技术生成


一、Hash是什么?

Hash也称散列、哈希。其原理就是把任意长度的输入,通过对应的Hash函数(或者叫散列函数)变成固定的输出,所构造出来的结构就叫哈希表(Hash Table)(或者叫散列表)。Hash表的搜索不必进行多次没有必要的操作,大大提高了搜索效率,但这样存在有一定的问题,那就是当两个不同的关键值通过同一个Hash函数计算后得到了相同的Hash地址,又怎么办呢?

我们将这个情况称为Hash冲突或Hash碰撞。把具有不同关键码而具有相同Hash地址的数据元素称为“同义词”。

由于底层的Hash表的数组容量一般是小于实际要存储的数据容量,所以冲突的发生是必然的,我们只能做到降低冲突率。

那么如何做到降低冲突率呢?

我们需要设计一个合理的哈希函数。

下面是哈希函数设计的原则:
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单

常用的有:

直接定制法
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况。

除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

但是不管怎么样,哈希冲突是必然会发生的,那么我们该如何解决冲突呢?首先我们来了解一下一个概念,那就是散列表的负载因子,负载因子 = 表中的元素个数/散列表的长度。当负载因子越大,发生冲突的概率就越大,所以我们就需要调整哈希表中数组的大小了。

解决冲突最常用的两种方法就是闭散列和开散列两种方法。

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

  • 线性探测法:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
     
  • 二次查找法:从发生冲突位置往后找,下一个位置的求法为:Hash(key) = (Hash(key) + d^2) % 11   其中d是距离冲突位置的长度

需要注意的是:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素
 

开散列:也叫链地址法  、开链法,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中,当出现重复冲突时,把冲突元素新建一个链表结点,尾插到哈希表中的对应冲突位置的元素后面。当链表的元素过大时,就需要将链表树化,变为红黑树,加强它的搜索效率。
 

二、关联

Map和Set是一种专门用来查找的容器或者说是一种数据结构,他们的搜索效率都是非常快的,在我们日常的编写代码中经常会用到。

HashMap和HashSet的底层都是用Hash表来实现的。Set只能够存放Key值,Map可以存放Key-Val的键值对,但是两个的Key值都是只能够存在一个相同的值。

这里我们看一下HashSet的add方法的原码。很显然,它是用HashMap的put方法来实现的。所以HashSet的底层就是一个HashMap。


 三、Hash表的实现

要实现Hash表,我们先来

public class HashBuck {
    static class Node {//hash表中的存储数据的类
        public int key;
        public int val;
        public Node next;//定义成链表的形式,可以往后添加元素

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

    public Node[] array ;//hash表
    public int usedSize;//记录 当前哈希桶当中 有效数据的个数

    public HashBuck() {
        this.array = new Node[10];
        this.usedSize = 0;
    }
    private final float FACTOR = 0.75f;//定义最大负载因子数

    
     //存储key val
    
    public void put(int key,int val) {
        Node node = new Node(key,val);//先创建新结点
        int index = key%array.length;//利用hash函数计算key值对应的hash位置
        Node cur = array[index];
        while (cur != null){//当不为空时跳出
            if (cur.key == key){//如果有相同的key,那么更新它的val
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        node.next = array[index];//采用头插法,
        array[index] = node;
        usedSize++;
        float f = usedSize*1.0f/array.length;//看插入后负载因子是否超过了最大限度
        if (f >= FACTOR){//如果超过了,就扩容
            grow();
        }
    }
    *//**
     * 1. 遍历数组的每个元素的链表
     * 2. 每遍历到一个节点,就重新哈希  key % len
     * 3. 进行头插法
     *//*
    private void grow() {

        Node[] newArray = new Node[2* array.length];//二倍扩容
        //重新的哈希
        for (Node cur : array){
            while (cur != null){
                Node curNext = cur.next;
                int key = cur.key;
                int index = key%newArray.length;
                cur.next = newArray[index];
                newArray[index] = cur;
                cur = curNext;
            }
        }
        this.array = newArray;
    }

    *//**
     * 通过key值 获取val 值
     * @param key
     * @return
     *//*
    public int get(int key) {
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while (cur != null){
                if (cur.key == key){
                    return cur.val;
                }
                cur = cur.next;
            }
        }
        return -1;
    }

总结

这就是我对HashMap和HashSet的一点讲解和认识,希望能够帮助到你。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值