哈希表(带图解)

2023-11-09

哈希表

常见的搜索方式:
1、顺序搜索——O(N)
2、二分搜索——O(log₂N)
3、搜索树结构中的查找:二叉树搜索——O(N),AVL——O(log₂N),红黑树——O(log₂N)
以上都需要比较,那有没有不需要比较就能查找的方法呢?

概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log₂N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中插入元素时,根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
当向该结构中搜索元素时,对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该结构即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)
哈希函数常设置为:hash(key)= key % capacity,capacity为存储元素底层空间的总大小,对于数组 capacity = array.length
在这里插入图片描述
多个不同元素,通过相同的哈希函数计算出相同的值,很明显44插入的位置上已经有元素了,这就是哈希冲突(碰撞)

哈希冲突

对于两个数据元素的关键字Ki和Kj(i != j),有Ki!=Kj,但有:Hash(Ki) == Hash(Kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”

冲突避免

首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率

冲突避免-哈希函数设计

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

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单
    常见哈希函数(这里介绍两种常用的)
    1. 直接定制法
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况
    2. 除留余数法
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

冲突避免-负载因子调节

哈希的负载因子=哈希表中有效元素个数 / 表格容量
负载越小:存的元素越少,发生的冲突概率越低

负载因子和冲突率的关系粗略演示
在这里插入图片描述
所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。

哈希冲突的解决方式

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
1、线性探测
比如上面的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

插入

  • 通过哈希函数获取待插入元素在哈希表中的位置
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
  • 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素
    哈希表格中每个空间必须有带状态的标记:EMPTY(判断空),EXIST(判断存在),DELETE(删除元素)
    在这里插入图片描述
    线性探测
    优点:处理哈希冲突方式简单——逐个挨着往后找
    缺点:容易产生数据堆积——一旦发生冲突,数据就会连成一片

    2、二次探测
    线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:Hi= (H0+i²)% m, 或者:Hi= (H0-i²)% m。其中:i = 1,2,3…, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
    研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。 因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
    二次探测
    优点:解决了线性探测数据堆积的问题
    缺点:当表格中元素越来越多,二次探测需要的次数也会增加

开散列/哈希桶

相同于数组+链表
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
在这里插入图片描述
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。

实现

public class HashBucket {
    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 HashBucket(int initCapacity){
        initCapacity = initCapacity <= 0? 16 : initCapacity;
        table = new ListNode[initCapacity];
    }

    // O(1)
    public Integer put(Integer key, Integer value){

        // 0. 是否需要扩容
        checkCapacity();

        // 1. 根据哈希函数计算对应的桶号
        int bucketNo = hashFunc(key);

        // 2. 检测key在bucketNo桶中是否存在
        // 相当于在链表中查找一个元素是否存在
        ListNode cur = table[bucketNo];
        while(cur != null){
            if(key.equals(cur.key)){
                Integer oldValue = cur.value;
                cur.value = value;
                return oldValue;
            }

            cur = cur.next;
        }

        // 2. 将元素插入到bucketNo桶中
        ListNode newNode = new ListNode(key, value);
        newNode.next = table[bucketNo];
        table[bucketNo] = newNode;
        ++size;
        return value;
    }

    // O(1)
    public Integer get(Integer key){
        // 1. 通过哈希函数计算key对应的桶号
        int bucketNo = hashFunc(key);

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

            cur = cur.next;
        }

        return null;
    }

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

        return value;
    }

    public Integer remove(Integer key){
        // 1. 通过哈希函数计算key对应的桶号
        int bucketNo = hashFunc(key);

        // 2. 在bucketNo桶中找待删除的节点
        ListNode cur = table[bucketNo];
        ListNode prev = null;
        while(cur != null){
            if(key.equals(cur.key)){
                Integer oldValue = cur.value;

                // 要删除的节点刚好是第一个
                if(table[bucketNo] == cur){
                    table[bucketNo] = cur.next;
                }else{
                    prev.next = cur.next;
                }


                cur.next = null;
                --size;
                return oldValue;
            }

            // prev = cur;
            cur = cur.next;
        }

        return null;
    }

    // O(1)
    public boolean containsKey(Integer key){
        // 可以按照哈希的特性来查找
        return null != get(key);
    }

    // O(n)
    public boolean containsValue(Integer value){
        for(int bucketNo = 0; bucketNo < table.length; ++bucketNo){
            ListNode cur = table[bucketNo];

            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先从table[i]桶中移除---类似头删
                    table[i] = cur.next;

                    // 将cur往newTable中插入
                    int bucketNo = cur.key % newTable.length; //hashFunc(cur.key);
                    cur.next = newTable[bucketNo];
                    newTable[bucketNo] = cur;

                    //...

                    cur = table[i];
                }
            }

            table = newTable;
        }
    }

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

哈希表(带图解) 的相关文章

