一、概述

以 Key-Value 的形式进行数据存取的映射(map)结构

简单理解:用最基本的向量(数组)作为底层物理存储结构,通过适当的散列函数在词条的关键码与向量单元的秩(下标)之间建立映射关系

更详细的定义:开辟物理地址连续的桶数组ht[],借助散列函数hash(),将词条关键码key映射为桶地址(数组下标),从而快速地确定待操作词条的物理位置。

1.1 散列结构优点

  • 可以实现O(1)时间的数据项查找(注:给定关键码,通过散列函数可直接计算出所在地址)
  • 能以节省空间的方式实现上述O(1)查找

1.2 部分概念

  1. 桶/桶单元(bucket):散列表的物理存储结构,在物理上连续排列的用于存放词条的单元。
  2. 桶数组(bucket array):用数组作为桶单元。
  3. 地址空间(address space):桶数组的合法秩区间,如容量为R 则地址空间为[0, R)。
  4. 散列函数(hash function):词条与桶地址之间的映射关系,即从关键码到桶数组地址空间的映射函数。
  5. 散列地址(hashing address):给定关键码所对应的桶的秩,即 若给定散列函数hash(),关键码key,则散列地址为hash(key)。
  6. 散列冲突(collision):关键码不同的词条被映射到统一散列地址的情况。
  7. 装填因子(load factor):散列表中非空桶数量与桶单元总数的比值。
  8. 完美散列(perfect hashing):时间与空间性能均最优的散列。即给定问题实例下,对于任意关键码,均可在O(1)时间查找确定,且每个桶恰好存放一个词条,无空余无重复。完美散列实际上并不常见。
  9. 词条的聚集(clustering):词条集中到散列表内少数若干桶中(或附近)的现象。

1.3 简单的散列表设计实例

需求

存储某校2500门固定电话号码及其相关信息,号码随机分布在 0000-0000~9999-9999之间,需在O(1)时间进行高效查找。

简单设计

直接使用[99999999]的数组,以电话号码直接作为秩,即 hash(key)=key

缺点

这样设计缺点很明显:在词典中需要保存的词条数远小于关键码所有可能的情况下,直接用hash(key)=key 作为秩,导致装填因子太小,即实际装填数远小于桶单元总数,从而造成大量空间的浪费。

因此,需要合理设计散列函数,能够只创建实际需要保存数量的桶,并将关键码空间压缩到散列地址空间

二、散列函数

2.1 设计原则

散列函数首先面对的问题就是 散列冲突(Collision),该问题必然存在,需尽可能避免,后续会提到相关解决方法。

其次便是以下一些设计原则:

  1. 确定性,不论所含数据项如何,词条E的散列地址hash(E.key)必须完全取决于E.key。
  2. 映射过程自身不能过于复杂,保证散列地址计算可快速完成
  3. 所有关键码经映射后应尽量覆盖整个桶地址空间,以充分利用,即尽量满射。
  4. (重要)关键码映射到各桶的概率应尽量接近于1/M,M为桶总数(若关键码本身均匀且独立随机分布,则这也是任意一对关键码相互冲突的概率)

总之随机性越强、规律性越弱的散列函数越好。(使得在M上分配均匀,降低冲突)

2.2 常见散列方法

1) 除余法(division method)

思路

最简单的散列办法,将散列表(桶)长度M取作素数,然后用关键码key与M取余作为散列地址。

取余使得散列地址分配概率既均匀,也不会>=M,正好符合散列地址空间

表达式

hash(key) = key % M

为什么M需要为素数?

实际应用中,存储的词条关键码往往具有某种周期性,如{1000,1015,1030...},若周期与M若具有公共素因子,则冲突概率急剧攀升。

一般地,若M与词条关键码间隔T(上例为5)之间的最大公约数越大,则发生冲突可能性也越大。【TODO:底层数学原理暂无法探究】

注:若关键码本身独立随机,则概率还是平均的,只是在周期存取的情况下才会出现该情况。

2) MAD法(multiply-add-divide method)

除余法存在的不足

除余法虽能一定程度保证词条均匀分布,但从关键码空间到散列地址空间依然残留有一定的连续性,如 相邻关键码对应散列地址也相邻。

