一文吃透哈希表

简介: 哈希表

1.什么是哈希表


线性表、树


在这些结构中,记录在结构中的相对位置是随机的,和记录的关键字之间不存在确定关系,因此,在结构中查找时需要进行一系列和关键字的比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log2N),搜索的效率取决于搜索过程中元素的比较次数。


哈希表


理想的情况下:可以不经过任何比较,一次直接从表中得到要搜索的元素。

哈希表就是通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

在向该结构中插入元素时,通过函数可以计算出该元素的存储位置;同样地,在搜索该元素时,对元素的关键码进行同样的函数计算,就又可以找到该元素。

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


eg:

数据集合:{1,3,5,7,9}

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


微信图片_20230111103947.png

2.哈希冲突


不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。


比如在1中的例子中,如果在数据集合中添加一个数“13”,那么它通过哈希函数计算出来的位置与元素“3”位置相同,这种情况就叫做哈希冲突。


3.解决冲突


由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率,下边有几种方式来尽量解决冲突。


3.1 哈希函数设计


引起哈希冲突的一个原因可能是:哈希函数设计不够合理。

下边我们介绍两种常见的函数设计方法:


1.直接定制法:


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

优点:简单、均匀

缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况


2.除留取余法:


设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:

Hash(Key)=Key%p(p<=m),将关键码转换成哈希地址


3.2 负载因子调节


负载因子的定义为:α=填入表中元素的个数/表的容量


微信图片_20230111103941.png

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


也就是说随着元素的填入,负载因子增大,冲突率也会增大。

为了降低冲突率,只能降低负载因子,但是要填入的元素的个数不能少,所以只能扩大容量


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


3.3 解决方法——闭散列


闭散列也叫做开放地址法,就是在发生哈希冲突时,如果哈希表还没有被装满,Key就可以被存放到冲突位置的“下一个”空位置处。寻找“下一个”空位置的方法有以下两种:


1.线性探测法


从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。


微信图片_20230111103938.png

弊端:


  • 这种方法会把可能冲突的元素都放在一起
  • 删除影响大:假如删除了3这个元素,会影响到13、23等冲突的元素


2.二次探测法

Hi=(H0+i2)%capacity

H0为冲突位置,Hi为第i个冲突元素的位置,i为冲突的次数


微信图片_20230111103933.png

闭散列在负载因子超过0.5时就必须要考虑扩容,所以导致其空间利用率比较低。


3.4 解决方法——开散列/哈希桶(重点掌握)


开散列法又叫做链地址法(开链法),将具有相同哈希地址的元素(或记录)存储在同一个线性链表(桶)中,各链表的头结点存储在哈希表中。(注:当桶中元素较多时,线性链表结构就会变为树形结构——红黑树)


微信图片_20230111103929.png

由上图可得,开散列的每个桶中都是发生哈希冲突的元素


开散列可以认为是把在大集合中搜索转换为在小集合中搜索了。


4.代码实现


哈希表结构的实现:


