哈希表以及哈希冲突

2023-11-03

目录

哈希表

哈希冲突

1. 冲突发生

2. 比较常见的哈希函数

3. 负载因子调节(重点)

散列表的载荷因子概念 

负载因子和冲突率的关系

冲突-解决-闭散列

线性探测

二次探测

冲突-解决-开散列

结尾


我们在前面讲解了TerrMap(Set)的底层是一个搜索二叉树,同时Set和Map还存在HashSet(Map)类,但是HahsMap(Set)到底是什么呢?本章来详细介绍以下。

首先来了解以下什么叫做哈希表:

哈希表

在学过的诸多数据结构中如果存在一种删除和插入的结构,不经过任何比较,一次直接从表中搜索到数据,其时间复杂度能够达到O(1),那么这将非常恐怖,其他数据结构在它面前都不值一提。不错,哈希表这种结构就是能够达到O(1)的恐怖结构,但是如果它真的那么好用,不就说明不用学习其他的结构吗?那么它一定存在它的问题。

当向该结构中:
插入元素:
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素:
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)。

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

 当然如果我们再次插入14呢?结果为4啊,又该插入在哪里呢?

这个就是我们本章的重点,哈希冲突,4%10 = 4;14%10 = 4,此时发生了哈希冲突。

哈希冲突

1. 冲突发生

首先我们得知道,哈希冲突是必然的,无论怎么插入,插入多少都无法杜绝,哪怕就插入两个元素4,14都发生了哈希冲突,我们能做的就是尽量避免哈希冲突的发生。

这也就是我们哈希表这种结构存在的问题。

哈希冲突的概念:两个不同关键字key通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

引起哈希冲突的一个原因可能是:哈希函数设计不够合理;那我们来看看哈希函数设计原则:

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

2. 比较常见的哈希函数

常见哈希函数

直接定制法--(常用)

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

除留余数法--(常用)

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

平方取中法--(了解)

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对 它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分 布,而位数又不是很大的情况

等等.......

3. 负载因子调节(重点)

散列表的载荷因子概念 

负载因子和冲突率的关系

例如:

所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。

冲突-解决-闭散列

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

线性探测

比如上面的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入
·通过哈希函数获取待插入元素在哈希表中的位置
·如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
 


这样插入没问题,但是我们利用哈希表不只是插入元素,我们还有其他操作;例如删除:

 我们第一次删除了4,根据我们之前查找的原则,我第二次怎么去找到44呢?我们是根据4存在才能找到44,这时就发生问题了,第二次删除时哈希表就认为不存在44这个元素。

那么我们有什么办法解决呢?

这时就需要我们标记一下。

 这就涉及到了二次探测。

二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法:

 

其中: i = 1,2,3.....,是通过散列函数Hash(x)对元素的关键码key进行计算得到的位置m是表的大小。

那么我们就按照该方法插入进数组中:

无论如何,二次探测的空间利用率是不高的,假设负载因子时0.75,那么插入第八个数据时就需要扩容了。

哈希还有一种解决方法:开散列。

冲突-解决-开散列

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

例如:

开散列中每个桶中放的都是发生哈希冲突的元素。

开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。

 当数据非常大的时候,那么这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,也就是再对其进行树化,将其转化为红黑树,这个后面再讲。

所以我们的哈希桶也就是由 数组 + 链表 + 红黑树 组成的。

上述所有的一切都是为了引出HashSet(Map)的实现逻辑,原理就是 数组 + 链表 + 红黑树 ,我们接下来用数组 + 链表简单模拟实现哈希桶。

模拟实现代码:

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 int usedSize;//存放的数据的个数

    public static final double DEFAULT_LOAD_FACTOR = 0.75;//默认的负载因子

    public HashBuck () {
        array  = new Node[10];
    }
    public static final double LOAD_FACTOR = 0.75;

    /**
     *
     * @param key
     * @param val
     * @return 代表你插入的元素的val
     */
    public void put(int key,int val) {
        int index = key % array.length;
        Node cur = array[index];
        while (cur != null) {
            if(cur.key == key) {
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        //采用头插法进行插入
        Node node = new Node(key,val);
        node.next = array[index];
        array[index] = node;
        usedSize++;
        if(calculateLoadFactor() >= LOAD_FACTOR) {
            //扩容
            resize();
        }
    }
    private void resize() {
        Node[] newArray = new Node[2* array.length];
        for (Node node : array) {
            Node cur = node;
            while (cur != null) {
                Node curNext = cur.next;
                int index = cur.key % newArray.length;//找到了在新的数组当中的位置
                cur.next = newArray[index];
                newArray[index] = cur;
                cur = curNext;
            }
        }
        array = newArray;
    }

    //计算负载因子
    private double calculateLoadFactor() {
        return usedSize*1.0 / array.length;
    }

    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;
    }
}