因此便有mad法,若常数ab选取得当,可以很好地克服除余法的这种连续性。除余法也可以看作Mad法a=1和b=0的特例,只是两个常数并未发挥实质作用。

表达式

hash(key) = (a*key+b) % M, 其中M仍为素数,a>0,b>0,且a % M != 0

3) 数字分析法(selecting digits)

注:以下各方法为保证落在合法的散列地址空间上,最后通常还需对表长M取余。

思路

从关键码key特定进制的展开中抽取特定的若干位,构成整型地址。

表达式

例:选取key十进制展开中的奇数位

hash(123456789) = 13579

4) 平方取中法(mid-square)

思路

从关键码key的平方的十进制或二进制展开中取居中的若干位,构成一个整型地址。

表达式

例:取平方并用十进制展开中的居中3位作为散列地址

123^2 = 15129,hash(123) = 512

5) 折叠法(folding)

思路

将关键码的十进制或二进制展开分割成等宽的若干段,取其总和作为散列地址。

表达式

例:以十进制三个数位为分割单位

hash(123456789) = 123+456+789 = 1368

6) 异或法(xor)

思路

将关键码的二进制展开分割成等宽的若干段,经异或运算得到散列地址。

表达式

例:以二进制三个数位为分割单位

hash(411) = hash(110011011b) = 110^011^011 = 110b = 6

三、冲突及排解策略

3.1 冲突的普遍性

冲突必然的

因为用短位(散列地址空间)表示长位数据(关键码空间),肯定会出现冲突。比如 常见的 MD5 码,一共就128bit,但却要表示无限的数据的散列码,因此必然会出现不同数据具有相同MD5码的情况。

3.2 冲突排解策略

冲突排解策略分为以下两种类型:

  • 开放定址(open addressing) / 闭散列(closed hashing):散列地址空间对所有词条开放(即 桶单元允许装hash(key)不对应的词条);词条存储地址(散列地址)仅限于散列表所覆盖的范围之内。

    如:线性试探、查找链法等。

    注:因闭散列不得使用附加空间的原因,装填因子通常<=0.5
  • 封闭定址(closed addressing) / 开散列(open hashing):散列地址空间只对对应的词条开放;词条存储地址不局限于散列表范围之内。

    如:多槽位法、独立链法、公共溢出区等

1)多槽位法(multiple slots)

思路



每个桶本身再细分为若干槽位,用于存放彼此冲突的词条。每个桶槽位的词典结构为向量,因此整体物理存储结构类似于二维数组。

如:put操作,首先通过hash(key)定位到对应的桶单元,并在该桶内部槽位中进一步查找key,若没找到,则创建新词条插入到该桶的空闲槽位中。

缺点

  1. 绝大多数的槽位都处于空闲状态,造成空间浪费。若桶被细分为k个槽位,则装填因子将直接降低为原来的1/k.
  2. 很难实现确定应该细分为多少个槽位,才能保证够用。

2) 独立链法(separate chaining) / 拉链法

思路



与多槽位思想类似,但每个桶的子词典是使用链表实现,令彼此冲突的词条互相串接。

优点

能灵活动态地调整子词典的规模,有效地使用空间。

缺点

空间未必连续分布,会导致系统缓存失效。

3) 公共溢出区

原理



在原散列表之外另设一个词典结构$$D_{overflow}$$,插入词条一旦发生冲突,则转存到该词典中。$$D_{overflow}$$相当于存放冲突词条的公共缓冲池。

4) 线性试探法

原理



在插入词条时,若发生冲突,则转而试探桶单元ht[hash(key)+1],若ht[hash(key)+1]也被占用,则继续试探ht[hash(key)+2],如此不断...直到找到空桶。

第i次试探的散列地址:(hash(key) + i) mod M, i=1,2,3...

具体查找逻辑

查找链(probing chain):对于待查找的key,从hash(key)桶单元开始,直接空桶结束的顺序序列。

  1. 经hash(key)算得的当前桶单元,若关键码相等,则成功返回。
  2. 当前桶单元非空,但关键码不等,则转入下一桶单元继续试探。
  3. 当前桶为空,则返回查找失败。

