首页 > 编程知识 正文

数据结构哈希表设计,设计散列函数

时间:2023-05-04 18:08:51 阅读:154792 作者:3529

文章目录1、yldmz表2是什么,yldmz函数3、yldmz冲突的原因及解决方法1、数组扩展2、优秀的yldmz函数3、开放地址法4、链表法4、总结

一.什么是yldmz表

yldmz表是数组yldmz函数,以数组可以根据下标的索引随机访问数据的特性为中心。

举个例子吧。 一个班有50人,每个人都有学号。 按自然数顺序编号。 学号1是zxdxf,学号2是儒教的虾,学号3是tmdyet等等。 在这板栗中类是数组容器,学校号是数组下标,学生是数组中的元素,通过学校号找人是利用数组下标随机访问元素的特性。 如何给学生编号是yldmz函数的事。

第二,yldmz函数yldmz函数正如其名字所示,是函数。 式: hash(key )、key是与数组下标无关的关键字。 yldmz函数计算的值与数组下标连接,可以直接作为数组下标,也可以通过模拟yldmz值等运算得到数组下标。

刚才学号的栗子是hash (自然数)=自然数。 计算得到的yldmz值可以直接作为数组下标使用,但随着yldmz表的实用化,计算出的yldmz值容易变大,如果直接将其作为数组下标,数组的长度会变长,会浪费内存。 因此,在有限的数组上用yldmz函数映射下标的话,会导致yldmz的冲突。

三. yldmz碰撞的原因和解决方法产生yldmz碰撞的原因不仅是序列的有界,还包括yldmz函数的计算和yldmz值的映射。

解决yldmz冲突的方法也是基于这三个原因进行对症治疗。

有界适当扩张。 设计优秀的yldmz算法。 开放地址法和链表法为yldmz值找到合适的映射。 1、序列扩增序列中空位越多,在一定程度上yldmz碰撞也越小。 但是,为此并不是将排列的长度设定得很长,而是设定合适的初始长度,之后逐渐扩展。

什么时候扩大容量? 扩张量是多少? 正在被研究。 如果容量扩展过少,频繁的容量扩展会影响性能,而容量扩展过多会浪费内存。 根据一般经验,在要素的数量占数组长度的3/4时进行扩展,扩展的长度为原来的2倍。 这也是java中HashMap的扩展思想,但需要根据实际情况进行调整。

扩展中什么时候有名词?装载因子表示元素数相对于数组长度的比例。 加载因子=元素数/数组长度。 加载因子的设置必须考虑时间和空间的复杂性,加载因子太大,yldmz碰撞越严重,加载因子太小,内存浪费越严重。

在存储器容量不紧张、对执行效率要求较高的情况下,可以降低加载因子的阈值; 相反,存储器空间紧张,对执行效率要求不高的情况下,可以加大输入因子的大小,也可以大于1 (链表法的冲突解决)。

对于简单的数组扩展,数据迁移很简单,只需要根据位置复制,但是yldmz表的扩展迁移很复杂,yldmz表的长度改变,元素的位置也改变,需要一个个重新计算yldmz图的新位置。 扩展的时间复杂度为o(n ),单纯插入一个数据的时间复杂度为o )1),正好遇到扩展时时间复杂度为o ) n )。

由于容量扩展在一定程度上会影响插入数据的性能,因此要避免无效的容量扩展,不仅可以设计合理的安装系数和容量扩展比率,还可以从容量扩展过程中进行优化。

)扩展行为分布在每个插入操作中,新数据插入新数组中,插入数据的同时旧数组的元素被复制到新数组中。 因此,每个插入操作的时间复杂度为o )1),但需要互换地维持新旧排列,检索和删除操作首先在旧排列中进行,不在新排列中进行。 (jdk1.8 ConcurrentHashMap多线程扩展思想)

)2)对于用链表法解决冲突构成的yldmz表,在转移时可以以链表为单位进行复制,所有的要素不需要重新计算yldmz值。 (ConcurrentHashMap的基于扩展链表的整体迁移复制)

2、优秀的yldmz函数和不合理的yldmz函数,数组扩展失败。 如果计算的yldmz值本身容易冲突,或者映射到数组下标的不均匀分布,则有更多的空闲位置也没有用。 因此,优秀的yldmz函数需要以下两个要素。

通过尽可能随机且均匀地分布yldmz值,不仅可以使yldmz碰撞最小化,而且即使发生碰撞也可以在各位置进行平均化,有助于改善碰撞的解决(开放地址法和链表法)。 yldmz算法计算性能高,不影响yldmz表的正常操作。 数组的扩展和优秀的yldmz函数仍然无法避免yldmz的冲突,也可以从yldmz值映射着手。 常用的方法是开放地址法和链表法。

3、开放地址法开放地址法是指发生yldmz冲突时,重新发现空闲位置,插入元素。 寻址方式有很多种,常用的是线性寻址、平方寻址、双重yldmz寻址。

