散列表 订阅
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。 展开全文
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
信息
作    用
直接进行访问的数据结构
别    名
散列表
中文名
哈希表
外文名
Hash table
哈希表基本概念
收起全文
精华内容
下载资源
问答
  • 散列表通讯录系统

    2018-08-06 16:47:16
    一个用c语言编写的散列表通讯录系统,实现了增删改查功能。
  • C语言实现散列表(哈希Hash表) 实例代码: //散列表查找算法(Hash) #include #include #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define SUCCESS 1 #define UNSUCCESS 0 #define HASHSIZE...
  • 设计散列表实现电话号码查找系统。【基本要求】 1)设每个记录有下列数据项:电话号码、用户名、地址﹔ 2)从键盘输入各记录,分别以电话号码和用户名为关键字建立散列表;3)采用一定的方法解决冲突; 4)查找并显示给定...
  • 散列表

    2021-01-23 23:25:48
    散列表(Hash Table) 散列表的英文叫“Hash Table”,我们平时也叫它“哈希表”或者“Hash 表”。 散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,...

    散列表(Hash Table)

    散列表的英文叫“Hash Table”,我们平时也叫它“哈希表”或者“Hash 表”。
    散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。

    • key:键或者关键字。
    • 散列函数(或“Hash 函数”“哈希函数”):把key值转化为数组下标的映射方法。
    • 散列函数计算得到的值就叫作散列值(或“Hash 值”“哈希值”)。

    在这里插入图片描述

    散列函数

    散列函数设计的基本要求:

    • 散列函数计算得到的散列值是一个非负整数;
    • 如果 key1 = key2,那 hash(key1) == hash(key2);
    • 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。(往往这个条件很难办到,key不同可能出现相同的散列值,于是出现散列冲突

    解决散列冲突

    1. 开放寻址法

    (1)线性探测(Linear Probing)(最好O(1);最坏情况下的时间复杂度为 O(n))
    • 插入:如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
    • 查找:过程和插入一样,找到对应数组下标后,对比x与数组中存储的值是否相等,若不等则依次往后查找…
      在这里插入图片描述
    • 删除:删除的元素,特殊标记为 deleted。当线性探测查找的时候,遇到标记为 deleted 的空间,并不是停下来,而是继续往下探测。
      在这里插入图片描述
    (2)二次探测(Quadratic probing)

    和线性探测(Linear Probing)一样,只不过每次步长为2。

    (3)双重散列(Double hashing)

    所谓双重散列,意思就是不仅要使用一个散列函数。我们使用一组散列函数 hash1(key),hash2(key),hash3(key)……我们先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。

    2. 链表法(HashMap和Hashtable就是这样存储的嘛)

    散列表中,每个“桶(bucket)”或者“槽(slot)”会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中:
    在这里插入图片描述

    装载因子(现在知道为什么HashMap和Hashtable有扩容因子0.75了吧!)

    当散列表中数组空闲位置不多的时候,散列冲突的概率就会大大提高。

    装载因子的计算公式是:散列表的装载因子=填入数组中的元素个数/数组长度
    

    装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。

    思考题

    1. 假设我们有 10 万条 URL 访问日志,如何按照访问次数给 URL 排序?

    遍历 10 万条数据,以 URL 为 key,访问次数为 value,存入散列表,同时记录下访问次数的最大值 K,时间复杂度 O(N)。

    如果 K 不是很大,可以使用桶排序,时间复杂度 O(N)。如果 K 非常大(比如大于 10 万),就使用快速排序,复杂度 O(NlogN)。

    2. 有两个字符串数组,每个数组大约有 10 万条字符串,如何快速找出两个数组中相同的字符串?

    以第一个字符串数组构建散列表,key 为字符串,value 为出现次数。再遍历第二个字符串数组,以字符串为 key 在散列表中查找,如果 value 大于零,说明存在相同字符串。时间复杂度 O(N)。
    (其实就用 Set思想就可以了, 第一个String数组 放到 set中(其实就是个散列表, key 是 String, value 无所谓) 然后 拿散列表和 第二个String数组中元素contains() 有就存在 没有就不存在)

    设计散列表

    一、散列函数怎么设计?

    1. **散列函数的设计不能太复杂。**过于复杂的散列函数,势必会消耗很多计算时间,也就间接地影响到散列表的性能。
    2. 散列函数生成的值要尽可能随机并且均匀分布,这样才能避免或者最小化散列冲突,而且即便出现冲突,散列到每个槽里的数据也会比较平均,不会出现某个槽内数据特别多的情况。

    数据分析法:
    处理手机号码,因为手机号码前几位重复的可能性很大,但是后面几位就比较随机,(因为随机,所以会分布比较均匀)我们可以取手机号的后四位作为散列值。这种散列函数的设计方法,我们一般叫做“数据分析法”。

    二、装载因子过大了怎么办?

    为避免低效扩容:

    1. 当装载因子触达阈值之后,我们只申请新空间,但并不将老的数据搬移到新散列表中。
    2. 当有新数据要插入时,我们将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,我们都重复上面的过程。经过多次插入操作之后,老的散列表中的数据就一点一点全部搬移到新散列表中了。
    3. 这期间的查询操作怎么来做呢?对于查询操作,为了兼容了新、老散列表中的数据,我们先从新散列表中查找,如果没有找到,再去老的散列表中查找。

    在这里插入图片描述
    将一次性扩容的代价,均摊到多次插入操作中:任何情况下,插入一个数据的时间复杂度都是 O(1)。

    如何选择冲突解决方法?

    Java 中 LinkedHashMap 就采用了链表法解决冲突,ThreadLocalMap 是通过线性探测的开放寻址法来解决冲突。

    1. 开放寻址法

    1. 优点:开放寻址法不像链表法,需要拉很多链表。散列表中的数据都存储在数组中,可以有效地利用 CPU 缓存加快查询速度。而且,这种方法实现的散列表,序列化起来比较简单。链表法包含指针,序列化起来就没那么容易。
    2. 缺点:开放寻址法解决冲突的散列表,删除数据的时候比较麻烦,需要特殊标记已经删除掉的数据。而且,在开放寻址法中,所有的数据都存储在一个数组中,比起链表法来说,冲突的代价更高。所以,使用开放寻址法解决冲突的散列表,装载因子的上限不能太大。这也导致这种方法比链表法更浪费内存空间。

    当数据量比较小、装载因子小的时候,适合采用开放寻址法。 这也是 Java 中的ThreadLocalMap使用开放寻址法解决散列冲突的原因。

    2. 链表法

    1. 首先,链表法对内存的利用率比开放寻址法要高。因为链表结点可以在需要的时候再创建,并不需要像开放寻址法那样事先申请好。
    2. 对于链表法来说,只要散列函数的值随机均匀,即便装载因子变成 10,也就是链表的长度变长了而已,虽然查找效率有所下降,但是比起顺序查找还是快很多。
    3. 链表因为要存储指针,所以对于比较小的对象的存储,是比较消耗内存的,(如果我们存储的是大对象,也就是说要存储的对象的大小远远大于一个指针的大小(4 个字节或者 8 个字节),那链表中指针的内存消耗在大对象面前就可以忽略),还有可能会让内存的消耗翻倍。而且,因为链表中的结点是零散分布在内存中的,不是连续的,所以对 CPU 缓存是不友好的,这方面对于执行效率也有一定的影响。
    4. 实现一个更加高效的散列表。那就是,我们将链表法中的链表改造为其他高效的动态数据结构,比如跳表、红黑树。这样,即便出现散列冲突,极端情况下,所有的数据都散列到同一个桶内,那最终退化成的散列表的查找时间也只不过是 O(logn)。这样也就有效避免了前面讲到的散列碰撞攻击。
      在这里插入图片描述

    基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。

    HashMap

    1. 初始大小

    HashMap 默认的初始大小是 16,当然这个默认值是可以设置的,如果事先知道大概的数据量有多大,可以通过修改默认初始大小,减少动态扩容的次数,这样会大大提高 HashMap 的性能。

    2. 装载因子和动态扩容

    最大装载因子默认是 0.75,当 HashMap 中元素个数超过 0.75*capacity(capacity 表示散列表的容量)的时候,就会启动扩容,每次扩容都会扩容为原来的两倍大小

    3. 散列冲突解决方法

    HashMap 底层采用链表法来解决冲突。即使负载因子和散列函数设计得再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响 HashMap 的性能。于是,在 JDK1.8 版本中,为了对 HashMap 做进一步优化,我们引入了红黑树。而当链表长度太长(默认超过 8)时,链表就转换为红黑树。我们可以利用红黑树快速增删改查的特点,提高 HashMap 的性能。**当红黑树结点个数少于 6个的时候,又会将红黑树转化为链表。**因为在数据量较小的情况下,红黑树要维护平衡,比起链表来,性能上的优势并不明显。

    4. 散列函数

    散列函数的设计并不复杂,追求的是简单高效、分布均匀。


    散列表和链表的配合使用

    LRU 缓存淘汰算法

    一个缓存(cache)系统主要包含下面这几个操作:

    • 往缓存中添加一个数据;
    • 从缓存中删除一个数据;
    • 在缓存中查找一个数据。

    其实很简单:
    因为链表的查询效率低,而插入和删除的效率高。
    于是:利用数组(散列表)的查询效率,用散列表的散列函数给双向链表做一个“索引”:
    在这里插入图片描述
    前驱和后继指针是为了将结点串在双向链表中,hnext 指针是为了将结点串在散列表的拉链中

    1. 查找一个数据

    散列表中查找数据的时间复杂度接近 O(1),所以通过散列表,我们可以很快地在缓存中找到一个数据。当找到数据之后,我们还需要将它移动到双向链表的尾部。

    2. 删除一个数据

    需要找到数据所在的结点,然后将结点删除。借助散列表,我们可以在 O(1) 时间复杂度里找到要删除的结点。因为我们的链表是双向链表,双向链表可以通过前驱指针 O(1) 时间复杂度获取前驱结点,所以在双向链表中,删除结点只需要 O(1) 的时间复杂度。

    3.添加一个数据(链表不允许有重复key!!!)

    需要先看这个数据是否已经在缓存中。(用散列表去查找)如果已经在其中,需要将其移动到双向链表的尾部;如果不在其中,还要看缓存有没有满。如果满了,则将双向链表头部的结点删除,然后再将数据放到链表的尾部;如果没有满,就直接将数据放到链表的尾部。这整个过程涉及的查找操作都可以通过散列表来完成。

    其他的操作,比如删除头结点、链表尾部插入数据等,都可以在 O(1) 的时间复杂度内完成。所以,这三个操作的时间复杂度都是 O(1)。至此,我们就通过散列表和双向链表的组合使用,实现了一个高效的、支持 LRU 缓存淘汰算法的缓存系统原型。


    Redis 有序集合

    在有序集合中,每个成员对象有两个重要的属性,key(键值)和 score(分值)。我们不仅会通过 score 来查找数据,还会通过 key 来查找数据。

    Redis 有序集合的操作,那就是下面这样:

    • 添加一个成员对象;
    • 按照键值来删除一个成员对象;
    • 按照键值来查找一个成员对象;
    • 按照分值区间查找数据,比如查找积分在[100,356]之间的成员对象;
    • 按照分值从小到大排序成员变量;

    散列表配合跳表使用:
    1. 利用跳表:按照分值(区间)查找对象(那么链表必须要按分值排序)
    2. 再按照键值构建一个散列表:按照 key 来删除、查找一个成员对象(时间复杂度O(1))


    LinkedHashMap(有序)

    但还是不能重复,链表就是不能重复!

    LinkedHashMap 是通过双向链表和散列表这两种数据结构组合实现的。

    和上面的LRU 缓存淘汰算法实现原理一样!(只是,LinkedHashMap支持扩容,和HashMap容量、扩容什么的都一样!)

    在这里插入图片描述

    // 10是初始大小,0.75是装载因子,true是表示按照访问时间排序
    HashMap<Integer, Integer> m = new LinkedHashMap<>(10, 0.75f, true);
    m.put(3, 11);
    m.put(1, 12);
    m.put(5, 23);
    m.put(2, 22);
    
    m.put(3, 26);
    m.get(5);
    
    for (Map.Entry e : m.entrySet()) {
      System.out.println(e.getKey());//1,2,3,5
    }
    
    1. 添加数据到双链表尾部
      先用散列表查找键值是否已存在:
      若存在,则删除存在元素,将新对象插入到双向链表的尾部,并且串在对应散列表的拉链中;
      若不存在,则直接将新对象插入到双向链表的尾部,并且串在对应散列表的拉链中。
      在这里插入图片描述在这里插入图片描述
    2. 删除就用散列表查询到后删除
    3. 访问元素后移到双向链表尾部(因为构造LinedHashMap时,第三个参数accessOrder为true)
      利用散列表访问到元素后,将元素变为双向链表尾结点(但此过程元素仍然串在对应散列表的拉链中)

    为什么散列表和链表经常会一起使用?

    就是为了散列表存储下的数据能够有序遍历!!!
    因为查找、插入、删除这些散列表自己就可以做!!!

    思考题

    1.如果把LinkedHashMap双向链表改成单链表,还能否正常工作呢?为什么呢?
    不能,因为在双向链表中删除元素O(1),而单链表中这就变成O(n)。

    2.假设猎聘网有 10 万名猎头,每个猎头都可以通过做任务(比如发布职位)来积累积分,然后通过积分来下载简历。假设你是猎聘网的一名工程师,如何在内存中存储这 10 万个猎头 ID 和积分信息,让它能够支持这样几个操作:

    • 根据猎头的 ID 快速查找、删除、更新这个猎头的积分信息;
    • 查找积分在某个区间的猎头 ID 列表;
    • 查找按照积分从小到大排名在第 x 位到第 y 位之间的猎头 ID 列表。

    答:以积分排序构建一个跳表,再以猎头 ID 构建一个散列表。

    1)ID 在散列表中所以可以 O(1) 查找到这个猎头;
    2)积分以跳表存储,跳表支持区间查询;
    3)这点根据目前学习的知识暂时无法实现。

    3.Word 文档中单词拼写检查功能是如何实现的?

    常用的英文单词有 20 万个左右,假设单词的平均长度是 10 个字母,平均一个单词占用 10 个字节的内存空间,那 20 万英文单词大约占 2MB 的存储空间,就算放大 10 倍也就是 20MB。对于现在的计算机来说,这个大小完全可以放在内存里面

    所以我们可以用散列表来存储整个英文单词词典。当用户输入某个英文单词时,我们拿用户输入的单词去散列表中查找。 如果查到,则说明拼写正确;如果没有查到,则说明拼写可能有误,给予提示。借助散列表这种数据结构,我们就可以轻松实现快速判断是否存在拼写错误。

    我知道你头楞了,来总结一下!

    • 需要以A属性区间查找对象 ===> 跳表:以A属性将对象排序构造跳表
    • 需要以B属性快速查找(删除 删除前要先查找)===> 散列表:以B属性构建散列表
    • 需要有序遍历整体对象 ===> 双向链表:按照插入(访问)顺序维护一个双向链表

    万变不离其宗:其实这些都是链表和数组的结合使用,数组有利于随机访问,链表有利于插入删除。各种衍生方式的出现都是按照情况将两者结合,扬其所长、避其所短。

    展开全文
  • 设计散列表实现电话号码查找系统。 2、功能需求分析 1) 每个记录有下列数据项:电话号码、用户名、地址; 2) 从键盘输入各记录,分别以电话号码和用户名为关键字建立散列表; 3) 采用一定的方法解决冲突; 4) 查找并...
  • 【编程随想】聊聊分布式散列表(DHT)的原理——以 Kademlia(Kad) 和 Chord 为例
  • 广州XX学院 数据结构与算法 实验报告 成绩 专业班级 计科181 实验日期 2019.12.10 姓 名 XX 学 号 20181533 实验名称 实验6散列表查找操作 指导教师 曾岫 一实验目的 1熟悉散列查找方法和特点 2掌握散列查找解决冲突...
  • 广 州 XX 学 院 数据结构与算法 实验报告 成 绩 专业班级 计科 181 实验日期 2019.12.10 姓 名 XX 学 号 20181533 实验名称 实验 6 散列表查找操作 指导教师 曾岫 一实验目的 1熟悉散列查找方法和特点 2 掌握散列...
  • c代码-散列表的建立,查找,插入,删除
  • 用javafx作为界面,java写的不用数据库的散列表通讯录,写的比较乱,通过main运行
  • HUNAN LNIVLRS1TY 课程实验报告 课 程名称数据结构 实验项目名称 散列表 专业班级 姓 名: XXX 学 号 完成时间 2015 年 06 月 13 日 散列表(Hash table也叫哈希表)是根据关键码值(Key value)而直接进行访问的数据 ...
  • 主要介绍了详解散列表算法与其相关的C语言实现,平时经常出现于各大考试竞赛与程序员面试题目当中,需要的朋友可以参考下
  • 精选文档 PAGE PAGE 5 课程实验报告 课 程 名 称 数据结构 实验项目名称 散列表 专 业 班 级 姓 名 XXX 学 号 完 成 时 间 2015 年 06 月 13 日 背景 散列表Hash table也叫哈希表是根据关键码值(Key value)而直接...
  • Redis散列表(hash)使用

    2021-02-04 18:37:25
    Redis的散列(hash)表会将一个键和一个散列表在数据库中关联起来,用户可以在散列表中为任意多个字设置值。与字符键一样,散列的字段和值既可以是文本数据,可以是二进制数据。 通过使用散列表,用户可以把相关联的...

    Redis有5种常用的数据结构,分别为:string(字符串),list(列表),hash(哈希表或散列表),set(集合)和zset(有序集合)。5种数据结构指的是作为键值对的值存在于Redis库中的形态,顾名思义,string字符串的value值是以字符串结构存在的;hash散列表的value是一个hash散列表。

    散列表简单说明

    Redis的散列(hash)表会将一个键和一个散列表在数据库中关联起来,用户可以在散列表中为任意多个字设置值。与字符串键值对一样,散列表里面的字段和值既可以是文本数据,可以是二进制数据。

    通过使用散列表,用户可以把相关联的多项数据存储到同一个散列里面,以便对这些数据进行管理,或者针对他们进行批量操作。

    上图可以看到,字符串键值对的值是一个单字符串形态的存在;而散列表的value值是一个散列表,或称为哈希表,散列表内部的字段和值对照关系就与字符串的键值对类似了。

    Redis为散列舰提供了一系列操作命令,通过这些命令用户可以:

    • 为散列的字段设置值,或者只在字段不存在的情况下为他设置值从散列表里面获取给定字段的值。
    • 对存储着数字值的字段执行加法操作或者减法操作。
    • 检查给定字段是否存在于散列表当中。
    • 从散列表中删除指定字段。
    • 查看散列表包含的字段数量。
    • 一次为散列表的多个字段设置值或一次从散列表中获取多个字段的值。
    • 获取散列表包含的所有字段,所有值或者所有字段和值。

    散列表命令说明

    hset:为字段设置值

    用户可以通过执行hset命令为散列表中的指定字段设置值。

    hset key field value

    根据给定的字段是否已经存在于散列中命令的执行也会有所不同:

    • 如果给定字段并不存在于散列当中,那么这次设置就是一次创建操作命令,将在散列里面关联起给定的字段和值,然后返回1。
    • 如果给定的字段原本已经存在于散列当中,那么这次设置就是一次更新操作命令,将使用用户给定的新值去覆盖字段原有的旧址,然后返回0。

    上图示例可以看到,在给student_zhangsan第一次设置age字段为14时,因为散列表中没有age字段,字段age属于第一次创建,命令执行完成后返回的是1;待第二次给age再设置值为16时,因为age已经存在于散列表中,命令执行时候,新的字段值16会覆盖掉旧的字段值14,并返回0。


    hsetnx:只在字段不存在的情况下为它设置值

    hsetnx命令的作用和hset命令的作用非常相似,都是给散列表的字段设置值。他们的不同之处在于,hsetnx命令只会在指定字段不存在的情况下执行设置操作。

    hsetnx key field value

    hsetnx命令在字段不存在并且设置成功时返回值1;在字段存在并导致设置失败时返回值0。


    hget:获取字段的值

    hget命令可以根据用户给定的字段,从散列表中获取该字段的值。

    hget key field

    如果给定的字段并不存在于散列表中,那么hget命令将返回一个空值

    通过hkeys命令,可以获取到散列表的所有字段名称,再通过hget去获取一个不存在的字段birthday的值,因散列表中没有该字段,返回为nil。

    如果散列表本身就不存在的时候,我们再去获取字段值,会发生什么事情

    通过上面的示例可以看出,当hget获取结果为nil时,有两种情况:

    • 散列表中该字段不存在
    • 散列表本身不存在

    hincrby:对字段存储的整数值执行加减法操作

    与字符串的incrby命令相似,如果散列表的字段里面存储的能够被Redis解释为整数的数字,那么用户就可以使用hincrby命令为该字段的值加上指定的增量。

    hincrby key field increment

    hincrby命令操作成功后,会返回该字段当前值作为结果。

    与字符串数值加法有decrby命令不同的是,散列表没有对对应字段做减法操作的命令。即如果要对散列表的数字值操作减法,使用hincrby命令,并将增量increment值设置为负值即可。

    同时,字符串里面有增量加1的默认命令incr,在散列表中没有了,对于散列表的增减值都只能用hincrby命令,并根据自己的须要设置合适的增量值increment。

    如果对散列表中非整数的字段使用hincrby 或 增量值increment非整数的时候,会有什么结果

    示例可以看到:

    • hincrby student_zhangsan age 1.2 对age字段增加1.2的时候,因1.2非整数值,报错(error) ERR value is not an integer or out of range
    • hincrby student_zhangsan addr 1 对zddr字段加1的时候,因addr字段值为字符串"beijing",无法解析为数字,报错(error) ERR hash value is not an integer

    hincrbyfloat:对字段存储的数字值执行浮点加减法操作

    hincrbyfloat命令作用与hincrby命令作用类似,hincrby是对字段值为整数的字段进行整数增量值的操作,hincrbyfloat命令可以对整数和浮点数的字段值进行操作,且增量值可以是整数可以是浮点数。与hincrby命令类似,hincrbyfloat命令没有对应的减法命令,如果要操作减法,即要对增量值做负数处理。

    hincrbyfloat key field increment

    hincrbyfloat命令在执行成功后,将返回给定字段值当前值作为结果

    上面示例,展示了hincrbyfloat命令给浮点数增加浮点数值,给整数增加浮点数值。

    上面示例,展示hincrbyfloat命令的减法操作,给增量increment字段赋负值。

    示例中,前面的score均为浮点数,都是在用hincrbyfloat命令做操作,在执行hincrbyfloat student_zhangsan score -0.5后,score值变为了87,后面使用hincrby命令对score字段值操作成功。说明,散列表操作中,字段值若从浮点数变化为整数的时候,系统中默认按照整数存储。


    hstrlen:获取字段值的字节长度

    用户可以使用hstrlen命令获取给定字段值的字节长度

    hstrlen key field

    示例的时候尴尬了,提示hstrlen命令不存在,翻阅了资料发现,hstrlen命令是Redis在3.2.0版本加进来的

    而目前使用的Windows版的Redis是3.0.503版本的。

    hstrlen命令的用法与字符串对应的strlen类似,可以参考前一篇博文Redis字符串键值对使用中关于strlen命令部分。

    hstrlen命令在对于散列表中不存在的字段操作时候,返回0;hstrlen对不存在的散列表操作时候,返回0。


    hexists:检查字段是否存在

    hexists命令用于检查用户给定的字段是否存在于散列表中

    hexists key field

    如果散列表中包含了给定的字段filed,则命令返回1,否则命令返回0。

    上面示例,可以看到name是student_zhangsan中存在的字段,使用hexists命令操作返回1;对于不存在的字段youreyes命令操作时候返回0;对于不存在的散列表not-exists-hash操作返回0。

    同样说明,如果hexists命令结果为0,可能有两种情况:

    • 散列表中字段不存在
    • 散列表不存在

    hdel:删除字段

    hdel命令用于删除散列表中指定字段极其对应的关联值

    hdel key field

    当给定的字段存在于散列表中并且被成功删除的时候,命令返回1;如果给定的字段不存在散列表中,或者给定的散列表不存在的时候,命令返回0。

    上面示例,name字段存在于student_zhangsan中,执行hdel命令后,命令返回1,再通过hget获取name字段,返回空值nil

    上面示例,展示hdel删除散列表中不存在的字段,和hdel操作不存在的散列表情况,结果都是删除失败,返回0。

    上面说的是hdel命令删除散列表中三个字段值的情况,hdel命令可以删除多个字段值

    hdel key field1 field2 ... fieldn

    hdel命令删除多个字段值的时候,命令结果返回删除成功的字段的个数。若字段都不存在或散列表不存在,则返回0。

    上图示例,散列表student_zhangsan中包含addr、score、id、name和age五个字段,使用hdel student_zhangsan name age youreyes 来删除字段name age youreyes,其中name和age是散列表student_zhangsan中存在的字段,youreyes是散列表中不存在的字段。执行hdel命令后,成功删除name和age两个字段和其对应的值,命令返回删除成功的字段的个数2。

    上图示例,展示操作完全不存在的字段和操作不存在的散列表的情况,结果都是0。


    hlen:获取散列表包含的字段数量

    hlen命令获取给定的散列表的字段数量,命令返回结果为散列表中包含的字段数量

    hlen key field

    如果对不存在的散列表执行hlen命令,结果为0。


    hmset:一次为多个字段设置值

    hset命令一次只可以为一个字段设置值,hmset命令一次为散列表的多个字段设置值,hset命令在设置成功时返回OK。

    hmset key field value [field value ...]

    上面示例,展示了hmset的用法,且与hset做了个简单比对。假如我们要给散列表中设置4个字段值进去,使用hset命令须要操作4次,即客户端与Redis服务器有4次通信交互;使用hmset只须要一次即可完成,这里节省了3次通信交互的消耗。所以,在给散列表设置多个字段值的时候,使用hmset是比较高效的。

    上面命令中,我们的字段值都是单字符串,如果是句子或字符串中有空格呢,试试

    上面示例,第一次使用hmset设置hmset db_book3 book1 redis is book1,这里的原意是字段book1的值为redis is book1,设置成功。再通过hmset 设置 hmset db_book3 book1 redis is book1,多设置一个book2字段进去,设置成功。通过hlen命令查看字段数量时候,发现数量是3。通过hgetall命令获取所有字段和值,得到三个字段和对应值。

    这里的问题,由于value值没有加双引号“" "”,系统默认按照空格把每一个词按照字段-值-字段-值的方式处理了,如果value中间有空格时候,value值一定要用双引号引起来。


    hmget:一次获取多个字段的值

    通过hmget命令,用户可以一次从散列表中获取多个字段的值

    hmget key field [field ...]

    执行hmget命令后,结果字段顺序,依次返回字段对应的值,对于不存在的字段,返回空值nil。


    hkeys、hvals、hgetall:获取所有字段、所有值、所有字段和值

    hkeys命令,返回散列表中所有的字段名称

    hkeys key

    hvals命令,返回散列表中所有的字段对应的值

    hvals key

    hgetall结合hkeys和hvalues两个命令的作用,返回散列表中所有的字段名称和对应的值

    hgetall key

    hgetall命令返回结果列表中,每两个连续的元素代表了散列表中的一对字段和值,其中奇数位置上的元素为字段,偶数位置上的元素为字段的值。


    散列表使用总结

    1、散列键会将一个键和一个散列在数据库中关联起来,用户可以在散列中为任意多个字段设置值与字符串键一样散列的字段和值,既可以是文本数据也可以是二进制数据。

    2、用户可以通过散列键,把相关联的多项数据存储到同一散列中,以便对其进行管理,或者针对他们执行批量操作。

    3、因为Redis并没有为散列提供相应的减法操作命令,所以如果用户想要对字段存储的数字值进行减法操作,需要将负数增长量传递给命令。

    4、Redis散列包含的字段在底层是以无序方式存储的,根据字段插入的顺序不同,包含相同字段的散列在执行等命令时可能会得到不同的结果,因此用户在使用这三个命令时,不应该对命令返回元素的排列顺序,做任何假设。

    展开全文
  • 设计散列表实现电话号码查找系统。基本要求:(1)设每个记录有下列数据项:电话号码、用户名、地址;(2)从键盘输入各记录,分别以电话号码和用户名为关键字建立散列表;(3)采用双散列法解决冲突;(4)查找并显示给定...
  • 实验十一 散列表实验

    2012-06-02 23:13:36
    对于给定的一组关键码,分别采用线性探测法和拉链法建立散列表,并且在这两种方法构建的散列表中查找关键码k,比较两种方法的时间性能和空间性能。 2. 基本要求 ⑴ 用线性探测法处理冲突建立闭散列表; ⑵ 用拉链法...
  • 散列存储:散列表,采用的存储方式是散列存储。那么何为散列存储呢?散列存储是根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。采用散列存储的方式存储数据时,具备的优点是在散列表中检索、...

    散列存储的特性

    散列存储:散列表,采用的存储方式是散列存储。那么何为散列存储呢?散列存储是根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。采用散列存储的方式存储数据时,具备的优点是在散列表中检索、增加和删除结点的操作很快;相反,它的缺点也相对比较明显,在插入结点的过程中,若散列函数选择不好,就可能在散列表中出现元素存储单元的冲突,解决冲突会额外的时间和空间开销,费时费力。

    什么是散列表

    散列表:散列表是根据数据元素的关键字而直接进行访问的数据结构。通俗地讲,就是散列表建立了关键字和存储地址之间地一种直接映射关系。这种直接映射关系通过选择的散列函数来完成。

    什么是散列函数

    散列函数:散列函数又是什么呢?散列函数其实是一个把查找表中的关键字映射成该关键字对应的存储地址的函数,记为: Hash(key) = Adr(地址可以是数组下标、索引或内存地址等)。

    什么是冲突

    冲突:何为冲突?冲突其实是通过选择的散列函数,对于两个或两个不同的关键字映射到同一个存储地址中。对于这些发生碰撞的不同关键字称为同义词。
    如何解决冲突:解决冲突的发生有两个方面,一方面,是设计好的散列函数尽量去减少冲突的发生;另一方面,由于冲突不可避免,可以设计好处理冲突的方法。

    理想状况下,散列表进行查找的时间复杂度为O(1),

    散列函数的构造方法

    在构造散列函数时,应该要注意以下几点:
    (1)散列函数的定义域(存放关键字的存储单元)必须包含全部存储的关键字,而值域的范围则依赖于散列表的大小或地址范围。
    (2)散列函数计算出来的地址是等概率的、均匀分布在整个地址空间中,减少冲突的发生。
    (3)散列函数尽量简单,能够在较短时间内计算出任一个关键字的散列地址。

    1. 直接定址法
    • 直接取关键字的某个线性函数值为散列地址,散列函数为:
      H(key) = key 或 H(key) = a *key + b,式中,a 和 b 是常数,计算方便,不会产生冲突。
      适用于:关键字的分布基本连续,例如:3,4,5,7,8;若关键字分布不连续,空位较多,会造成存储空间的浪费。
    2. 除留余数法
    • 除留余数法是一种最简单、最常用的方法,简单介绍除留余数法是如何使用的。
      假设散列表表长为 m,取一个不大于 m 但近视接近或等于 m 的质数 p,利用除留余数法的散列函数把关键字转换成散列地址。散列函数为:H(key) = key % p 。
      采用除留余数法关键是选好 p,这样就使得每个关键字通过该散列函数转换后等概率地映射到散列空间上地任一地址。
    3. 数字分析法
    • 数字分析法基本上不常用,不做介绍。不过数字分析法这种方法适合于已知的关键字集合,若换了关键字,则需要重新构造新的散列函数。
    4. 平方取中法
    • 顾名思义,这种方法取关键字的平方值的中间几位作为散列地址。该方法不太适用,且操作相对较麻烦,不过多介绍。

    处理冲突的方法

    • 冲突,顾名思义,就是不同的关键字经过 hash 函数的计算可能得到同一个 hash 地址,即 key1 不等于 key2 时,H(key1 ) = H(key2 ),出现的这种现象便叫做冲突。
    • 解决冲突,即在关键字发生冲突时,为产生冲突的关键字寻找下一个“空”的 Hash 地址。利用探测方法进行探测,若探测到的 Hash 地址仍然产生冲突,就继续探测下一个地址,直到为该关键字找到不产生冲突的存储地址即可。
    • 处理冲突的方法有两种:开放定址法和拉链法(链接法)。
    1. 开放定址法

    所谓开放定址法,是指可存放新表项的空间地址既向它的同义词开放,又向它的非同义词表项开放。其数学递推公式为:Hi = (H(key)+di)%m,式中,H(key) 为散列函数;i = 0,1,2,…,k(k<=m-1);m 表示 散列表表长;di为增量序列。通常有以下 4 种取法,分别是线性探测法、平方探测法、再散列法和伪随机序列法,简单介绍线性探测法和平方探测法。

    • 线性探测法
      当 di =0,1,2,…,m-1时,称为线性探测法。
      特点:冲突发生时,顺序查看表中下一个单元(探测到表尾地址 m-1时,下一个探测地址是表首地址 0),直到找到一个空闲单元(当表未满时一定能找到一个空闲单元)或查遍全表。
      缺点:线性探测法可能使第 i 个散列地址的同义词存入第 i+1 个散列地址,将本该存入第 i+1 个散列地址的元素就争夺第 i+2 个散列地址的元素地址,以此类推,从而照成大量元素在相邻的散列地址上“聚集”(或堆积)起来,大大降低了查找效率。

    • 平方探测法(简单了解即可)
      当 di = 0^2, 1^2, -1^2 , 2^2, -2^2,… ,k^2, -k^2 时,称为平方探测法,其中 k<=m/2,散列表长度 m 必须是一个可以表示成 4K+3 的素数,又称二次探测法。
      特点:平方探测法是一种较好的处理冲突的方法,可以避免出现“堆积”问题。
      缺点:不能探测到散列表上的所有单元,只能探测到散列表上的一半单元。

    2. 拉链法(链接法)
    • 对于不同的关键字可能会通过散列函数映射到同一地址,为避免非同义词发生冲突,把所有的同义词存在在一个线性链表中,线性链表由其散列地址唯一标识。
    • 适用:拉链法常用于进行插入和删除的情况。
    • 关键字序列为 {15,16,29,37,48,12,25,56,67,47,22,34},应用拉链法处理冲突的散列表如下图所示。
      在这里插入图片描述
    3. 拉链法查找和性能分析
    • 在利用拉链法进行查找和处理冲突时,通常也会去计算它的性能情况,下面分别计算利用拉链法处理冲突时各个关键字的查找成功的平均查找长度和查找不成功的平均查找长度。
    • 通过上述利用拉链法来进行冲突处理的图片可知,利用拉链法查找关键字的查找成功的平均查找长度如下:
      ASL(成功)=( 1 * 9 + 2 * 3 ) / 12 = 15 / 12 = 1.25
    • 简单理解方法:可以把拉链表看成是由 12 个具有头结点的单链表组成。1 * 9(从头结点下的第一个结点开始计数)可看成是头结点的下一个元素是否为空,刚好头结点下的结点不为空的有 9 个。 同理,便可知道 2*3从何而来。除以12中12表示有12个关键字,根据关键字个数来判断除以多少。
    • 计算出查找不成功的平均查找长度如下:
      ASL(不成功) = (3+3+1+2+2+2+1+2+2+1+3+2)/12=24/12=2
      其中第一个3表示第一个链表中查找3次都没有查到指定的关键字,第2个3同理,依次查找,可以得出上述公式,除以12表示具有12个单链表存储单元。
    4. 线性探测法处理冲突
    • 关键字序列{19,14,23,01,68,20,84,27,55,11,10,79}按散函数 H(key)=key%13 和线性探测法处理冲突所构造的散列表如下图所示。
      在这里插入图片描述

    • 该散列表是通过散列函数和线性探测处理冲突的方法得来的。例如19,通过19对13的余数可得余数为6,则19的散列地址为6,存入散列表中地址为6的存储单元;14除以13余数为1,存入1号地址;23除以13余数为10,存入 10号地址;01除以13余数为1,本来需要存入1号地址,但1号地址已经存入14,故向后探测一个存储单元,可见2号存储单元为空,存入其中即,同理可得上述散列表。

    • 利用哈希函数和线性探测法处理冲突后,查找关键字得查找成功得平均查找长度和查找不成功得平均查找长度如下:

    • 查找成功的平均查找长度
      查找各关键字的比较次数如下图所示。
      在这里插入图片描述

    • 查找成功平均查找长度:
      ASL(成功)=(1 * 6 +2 * 1+3 * 3+4+9)/12 =30/12=2.5
      查找失败平均查找长度:
      ASL=(13+12+11+10+9+8+7+6+5+4+3+2)/13=90/13

    注意:对同一组关键字,设定相同的散列函数,不同的处理方法得到的散列表不同,它的平均查找长度也不同。散列表在查找过程中的时间复杂度为 O(1),平均查找长度 ASL=1,实际上由于冲突的存在,其ASL的值会比1大。

    • 散列表查找效率影响因素:散列函数、处理冲突的方法和装填因子(表的装满程度)
      散列表装填因子记为 a,定义一个表的装满程度,即:
      a = (表中记录数n)/ (散列表长度 m),散列表的平均查找长度依赖于散列表的装填因子a,而不直接依赖于 n 或 m 。直观地看,表示装填地记录越“满”,发生冲突的可能性越大,反之发生冲突的可能性越小。

    更多知识可点击博主主页进行查看,在这里你会学到很多知识,希望大家的支持和关注。

    展开全文
  • 当emule中开始使用Kademlia网络后,便不再会有中心服务器失效这样的问题了,因为在这个网络中,没有中心服务器,或者说,所有的用户都是服务器,所有的用户也是客户端,从而完完全全得实现了P2P。...
  • c++,散列表的实现

    2012-04-18 12:32:42
    散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组...
  • 图解散列表

    千次阅读 2019-05-07 14:43:40
    理想的散列表数据结构只是包含一些项(item)的具有固定大小的数组。而查找是对项的某个部分进行的, 这部分就叫关键字(key)。注意,关键字未必是可比较的。 散列表的大小记为TableSize,它也是散列数据结构的一部分...

    引言

    散列是一种用于以常数平均时间执行插入、删除和查找的技术。Java中HashMap就是基于它实现的。

    原理

    理想的散列表数据结构只是包含一些项(item)的具有固定大小的数组。而查找是对项的某个部分进行的,
    这部分就叫关键字(key)。注意,关键字未必是可比较的。

    散列表的大小记为TableSize,它也是散列数据结构的一部分。每个关键字(通过其hashcode/散列码/hash值)都被映射到从0到TableSize - 1这个范围
    中的某个数。通常该数就是数组的索引,可以将项放到索引对应的位置。从关键字被映射到某个数叫作定址
    这个映射叫作散列函数

    在这里插入图片描述

    如上图所示,假定hash值最大为M,散列表大小为Size,M通常是远远大于Size的。
    首先需要计算出key的hash值(一般通过key.hashCode()),然后经由散列函数进行定址。假设定到了entry这个地址,其指向的节点就是item。

    这就是散列的基本思想,但是还有一个问题是,很有可能两个不同的key,经过hash操作都定位到同一个地址。这种情况叫做冲突
    因此,选择什么样的散列函数能尽可能的降低冲突,使表中的元素更分散以及如何解决冲突是我们接下来要探讨的问题。

    散列函数

    什么样的散列函数更好? 需要根据散列函数的评价标准:

    • 确定:同一关键码总是映射到同一地址
    • 快速:计算必须要快速
    • 满射:尽可能充分的覆盖整个散列空间(使表中的元素更分散)
    • 均匀:关键码映射到散列表各位置的概率尽可能接近,要有效避免聚集现象(即都靠近某个位置,而其他位置少有问津)

    取余法

    常用的方法就是取余法:hash(key) = key % M

    这种适用于关键字是整数,但表的大小需要仔细考虑。一般大小设置为素数,这样能尽可能保证分配均匀。

    MAD法

    取余法的缺点有:

    • 不动点: 无论表长M取值如何,总有 h a s h ( 0 ) ≡ 0 hash(0) \equiv 0 hash(0)0
    • 零阶均匀:相邻关键码的散列地址也比相邻

    针对上述问题对取余法作出改进,引入MAD法(Multiply Add Divide)法:hash(key) = ( a * key + b) % M

    M为素数,a > 0,b > 0,a % M <> 0

    a相当于步长,b相当于偏移量。

    多项式法

    对于非数值key,首先就需要得到其hashcode。比如计算序列 s = A 0 A 1 . . . A n − 1 s=A_0A_1...A_{n-1} s=A0A1...An1(如Java中的String)的hash值。

    考虑下述算法(Horner法则)。计算 F ( X ) = ∑ i = 0 N A i X i F(X) = \sum_{i=0}^NA_iX^i F(X)=i=0NAiXi的值。

    通过以下代码将公式中较为复杂的乘法计算,转换为简单的乘法与加法,其伪代码如下:

    value = 0
    for(i = N;i >= 0; i--)
    	value = X * value + A[i]
    

    其中,X取素数

    我们就可以得到一个散列函数:

    public static int hash(String key,int tableSize) {
    	int hashVal = 0;
    	for( int i = 0;i < key.length(); i++ ) {
    		hashVal = 37 * hashVal + key.charAt(i);
    	}
    	hashVal %= tableSize;
    	if(hashVal < 0 ) {
    		hashVal += tableSize;
    	}
    	return hashVal;
    }
    

    该函数中X取37。

    如果看过String的源码,可以看到类似的代码(经过了小小的修改)

    public int hashCode() {
        int hash = 0;
        
        char val[] = value;//valule是 char数组(char[] value)
    
        for (int i = 0; i < value.length; i++) {
            hash  = 31 * hash  + val[i];
        }
        return hash;
    }
    

    冲突消除

    剩下的主要问题就是冲突消除,如果某个元素被插入时与一个已经插入的元素散列到相同的值,
    那么就产生一个冲突,这个冲突需要消除。常见的解决冲突的方法有:分离链接法开放定址法

    分离链接法

    将散列到同一个值的所有元素保留在一个链表中。
    我们通过一个示例来说明,为了简单我们散列函数采用取余法,并将表大小设为10。
    假定我们待插入的序列为:0,1,4,5,9,16,36,49

    散列函数为:hash(x) = x mod 10

    在这里插入图片描述

    初始散列表如上图所示,然后我们依次插入

    • 0 % 10 = 0;放到0位置
    • 1 % 10 = 1;放到1位置
    • 4 % 10 = 4;放到4位置
    • 5 % 10 = 5;放到5位置
    • 9 % 10 = 9;放到9位置
    • 16 % 10 = 6;放到6位置

    在这里插入图片描述
    接下来36 % 10 = 6,放到6位置,但是6位置上已经有元素16了,根据分离链接法思想,采用头插法将36插入到6位置的链表中如下:

    在这里插入图片描述49也冲突了,采取同样的方式来处理

    下面贴出该实现的代码:

    package com.algorithms.hash;
    
    import java.util.LinkedList;
    import java.util.List;
    
    /**
     * 分离链接法解决冲突的HashTable
     * <p>
     * 装填因子: 表中元素个数/表大小 ;在该实现类中为1
     *
     * @author yjw
     * @date 2019/5/6/006
     */
    @SuppressWarnings("unchecked")
    public class SeperateChainingHashTable<E> implements HashTable<E> {
        private List<E>[] lists;
    
        /**
         * 表中的元素数
         */
        private int size = 0;
    
        public SeperateChainingHashTable() {
            this(DEFAULT_TABLE_SIZE);
        }
    
        public SeperateChainingHashTable(int size) {
            //为了好计算hash值取素数
            lists = (List<E>[]) new LinkedList[nextPrime(size)];
            //初始化
            for (int i = 0; i < lists.length; i++) {
                lists[i] = new LinkedList<>();
            }
        }
    
        /**
         * 计算元素的hash值
         * @param x
         * @return
         */
        private int hash(E x) {
            int hashCode = x.hashCode();//得到对象的hash值
            hashCode %= lists.length;//通过取余法得到表中的位置
            if (hashCode < 0) {
                hashCode += lists.length;
            }
            return hashCode;
        }
    
    
    
    
        @Override
        public void insert(E x) {
            List<E> list = getOpList(x);
            if (!list.contains(x)) {
                list.add(0,x);//头插法
                if (++size > lists.length) {
                    rehash();
                }
            }
        }
    
        /**
         * 扩容,并重新计算hash值,填充数据
         */
        private void rehash() {
            System.out.println("size:" + size + " ,lists.length:" + lists.length);//log
            List<E>[] oldLists = lists;//指向原来的链表数组
            lists = new LinkedList[nextPrime(2 * oldLists.length)];//扩容
            for (int i = 0; i < lists.length; i++) {//重新初始化
                lists[i] = new LinkedList<>();
            }
    
            size = 0; //size清零
    
            //重新计算hash也就是取出老表中的元素不停的插入到新表,利用insert方法去计算新的hash值
            for (int i = 0; i < oldLists.length; i++) {
                //遍历每个链表
                for(E item : oldLists[i]) {
                    //直接调用插入方法
                    insert(item);
                }
            }
        }
    
        @Override
        public boolean contains(E x) {
            List<E> list = getOpList(x);
            return list.contains(x);
        }
    
        @Override
        public void makeEmpty() {
            for (List<E> list : lists) {
                list.clear();
            }
            size = 0;
        }
    
        @Override
        public void remove(E x) {
            List<E> list = getOpList(x);
            if (list.remove(x)) {
                size--;
            }
        }
    
        @Override
        public void print() {
            if (size == 0) {
                System.out.println("[]");
            }
            int index = 0;
            for (List<E> list : lists) {
                int i = 0;
                System.out.printf("%3d:",index);
                if (list.size() == 0) {
                    System.out.print("[]");
                } else {
                    for (E e : list) {
                        System.out.print("[" + e + "]");
                        if (i != list.size() - 1) {
                            System.out.print("->");
                        }
                        i++;
                    }
                }
                System.out.println();
                index++;
            }
        }
    
        /**
         * 得到x的hash值对应的list
         * @param x
         * @return
         */
        private List<E> getOpList(E x) {
            return lists[hash(x)];
        }
    }
    
    

    HashTable接口的定义为:

    **
     * Hash表公共接口
     *
     * @author yjw
     * @date 2019/5/6/006
     */
    public interface HashTable<E> {
        int DEFAULT_TABLE_SIZE = 101;
    
        void insert(E x);
    
        boolean contains(E x);
    
        void makeEmpty();
    
        void remove(E x);
    
        void print();
    
        /**
         * 获取比n大的最小素数
         *
         * @param n
         * @return
         */
        default int nextPrime(int n) {
            if (n % 2 == 0) {
                n++;
            }
            while (!isPrime(n)) {
                n += 2;
            }
            return n;
        }
    
        /**
         * 判断n是否是素数
         * @param n
         * @return
         */
        default boolean isPrime(int n) {
            if (n == 2 || n == 3) {
                return true;
            }
            if (n == 1 || n % 2 == 0) {
                return false;
            }
    
            for (int i = 3; i * i <= n; i += 2) {
                if (n % i == 0) {
                    return false;
                }
    
            }
            return true;
        }
    
    }
    

    我们定义散列表的装填因子(load factor) λ \lambda λ为散列表中元素个数对该表大小的比值。
    在上面的例子中,装填因子为1。链表的平均长度为 λ \lambda λ

    执行一次查找所需要的工作是计算散列函数值所需的常数时间加上遍历链表所用的时间。
    在一次不成功的查找中,要考查的节点数平均为 λ \lambda λ。一次成功的查找则需要遍历大约 1 + λ 2 1 +\frac\lambda2 1+2λ个链。

    开方定址法

    当发生冲突时,使用某种探测技术在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,直到找出空单元为止。
    探测序列单元: h 0 ( x ) , h 1 ( x ) , h 2 ( x ) , . . . h_0(x),h_1(x),h_2(x),... h0(x),h1(x),h2(x),...的公式为: h i ( x ) = ( h a s h ( x ) + f ( i ) ) m o d &ThinSpace;&ThinSpace; T a b l e S i z e h_i(x)=(hash(x) + f(i)) \mod TableSize hi(x)=(hash(x)+f(i))modTableSize ,且 f ( 0 ) = 0 f(0)=0 f(0)=0, i = ( 0 , 1 , 2... ) i=(0,1,2...) i=(0,1,2...)
    函数 f f f是冲突解决方法,TableSize为表长。
    现在我们来学习常见的冲突解决方案。

    线性探测:

    线性探测中 f ( i ) = i f(i)=i f(i)=i,相当于相继探测逐个单元以查找出一个空单元。
    下图显示使用与前面相同的散列函数将各个元素{89,18,49,58,69}加入到散列表中的情况:

    索引空表插入89插入18插入49插入58插入69
    0494949
    15858
    269
    3
    4
    5
    6
    7
    818181818
    98989898989

    从左往右看,首先是空表,然后是插入89后…

    第一个冲突在插入49时产生;它被放入了下一个空闲地址,即0,该地址是开放的。
    关键字58先与18冲突,再与89冲突,然后又和49冲突,试选3次后才得到一个空单元。
    对于69的冲突采用类似的方法处理,只要表足够大,总能找到一个空单元。但是花费的
    时间是相当多的,而且即使表相对较空,占据的单元也会形成一些块区。会造成元素聚集现象。

    平方探测

    平方探测是消除线性探测中元素聚集问题的冲突解决方法。通常的选择是 f ( i ) = i 2 f(i)=i^2 f(i)=i2。下图显示与前面
    线性探测例子相同的输入使用该冲突函数所得到的的散列表。

    索引空表插入89插入18插入49插入58插入69
    0494949
    1
    25858
    369
    4
    5
    6
    7
    818181818
    98989898989

    当49与89冲突时,其下一个位置 h 1 ( x ) = 1 2 h_1(x) = 1^2 h1(x)=12 也就是下一个单元,该单元(循环到0的位置,可以想象成一个环)是空的,插入。
    此后,58在位置8处产生冲突,下一个位置89冲突,再下一个位置为 h 2 ( x ) = 2 2 h_2(x)=2^2 h2(x)=22为4, ( 8+4) % 10 = 2,是空的,插入。
    对于69处理的过程也一样。

    如果使用平方探测,且表的大小是素数,那么当表至少有一半是空的时候,总能插入一个新的元素。

    另外,在探测散列表中标准的删除操作不能执行,因为相应的单元可能已经引起过冲突,元素绕过它存在了别处。因此,探测散列表需要懒惰删除——其实类似逻辑删除。

    代码如下:

    package com.algorithms.hash;
    
    /**
     * 使用单向平方探测法来解决冲突
     *
     * @author yjw
     * @date 2019/5/7/007
     */
    @SuppressWarnings("unchecked")
    public class QuadraticProbingHashTable<E> implements HashTable<E> {
        private HashEntry<E>[] items;
    
        /**
         * 集合中元素数量
         */
        private int size;
    
        public QuadraticProbingHashTable() {
            this(DEFAULT_TABLE_SIZE);
        }
    
        public QuadraticProbingHashTable(int size) {
            allocate(size);
            makeEmpty();
        }
    
        /**
         * 为items分配空间
         *
         * @param n
         */
        private void allocate(int n) {
            size = 0;
            items = new HashEntry[nextPrime(n)];
        }
    
        public E find(E e) {
            int pos = findPos(e);
            return isActive(pos) ? items[pos].data : null;
        }
    
    
        @Override
        public void insert(E x) {
            int pos = findPos(x);
            if (!isActive(pos)) {
                items[pos] = new HashEntry<>(x);
                size++;
                if (size > items.length / 2) { //装填因子为0.5
                    rehash();
                }
            }
        }
    
        private void rehash() {
            HashEntry<E>[] oldItems = items;
            allocate(2 * oldItems.length);
            for (int i = 0; i < oldItems.length; i++) {
                if (oldItems[i] != null && oldItems[i].active) {
                    insert(oldItems[i].data);
                }
            }
        }
    
        @Override
        public boolean contains(E x) {
            int pos = findPos(x);
            return isActive(pos);
        }
    
        @Override
        public void makeEmpty() {
            size = 0;
            for (int i = 0; i < items.length; i++) {
                items[i] = null;
            }
        }
    
        @Override
        public void remove(E x) {
            int pos = findPos(x);
            if (isActive(pos)) {
                items[pos].active = false;
                --size;
            }
        }
    
        /**
         * 返回pos处的元素是否有效
         *
         * @param pos
         * @return
         */
        private boolean isActive(int pos) {
            return items[pos] != null && items[pos].active;
        }
    
        private int findPos(E x) {
            int offset = 1;
            int pos = hash(x);
    
            while (items[pos] != null && !items[pos].data.equals(x)) {
                /**
                 * 进行平方探测的快速方法
                 */
                pos += offset;
                offset += 2;
                pos = pos % items.length;
            }
            return pos;
        }
    
        private int hash(E x) {
            int hashCode = x.hashCode();
            hashCode %= items.length;
            if (hashCode < 0) {
                hashCode += items.length;
            }
            return hashCode;
        }
    
        @Override
        public void print() {
            if (size == 0) {
                System.out.println("[]");
                return;
            }
            for (int i = 0; i < items.length; i++) {
                System.out.printf("%3d",i);
                System.out.print(":[");
                if (items[i] != null && items[i].active) {
                    System.out.print(items[i]);
                }
                System.out.println("]");
            }
        }
    
        /**
         * 带有删除标记的节点
         *
         * @param <E>
         */
        private static class HashEntry<E> {
            E data;
            /**
             * 是否激活,false表示已被删除
             */
            boolean active;
    
            public HashEntry(E e) {
                this(e, true);
            }
    
            public HashEntry(E e, boolean i) {
                data = e;
                active = i;
            }
    
            @Override
            public String toString() {
                return data + "";
            }
        }
    }
    
    

    另外附上测试代码:

    @Test
    public void testHashTable() {
        HashTable<String> hashTable = new SeperateChainingHashTable<>(10);
        String[] array = ("Norah Morahan O’Donnell is an American print and television journalist, currently " +
                "serving as the co-anchor of CBS This Morning. She is the former Chief White House Correspondent" +
                " for CBS News and the substitute host for CBS's Sunday morning show Face the Nation").split("\\W+");
        for (String s : array) {
            hashTable.insert(s);
        }
        System.out.println(hashTable.contains("host"));
    
        hashTable.print();
    }
    

    分离链接法的输出如下:

    true
      0:[]
      1:[and]
      2:[]
      3:[]
      4:[show]
      5:[]
      6:[]
      7:[]
      8:[Sunday]
      9:[]
     10:[for]->[anchor]
     11:[CBS]
     12:[]
     13:[]
     14:[serving]
     15:[House]->[an]
     16:[]
     17:[Face]->[former]
     18:[of]
     19:[White]
     20:[host]->[as]
     21:[s]->[News]
     22:[Norah]
     23:[Nation]->[journalist]
     24:[]
     25:[]
     26:[]
     27:[the]
     28:[]
     29:[Correspondent]
     30:[]
     31:[This]->[television]->[co]
     32:[O]
     33:[is]
     34:[Donnell]
     35:[]
     36:[currently]
     37:[]
     38:[]
     39:[substitute]->[She]
     40:[morning]->[print]->[Morning]
     41:[Chief]->[American]
     42:[]
     43:[Morahan]
     44:[]
     45:[]
     46:[]
    
    展开全文
  • 对于两个C程序,设计并实现两种不同的基于散列表的检测算法,计算两个程序的相近度,并分析比较两种算法的效率。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 80,306
精华内容 32,122
关键字:

散列表