注:相互冲突的关键码比属于同一查找链(即中途不包含空桶),但同一查找链的关键码未必相互冲突。多组各自冲突的关键码所对应的查找链,有可能相互交织和重叠。

优点

具体由良好的数据局部性,试探地桶单元在物理空间上依次连贯,系统缓存能发挥作用。

懒惰删除

定义:

从词典删除词条时,暂时并不实际将桶置空,而是额外维护一个删除标记Bitmap,标记该桶已删除。

为什么需要懒惰删除?

因为查找链中任何一环的缺失,都会导致后续词条的“丢失”,即无法找到已存在词条;同时因为开销问题,不可能每次删除操作都对查找链进行维护重建(在扩容时,才重建链)。

因此懒惰删除机制既能保证查找链的完整,也不需要太多开销。

加入懒惰删除后,操作逻辑的变化:
  1. 在删除等操作查询指定词条时,判断失败的条件变为:为空且不带懒惰删除标记。
  2. 在插入操作时,找空桶过程中,判断桶为空条件为:带有懒惰标记或当前桶为空。

5) 平方试探法

线性试探法的不足

线性试探法各查找链均由物理上连续的桶单元组成,会加剧关键码的聚集趋势。

定义



若发生冲突,则第j次试探地桶地址:(hash(key) + j^2) mod M, j=0,1,2...

该方式会使得试探地址加速逃离聚集区段。

该方式局部性会有所降低,但如今I/O页面规模较大,不必过于担心。

确保试探必然终止

只要散列表长度M为素数,且装填因子$$\lambda<=50%$$,则平方试探必然会终止于某个空桶。(数学证明暂未探究)

6) 再散列法(double hashing)

选取一个适宜的二级散列函数$$hash_2()$$,一旦发生冲突,则将再散列的结果作为偏移量,公式如下:

第 j 次试探地址:$$[hash(key)+j*hash_2(key)]%M$$

四、散列码转换 hahsCode()

4.1 定义

散列码(hash code):利用某一种散列码转换函数hashCode(),将关键码key统一转换为的一个整数。

4.2 为什么需要散列码

因为词条关键码不一定天然支持大小比较,而且也并不一定是整数类型,因此需制定一个函数能将任意类型的关键码先统一转成散列码,散列函数再由散列码计算散列地址。

4.3 散列码转换函数设计原则

  1. 作为中间桥梁的散列码,取值范围应覆盖系统所支持的最大整数范围
  2. 各关键码经hashCode()映射后的散列码之间,也应尽可能减少冲突。否则在该阶段的冲突,后续hash()必定无法消除。
  3. hashCode()应与判等器保持一致。即 判等器判断相等的对象,其散列码应该相等。

参考:

数据结构 邓俊辉