如果需要插入元素的位置被占用,则线性寻址按顺序向后寻址。 如果直到数组末尾都找不到可用位置,请从数组的开头插入数据,直到找到可用位置。 线性寻址的各个寻址步骤是1,寻址式hash(key ) n (n是寻址的次数)。二次方寻址是线性地址总步骤的平方,Hash(key ) n ) 2。双重yldmz寻址顾名思义,它会多次重复yldmz,直到找到不碰撞的yldmz值


采用开放寻址法解决yldmz冲突,又该如何查找元素和删除元素呢?

查找元素的过程和插入元素类似,用相同的寻址方式,寻址的同时比对key或者value是否相等,相等则认为元素存在,不相等则继续寻址,如果探测到空闲位置依然没有找到则认为该元素不存在

删除有些特别,不能单纯的把要删除的元素设置为空,因为在查找元素的过程中探测到的空闲位置是删除元素的位置,就会使得查找元素的寻址算法失效,本来存在的元素误判定为不存在。该如何解决这个问题呢?

只需要删除元素不是物理删除而是逻辑删除。给删除的元素做上delete标记,当查询元素寻址时遇到delete标记的位置时不会停下来而是继续向后探测,但是在插入元素寻址遇到delete标记的位置就会把应该删除的元素替换掉。

三种寻址方式都有着明显的不足:

线性寻址,寻址的性能虽然元素个数的增多逐步下降,最坏时间复杂度是O(n)。二次方寻址,寻址的次数比线性寻址较低了,但是会因为步长是二次方,所以需要较长的数组长度,内存利用率可能较低。双重yldmz寻址,多次yldmz可能会浪费时间,需要优质的yldmz函数做支撑。

而整个开放寻址法的不足也很明显:

插入、查找、删除都需要寻址。数组中元素越多,空闲位置越少,yldmz冲突越剧烈。所以装载因子不能太大,要及时扩容减小冲突,但是数组内存利用率较低。

看似开放寻址法有挺多问题,但是也有一些优点:

数据都存储在数组中,可以有效地利用 CPU 缓存加快查询速度。而且,这种方法实现的yldmz表,序列化也简单,不像链表还要考虑指针。

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

4、链表法

链表法相对于开放寻址法实现起来简单一些,在数组内存利用率上比开放寻址法高,同时对装载因子的忍耐度也相对较高。开放寻址法的装载因子只能小于1,越趋近于1,冲突越剧烈,寻址过程越耗时,而链表法的装载因子可以大于1(但是内存不紧张,在意性能的一般不会装载因子不会大于1)。

链表法就是将产生yldmz冲突的元素链接成一个链表,每个链表可以设想成一个桶(bucket)或者槽(slot):

插入元素就是通过yldmz找到对应的桶,然后插入到链表中,时间复杂度为O(1);查找元素也是通过yldmz找到对应的桶,然后遍历链表;删除元素同样通过yldmz找到对应的桶,遍历链表找到需要删除的元素删除。

当yldmz比较均匀时,理论上查询和删除的时间复杂度为O(n/m),n是数组中元素的个数,m是数组中桶的个数。但是当yldmz冲突非常严重时,数据都集中在一个桶里,数组退化成链表,查找和删除的时间复杂度为趋近与O(n)。

针对数组退化成链表或者链表过长导致的性能下降,可以在合适的时机将链表转换为红黑树,极端情况下数组退化成一个红黑树,时间复杂度也是O(logn),比O(n)强多了。(jdk8中ConcurrentHashMap对于jdk7有所优化,当链表节点的个数大于8个且数组的长度大于64时,链表转换为红黑树;当红黑树的节点小于8个时又退化为链表。)

可以容忍的缺点:

因为链表节点需要存放指针,所以内存占用上比开放寻址法高。链表中的节点在内存中是不连续分布的,所以对CPU缓存的利用率也不高,序列化也比开放寻址法复杂。

优点:

内存利用率较高。优化策略灵活,红黑树和链表可以互相转换。 四、总结

yldmz表的两个核心yldmz函数的设计与yldmz冲突的解决。

yldmz表就是数组+yldmz函数,其核心思想是利用数组可以按照下标索引随机访问数据的特性。yldmz冲突的原因:数组的有界,yldmz函数的计算,yldmz值的映射。解决yldmz冲突的方法:数组扩容,设计优秀的yldmz函数,开放寻址法和链表法为yldmz值找到合适的映射。开放寻址法,插入、查找、删除都需要相同的寻址逻辑,所以时间复杂度一样。数组中元素越多,空闲位置越少,yldmz冲突越剧烈。链表法需要注意,当yldmz冲突非常严重时,数组会退化成链表,查找和删除的时间复杂度趋近于O(n),可以采用红黑树进行优化。

参考:极客时间专栏《数据结构与算法之美》。

PS: 如若文章中有错误理解,欢迎批评指正,同时非常期待你的评论、点赞和收藏。我是典雅的曲奇,愿与你共同进步!

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。