由于在扩容过程中地址映射不在原来的位置了,所以要对其进行重新哈希:

 我们上面代码给的都是数字,很好去比较,加入我们有其他引用类型怎么比较?

例如给一个老师类:

class Teacher {
    public String id;
    public Teacher() {

    }

    public Teacher(String id) {
        this.id = id;
    }
}

jdk提供了一个方法名叫hashCode(),我们来简单看看:

 根据这个方法,计算得到一个哈希码值;点进去看看

 hashCode继承Object类;通过这样的一个方法我们就可以把一个引用类型转换为一个整数,进而进行比较。

假设还有一个老师id也是“1234”,我们认为他们是同一个人,但是从系统的角度来看,他们又是两个对象:

 所以我们要对其进行重写hashCode方法,通过他们的id去计算哈希码值。

通过快捷键,我们重写了两个方法,hashCode 和 equals  ,equals是用于两个对象的比较。

 再次运行结果如下:

 简单写一个泛型类型的:

public class HashBuck1<K,V> {
    static class Node<K,V> {
        public K key;
        public V value;
        public Node<K,V> next;
        public Node(K key,V value) {
            this.key = key;
            this.value = value;
        }
    }
    public Node<K,V>[] array = (Node<K,V>[])new Node[10];
    public int usedSize;

    public void put(K key,V value) {
        int hash = key.hashCode();
        int index = hash % array.length;
        Node<K,V> cur = array[index];
        while (cur != null) {
            if(cur.key.equals(key)) {
                cur.value = value;
                return;
            }
            cur = cur.next;
        }
        Node<K,V> node = new Node<>(key, value);
        node.next = array[index];
        array[index] = node;
        usedSize++;

    }
}

可以对照上面的那段代码。

结尾

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

哈希表以及哈希冲突 的相关文章