散列(Hash)表入门的更多相关文章

  1. java 散列与散列码探讨 ,简单HashMap实现散列映射表运行各种操作示列

    java 散列与散列码探讨 ,简单HashMap实现散列映射表运行各种操作示列 package org.rui.collection2.maps; /** * 散列与散列码 * 将土拔鼠对象与预报对象 ...

  2. StackExchange.Redis帮助类解决方案RedisRepository封装(散列Hash类型数据操作)

    本文版权归博客园和作者本人共同所有,转载和爬虫请注明本系列分享地址:http://www.cnblogs.com/tdws/p/5815735.html 上一篇文章的不合理之处,已经有所修改. 今天分 ...

  3. 散列(hash)

    散列(hash)是常用的算法思想之一,在很多程序中都会有意无意地使用到. 先来看一个简单的问题:给出N个正整数,再给出M个正整数,问这M个数中每个数分别是否在N个数中出现过. 例如N=5,M=3,N个 ...

  4. Redis散列(Hash)的相关命令

    散列 就像一个减配的Redis 内部及其类似Java的Map 内容就是key:value结构 hash类型在面向对象编程的运用中及其适合,因为它可以直接保存编程语言中的实体类关系 增 hset hse ...

  5. 非对称算法,散列(Hash)以及证书的那些事

    转载请注明出处 http://blog.csdn.net/pony_maggie/article/details/35389657 作者:小马 这几个概念在金融电子支付领域用得比較多,我忽然认为把它们 ...

  6. john快速破解各种散列hash

    0x01 john工具安装: kali系统自带的有,也可以自己安装在其他linux和windows系统上,关于安装步骤网上都有可自行百度. 0x02 john常用选项 具体的其他选项可通过john - ...

  7. 算法与数据结构(十二) 散列(哈希)表的创建与查找(Swift版)

    散列表又称为哈希表(Hash Table), 是为了方便查找而生的数据结构.关于散列的表的解释,我想引用维基百科上的解释,如下所示: 散列表(Hash table,也叫哈希表),是根据键(Key)而直 ...

  8. Django 用散列隐藏数据库中主键ID

    最近看到了一篇讲Django性能测试和优化的文章, 文中除了提到了很多有用的优化方法, 演示程序的数据库模型写法我觉得也很值得参考, 在这单独记录下. 原文的演示代码有些问题, 我改进了下, 这里可以 ...

  9. [No0000132]正确使用密码加盐散列[译]

    如果你是一个 web 开发工程师,可能你已经建立了一个用户账户系统.一个用户账户系统最重要的部分是如何保护密码.用户账户数据库经常被黑,如果你的网站曾经被攻击过,你绝对必须做点什么来保护你的用户的密码 ...

随机推荐

  1. [Python_4] Python 面向对象(OOP)

    0. 说明 Python 面向对象(OOP) 笔记.迭代磁盘文件.析构函数.内置方法.多重继承.异常处理 参考 Python面向对象 1. 面向对象 # -*-coding:utf-8-*- &quo ...

  2. 进程控制编程——Linux编程

    1.进程的创建 编写一段程序,使用系统调用fork( )创建两个子进程,在系统中有一个父进程和两个子进程活动.让每个进程在屏幕上显示一个字符:父进程显示字符“a”,子进程分别显示字符“b” 和“c”. ...

  3. 乘风破浪:LeetCode真题_035_Search Insert Position

    乘风破浪:LeetCode真题_035_Search Insert Position 一.前言 这次的问题比较简单,也没有限制时间复杂度,但是要注意一些细节上的问题. 二.Search Insert ...

  4. win7 win10双系统开机系统引导

    以win7启动管理器引导作为启动引导 安装一个easybcd 然后里面添加引导选项(添加新条目---->编辑引导菜单(选择倒计时30秒)把use metro bootloader勾去掉就是默认的 ...

  5. 用JS制作《飞机大作战》游戏_第2讲(四大界面之间的跳转与玩家飞机的移动)-陈远波

    一.通过点击按钮事件,实现四大界面之间的跳转: (一)跳转的思路: 1.打开软件,只显示登录界面(隐藏游戏界面.暂停界面.玩家死亡界面) 2.点击微信登录(QQ登录)跳转到游戏界面,隐藏登录界面 3. ...

  6. 题解 P1378 【油滴扩展】

    题面 在一个长方形框子里,最多有N(0≤N≤6)个相异的点,在其中任何一个点上放一个很小的油滴,那么这个油滴会一直扩展,直到接触到其他油滴或者框子的边界.必须等一个油滴扩展完毕才能放置下一个油滴.那么 ...

  7. 详解Web请求中的DNS域名解析

    当我们打开浏览器,输入一个URL去请求我们需要的资源,但是URL是需要解析成对应的IP地址才能与远程主机建立连接,如何将URL解析成IP就是DNS的工作范畴,即使作为开发人员,这个过程我们也感觉不到, ...

  8. 禁止选择DIV内的文本(css,js写法)

    css:<span style="font-family:SimSun;font-size:18px;">/* 禁止选择div内的文字 */ #hall_body { ...

  9. Tensorflow Object Detection API 安装

    git:https://github.com/tensorflow/models/tree/master/object_detection 中文文档:http://wiki.jikexueyuan.c ...

  10. 随手练——小米OJ 高弗雷勋爵

    高弗雷勋爵 题目链接:https://code.mi.com/problem/list/view?id=113 这个解法比较暴力,主要需要注意的是一颗子弹 弹死两个及以上的情况. #include & ...