public class HashBuck {
    static class Node {
        public int key;
        public int val;
        public Node next;
        public Node(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }
    public Node[] array;//节点数组 用于存储各链表的头结点
    public int usedSize;//记录当前哈希桶中有效数据的个数
    //默认的负载因子
    public static final float DEFAULT_LOAD_FACTOR=0.75F;
    public HashBuck() {
        this.array=new Node[10];
        this.usedSize=0;
    }
}


哈希表方法实现:


put方法:

/**
     * 存储key val
     * @param key
     * @param val
     */
    public void put(int key,int val) {
        Node node=new Node(key, val);
        int index=key%array.length;
        Node cur=array[index];
        //先判断桶中是否有相同的关键字,如果有完成value的替换
        while (cur!=null) {
            if(cur.key==key) {
                cur.val=val;
                return;
            }
            cur=cur.next;
        }
        //头插法完成节点的插入
        node.next=array[index];
        array[index]=node;
        usedSize++;
    }


该方法尚未完全完成,还需要考虑增容问题,在哈希表中判断是否需要增容,由负载因子决定。

负载因子的求法:


private float loadFactor() {
        return usedSize*1.0f/array.length;
    }


哈希表的增容并不像数组增容那样简单,由于每个节点都是通过哈希函数来确定位置的,而哈希函数与容量有关,所以在完成增容后,哈希表中的每一个节点都需要重新进行哈希,获取新的位置。


增容方法:


private void grow() {
        Node[] newArray=new Node[2*array.length];
        //遍历原哈希表中的每一个哈希桶,让每个节点都重新哈希
        for (int i = 0; i < array.length; i++) {
            Node cur=array[i];
            while (cur!=null) {
                //获取新的位置
                int index=cur.key% newArray.length;
                Node curNext=cur.next;//记录
                //头插法完成新的哈希
                cur.next=newArray[index];
                newArray[index]=cur;
                //因为需要遍历链表中的所有节点,所以需要提前记录cur.next
                cur=curNext;
            }
        }
        this.array=newArray;
    }


get方法:


 

public int get(int key) {
        int index=key%array.length;
        Node cur=array[index];
        while (cur!=null) {
            if(cur.key==key) {
                return cur.val;
            }
            cur=cur.next;
        }
        return -1;
    }


在刚才所有的代码示例中,key都是一个普通类型,可以直接通过哈希函数来获得一个具体位置,但如果key是一个引用类型呢?

此时就需要调用该类型的hashcode()方法将其转化为整数,所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方 法,而且要做到 equals 相等的对象,hashCode 一定是一致的。


代码示例:


自定义类:


class Person {
    public String id;
    public Person(String id) {
        this.id=id;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(id, person.id);
    }
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}


5.面试题


1.两个对象的hashcode一样,那么equals一样吗?


两个对象的equals一样,那么hashcode一样吗?


不同对象可能获得相同的hashcode值,这两个对象只是在经过哈希函数以后存储到了同一个位置处的链表上,但不一定相同。

如果两个对象的equals相同,说明一定在同一位置,hashcode一定相同。


2.HashMap<K,V> map=new HashMap<>();底层的数组有多大?


微信图片_20230111103920.png

在源码中可以看出,在不含参数的构造方法中,只有负载因子的初始化,并没有初始化数组,所以数组长度为0。


3.HashMap<K,V> map=new HashMap<>(25);底层的数组有多大?


微信图片_20230111104956.png


微信图片_20230111105001.png

微信图片_20230111105004.png



给定初始容量的初始化,会返回接近该容量的2次幂,所以该题返回36。


4.扩容需要注意什么?


扩容需要注意所有元素都需要重新根据新的容量进行哈希,放置到新的位置。

目录
打赏
0
0
0
0
4
分享
相关文章
『GitHub项目圈选周刊01』一款构建AI数字人项目开源了!自动实现音视频同步!
『GitHub项目圈选周刊01』一款构建AI数字人项目开源了!自动实现音视频同步!
1790 0
阿里云飞天企业版获评2024年AI云典型案例
近日,由全球数字经济大会组委会主办、中国信息通信研究院和中国通信企业协会承办的“云·AI·计算国际合作论坛”作为2024全球数字经济大会系列活动之一,在北京举办。论坛以“智启云端,算绘蓝图”为主题,围绕云·AI·计算产业发展、关键技术、最佳实践等展开交流讨论。阿里云飞天企业版异构算力调度平台获评2024年AI云典型案例。
333 3
使用java语言制作一个窗体(弹窗),用来收集用户输入的内容
该博客文章介绍了如何使用Java Swing中的JFrame创建一个窗体来收集用户输入的内容,并提供了详细的实现步骤和完整代码示例。
使用java语言制作一个窗体(弹窗),用来收集用户输入的内容
函数计算 FC:首发 GPU 极速模式,更弹性、更降本
2024 云栖大会上,函数计算 FC 为 AI 加码,首发 GPU 极速模式,让 GPU 可以更弹性、更便宜。
406 14
从入门到精通:Python 系统编程中的跨平台兼容性攻略
【8月更文挑战第6天】在编程领域,Python 以简洁强大著称。系统编程时,确保代码能在不同平台(如 Windows、Linux 和 macOS)上良好运行至关重要。本文探讨 Python 跨平台兼容性的关键点,帮助理解各系统间的差异,例如文件路径、权限管理和进程控制的不同。通过使用 `os` 和 `subprocess` 模块,可以编写出既灵活又兼容的代码。例如,使用 `os.path.join()` 处理路径差异,`subprocess.run()` 进行进程管理。此外,还需关注环境变量和权限管理等方面的平台特性。掌握这些技巧,您将能更自信地开发跨平台的系统程序。
437 0
西瓜书机器学习AUC与ℓ-rank(loss)的联系理解以及证明(通俗易懂)
西瓜书机器学习AUC与ℓ-rank(loss)的联系理解以及证明(通俗易懂)
460 0
Rust Web框架概览:Actix-Web与Yew的探索之旅
本文深入探讨了Rust编程语言中两个备受瞩目的Web框架:Actix-Web和Yew。我们将详细介绍这两个框架的核心特性、应用场景、性能优势以及如何使用它们构建高效、安全的Web应用。通过本文,您将更全面地了解Rust在Web开发领域的潜力和实践。
[Eigen中文文档] 高级初始化
本文介绍了几种用于初始化矩阵的高级方法。提供了有关之前介绍的逗号初始化程序的更多详细信息。还解释了如何获得特殊矩阵,例如单位矩阵和零矩阵。
278 0
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问