博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深入理解 hash 结构的另一种形式 —— 开放地址法
阅读量:2441 次
发布时间:2019-05-10

本文共 1889 字,大约阅读时间需要 6 分钟。

本文我们来探讨一个数据结构的基础话题:hash 结构

HashMap 无 Java 人不知无 Java 人不晓,它使用开链法处理 hash 碰撞,将碰撞的元素用链表串起来挂在第一维数组上。但是并不是所有语言的字典都使用开链法搞定的,比如 Python,它使用的是另一种形式 —— 开放地址法。相比 HashMap 是二维的结构,它只是一维的,只有一个数组。

开放地址法与开链法的不同之处在于如何处理 hash 冲突。当新来一个元素哈希到数组中的位置已经被其它元素占据了该怎么办?

开放地址法会根据当前的位置计算出下一个位置,将这个冲突的元素挪进来。如果这下一个位置也被占用了,那么就再计算下一个位置,直到找到一个空的位置。可以想像,将会有一条虚拟的链条将这些相关的位置串起来。这个虚拟的链条就好比开链法里面的第二维链表。只不过链表有显示的指针字段,而虚拟链条没有,它的这个链条完全是通过数学函数计算出来的。

root = hash(key) % m   // 第一个位置,m 为数组的长度index_i = (root + p(key, i)) % m  // 链条中的第 i 个位置index_1 = (root + p(key, 1)) % mindex_2 = (root + p(key, 2)) % m ...

在查找的时候,如果第一个位置上保存的 key 不是目标 key,那就沿着探测路径继续寻找,直到找到或者遇到一个空位置为止。

到这里你可能会担心又没有可能探测过程会出现死循环,探来探去又回到原点了,或者是回到路径的中间。这是很有可能的,所以这里的探测函数不能随意选择,它必须保证探测序列不会出现循环,经过 m-1 次探测生成的探测序列必须正好是 1..m-1的全排列。

这样的探测函数有很多,其中最常见的一种是线性探测函数。该探测序列和输入 key 无关。最终的探测路径只和初始位置相关。

// m = 2^n,c 必须是一个奇数p(key, i) = c * iindex_i = root + c * i

public class HashTest {    public static void main(String[] args) {        int m = 1 << 16;        int c = 111111;        Set
 nums = new HashSet<>();        for (int i = 1; i < m; i++) {            int p = c * i % m;            if(nums.contains(p)) {                System.out.println("duplicated");                return;            }            nums.add(p);        }        System.out.println("no duplicate");    }}------------no duplicate

为了不让探测路径中断,删除有两种实现方案

在删除的位置置一个特殊的删除标记,查找时可以直接跳过继续沿着探测路径往后寻找。需要注意的是这个删除的位置在后续的新元素插入时会得到回收利用。插入元素时,遍历探测路径,遇到了第一个删除标记的位置,这时不能立即插入。因为有可能这个元素存在于探测路径的后半部。所以需要遍历到底如果发现确实路径里不存在这个元素,这时候要回过头来插入到第一个发现删除标记的位置。如果删除的位置过多,会影响查找和插入性能。

将探测路径的后半部元素全部删除,然后重新插入。如果路径较长,可能会影响插入性能。

到这里似乎就结束了,其实还有一点我们没有注意到。那就是在采用 p(key, i) = c * i 探测函数的前提下,如果多个不同的 key 哈希的第一个位置相同,那么它们将会共享同一条探测路径。因为探测路径完全由第一个位置来决定的,和 输入 key 无关。那么这些相关的 key 就会在一条探测路径上聚集,这可能会导致数据分布的结果不那么均匀。

如果我们使用一个不同的探测函数,使得它和输入 key 相关,那么就可以消灭这个聚集问题。我们可以将探测函数中的常量 c 换成一个 hash 函数,只要这个函数总是返回奇数就可以了,这样的 hash 函数还是非常容易编写的。

p(key, i) = h2(key) * i

640?wx_fmt=png

转载地址:http://avbqb.baihongyu.com/

你可能感兴趣的文章
redis中的事务_如何在Redis中运行事务
查看>>
ubuntu定时执行命令_命令行基础知识:定时命令执行
查看>>
如何在CentOS 8上安装MariaDB
查看>>
如何在CentOS 8上安装Node.js
查看>>
如何在Ubuntu 20.04上安装Git
查看>>
javascript深度图_在JavaScript中深度克隆对象(及其工作方式)
查看>>
centos ssh密钥_如何在CentOS 8上设置SSH密钥
查看>>
JavaScript中的初学者排序算法:冒泡,选择和插入排序
查看>>
debian 10 安装_如何在Debian 10上安装Webmin
查看>>
使用CentOS 8进行初始服务器设置
查看>>
ecmascript v3_节点v12中的新ECMAScript模块简介
查看>>
vue jest 测试_Vue.js中的Jest快照测试简介
查看>>
Electron.js简介-第2部分:Todo应用
查看>>
gatsby_Gatsby CLI快速参考
查看>>
vue中的突变方法在哪_了解GraphQL中的突变
查看>>
redis修改配置重启命令_如何从命令行更改Redis的配置
查看>>
盖茨比乔布斯_使用wrapRootElement挂钩在盖茨比进行状态管理
查看>>
盖茨比乔布斯_通过盖茨比使用Airtable
查看>>
mern技术栈好处?_如何开始使用MERN堆栈
查看>>
路由器接路由器_路由器之战:到达路由器vsReact路由器
查看>>