随机推荐

  • 表或视图不存在 但是明明存在_为什么Oracle数据库插入数据提示表不存在但却能查到这个表?...

    概述 分享一个最近碰到的奇怪现象 数据库版本为11 2 0 1 插入数据提示表不存在但却能查到这个表 而且这个表所属用户就是他自己 奇怪现象 这张表是真实存在的表 右键查看是可以看到表定义的 可以看到查询是可以查到结果的 但是插入表却提示表
  • xss绕过尖括号和双括号_XSS绕过<>进行测试

    大家都知道 普遍的防御XSS攻击的方法是在后台对以下字符进行转义 但是经过本人的研究发现 在一些特殊场景下 即使对以上字符进行了转义 还是可以执行XSS攻击的 首先看一个JS的例子 Default 1 2 3 4 vars u003cu00
  • 什么是张量流

    An end to end open source platform for Machine Learning 端到端的机器学习开源平台 Before we start with TensorFlow we will need to kno
  • 论文简读 LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS

    论文地址 https arxiv org pdf 2106 09685 pdf 项目地址 https github com microsoft LoRA 全文翻译地址 https zhuanlan zhihu com p 611557340
  • kyrieboot管理系统开发记录------前端相关

    一直想要自己动手写一个管理系统 在这之前只是搭建了简单的项目框架和一部分简单功能 但是由于工作等种种原因一直没能完善功能 趁着今年五一假期 补上一些目前想到的功能 后续会补上一些快捷开发的功能 比如代码生成器等 但由于五一后要进入封闭开发
  • 10个算法从业人员必须知道的TensorFlow技巧

    作者 Rohan Jagtap 编译 ronghuaiyang 导读 掌握这些可以更高效的模型的提高开发效率 TensorFlow 2 x在构建模型和TensorFlow的整体使用方面提供了很多简单性 那么TF2有什么新变化呢 使用Kera
  • STL(标准模板库)的使用

    1 STL初识 1 1 STL的诞生 C 的面向对象和泛型编程思想 目的就是复用性的提高 为了建立数据结构和算法的一套标准 诞生了STL 1 2 STL基本概念 STL 标准模板库 STL从广义上分为 容器 算法 迭代器 容器和算法之间通过
  • springboot + 读写分离+redis集群+集群部署

    记录一次 集群部署springboot 项目 1 nginx 使用 upstream 进行集群管理 upstream serviceGroup server 127 0 0 1 9090 server xxxxx 9090 server x
  • JS 中的自定义对象加 js 中的事件的详细讲解

    JS 中的自定义对象 Object 形式的自定义对象 对象的定义 var 变量名 new Object 对象实例 空对象 变量名 属性名 值 定义一个属性 变量名 函数名 function 定义一个函数 对象的访问 变量名 属性 函数名
  • 一个不错的选色网站 color picker

    进入后 右上角点击进入 点击进入 转载于 https www cnblogs com sofire archive 2010 10 12 1849141 html
  • 通用图片分类项目

    generalImageClassification 文章目录 generalImageClassification 1 数据准备 1 1 开源数据集 1 2 利用特定网站爬数据 2 分类模型的选择 3 代码结构及使用方法 3 1 代码结构
  • python基础练习题--变量

    01计算下列表达式 30 32 8 3210 342 8 5 22 3 2 4 7 34 5 1 3 2 16mod7 7 30 3 2 8 3 2 10 result1 30 pow 3 2 8 pow 3 2 10 print resu
  • Delphi集合数据类型的应用

    集合类型的一般形式为 set of 基类型 type 集合类型名称 Set of 基类型 基类型可以为 字符型 布尔型 枚举型和子界型 不能是整型 实型 1集合中的元素是相异的 不重复 2集合中的元素是没有顺序的 3集合中的元素不能超过25
  • Java对象的比较

    在Java中的比较有两种 基本类型之间的比较和引用类型之间的比较 对于基本类型来说 可以进行直接的比较 int long short byte 可以用 lt gt 进行比较 返回值为 true 或者 false char 也是用 lt gt
  • UFT 自动化测试工具

    QTP是一种基于GUI录制的自动化测试工具 用于在回归测试阶段的时候自动批量执行回归测试用例 和HP 的 Loadrunner 差不多 了解过Loadrunner的学起来很轻松 但又有区别 QTP是记录用户浏览器的操作步骤数据等去达到录制回
  • cmd命令行访问远程mysql数据库

    mysql uhello pworld h192 168 1 88 P3306 Dmysql oa mysql u用户名 p密码 h远程数据库IP地址 P端口 D数据库名
  • ElementUI使用按钮进行图片预览

    使用ElementUI组件进行图片预览 Element官网给出的组件为el image组件 该组件是点击图片时进行预览 而我需要的是点击按钮进行预览图片 需求是点击预览按钮后先去请求接口返回图片 等图片返回后直接将图片预览展示 找到了el
  • elasticsearch-索引分片和副本设置

    索引设置 你可以通过修改配置来自定义索引行为 详细配置参照 ref index modules html 索引模块 Tip Elasticsearch 提供了优化好的默认配置 除非你理解这些配置的作用并且知道为什么要去修改 否则不要随意修改
  • 抖音壁纸表情包小程序流量主收益怎么样?

    壁纸小程序源码 后台 注意注意此处是原作者 注意注意此处是原作者 注意注意此处是原作者 抖音壁纸小程序源代码 此次新增和优化功能如下 达人入住 达人审核 收益管理 下载壁纸页面UI优化 素材管理 素材上传 新增抖音图片检测接口 消息通知 达
  • 哈希表(带图解)

    哈希表 常见的搜索方式 1 顺序搜索 O N 2 二分搜索 O log N 3 搜索树结构中的查找 二叉树搜索 O N AVL O log N 红黑树 O log N 以上都需要比较 那有没有不需要比较就能查找的方法呢 概念 顺序结构以及平