随机推荐

  • 如何在 Vim 中保存并退出

    VIM 是 Vi 改进版的缩写形式 它是一个免费的开源文本编辑器 可以安装在任何操作系统上 无论是 Windows 还是 Linux 操作系统 它可以在 CMD 命令行 模式以及 GUI 图形用户界面 模式下使用 它使用起来非常灵活和可靠
  • 如何在 Windows 上安装 Maven

    Apache Maven 是适用于任何软件项目的优秀构建工具 它可以帮助您管理项目代码及其构建过程 以便您的软件项目保持井井有条并保持其重点 Windows 并不是最受开发人员欢迎的操作系统 但企业和最终用户仍然广泛使用它 幸运的是 有多种
  • 如何在 Ubuntu 18.04 和 16.04 上使用 Nginx 安装多个 PHP 版本

    通常 网络托管管理器为每个 PHP 版本应用程序部署使用单独的服务器 这增加了托管成本 或者 您可以运行多个Docker多个 PHP 版本的容器 本教程帮助您在具有不同 PHP 版本的 Nginx Web 服务器上安装和配置两个 Virtu
  • 如何在 Python 中获取和更改当前工作目录

    在 Python 中处理目录中的文件时 使用绝对路径始终是一个好主意 但是 如果您使用相对路径 则需要了解当前工作目录的概念以及如何查找或更改当前工作目录 绝对路径指定从根目录开始的文件或目录位置 而相对路径从当前工作目录开始 当您运行 P
  • Grep 中的正则表达式 (Regex)

    grep是 Linux 中用于文本处理的最有用和最强大的命令之一 grep在一个或多个输入文件中搜索与正则表达式匹配的行 并将每个匹配行写入标准输出 在本文中 我们将探讨如何在 GNU 版本中使用正则表达式的基础知识grep 在大多数 Li
  • 如何在 Ubuntu 18.04 上安装和使用 Curl

    您正在学习使用以下命令下载文件的教程curl公用事业 您运行该命令并收到以下错误消息curl command not found 没有什么可担心的 这只是意味着curl您的 Ubuntu 计算机上未安装软件包 Curl 是一个命令行工具 允
  • 如何在 CentOS 上创建 sudo 用户

    The sudo命令旨在允许用户以另一个用户 默认为 root 用户 的安全权限运行程序 在本指南中 我们将向您展示如何在 CentOS 上创建具有 sudo 权限的新用户 您可以使用 sudo 用户在 CentOS 计算机上执行管理任务
  • 如何在 Linux 中挂载 NFS 共享

    网络文件系统 NFS 是一种分布式文件系统协议 允许您通过网络共享远程目录 使用 NFS 您可以在系统上安装远程目录并像使用本地文件一样使用远程文件 在 Linux 和 UNIX 操作系统上 您可以使用mount命令将共享 NFS 目录挂载
  • 如何使用 Rsync 排除文件和目录

    Rsync 是一种快速且多功能的命令行实用程序 可通过远程 shell 在两个位置之间同步文件和文件夹 使用 Rsync 您可以镜像数据 创建增量备份以及在系统之间复制文件 复制数据时 您可能需要根据名称或位置排除一个或多个文件或目录 在本
  • 如何在 CentOS 8 上安装 Slack

    Slack是世界上最受欢迎的协作平台之一 它将您的所有通信汇集在一起 Slack 中的对话按频道组织 您可以为您的团队 项目 主题或任何其他目的创建频道 您可以搜索频道或消息中发布的所有内容 Slack 还允许您通过音频或视频通话与队友交谈
  • 如何在 Debian 10 上安装 R

    R 是一种开源编程语言和免费环境 专门从事统计计算和图形表示 它由 R 统计计算基金会支持 主要供统计学家和数据挖掘人员用于开发统计软件和执行数据分析 本文提供有关如何在 Debian 10 上安装 R 的信息 先决条件 在继续本教程之前
  • 如何在 Ubuntu 20.04 上安装 MariaDB

    MariaDB 是一个开源关系数据库管理系统 它最初被设计为向后兼容的 二进制的 MySQL 直接替代品 MariaDB由MySQL的原始开发人员和开源社区开发和维护 本指南介绍了如何在 Ubuntu 20 04 上安装 MariaDB 先
  • 如何在 CentOS 8 上配置和管理防火墙

    防火墙是一种监视和过滤传入和传出网络流量的方法 它的工作原理是定义一组安全规则来确定是允许还是阻止特定流量 正确配置的防火墙是整个系统安全最重要的方面之一 CentOS 8 附带一个名为防火墙 它是一个带有 D Bus 接口的完整解决方案
  • 如何在 Debian 10 Linux 上安装 Jenkins

    Jenkins是一个开源自动化服务器 提供了一种设置持续集成和持续交付 CI CD 管道的简单方法 持续集成 CI 是一种 DevOps 实践 团队成员定期将代码更改提交到版本控制存储库 然后运行自动化构建和测试 持续交付 CD 是自动构建
  • 如何连接到 Docker 容器

    当您想查看容器内发生的情况时 连接到正在运行的 Docker 容器会很有帮助 如果 Docker 容器未按预期工作 您可以附加到容器或为容器获取 shell 并运行以下命令 ps or top 还可以进入容器 安装新的包 构建一个新的 Do
  • 【机器学习】支持向量机(2)——线性可分支持向量机(硬间隔最大化法,对偶算法)

    前言 此文中我们介绍了支持向量机用到的一些概念以及求解方法 接下来我们将分别介绍线性可分支持向量机 线性支持向量机以及非线性支持向量机 首先 我们考虑一个二类分类问题 假设输入空间与特征空间为两个不同的空间 输入空间为欧氏空间或离散集合 特
  • arduino实战 2——利用arduino做一个人体传感器

    arduino是较为简单的单片机 易上手 所以利用arduino开始探索之旅吧 目录 一 材料清单 一 模块介绍 1 HC SR501 2 HC SR04 二 实物展示 1 工作流程 2 连线 二 代码 1 代码展示 2 代码的理解 三 写
  • mybatisplus 修改某个字段为空值

    文章目录 前言 一 重现bug 二 解决方法 总结 前言 需求 移除企业 将ent id设置为null 一 重现bug 1 使用updateById 更新单独个字段为空值 结果报错 Override public void update S
  • Android:换肤框架Android-Skin-Support

    gihub地址 https github com ximsfei Android skin support 样例 默认 更换后 一 引入依赖 换肤依赖 implementation skin support skin support 4 0
  • 哈希表以及哈希冲突

    目录 哈希表 哈希冲突 1 冲突发生 2 比较常见的哈希函数 3 负载因子调节 重点 散列表的载荷因子概念 负载因子和冲突率的关系 冲突 解决 闭散列 线性探测 二次探测 冲突 解决 开散列 结尾 我们在前面讲解了TerrMap Set 的