数据结构和算法(12):词典

news2024/11/19 12:34:34

词典

逻辑上的词典,是由一组数据构成的集合,其中各元素都是由关键码和数据项合成的词条(entry)。
映射(map)结构与词典结构一样,也是词条的集合。
二者的差别仅仅在于,映射要求不同词条的关键码互异,而词典则允许多个词条拥有相同的关键码。
除了静态查找,映射和词典都支持动态更新,二者统称作符号表

在这里插入图片描述

散列

散列(Hashing) 是一种将任意大小的输入数据映射为固定大小的输出数据的过程。这个输出数据通常称为散列值或哈希值。
散列表(hashtable) 是散列方法的底层基础,逻辑上由一系列可存放词条(或其引用)的单元组成,故这些单元也称作桶(bucket)或桶单元;与之对应地,各桶单元也应按其逻辑次序在物理上连续排列。

往往直接使用数组来实现这种线性的底层结构,此时的散列表亦称作桶数组(bucket array)。若桶数组的容量为 R,则其中合法秩的区间 [0, R) 也称作地址空间(address space)。

散列函数

一组词条在散列表内部的具体分布,取决于所谓的散列(hashing)方案——事先在词条与桶地址之间约定的某种映射关系,可描述为从关键码空间到桶数组地址空间的函数: h a s h ( ) : k e y → h a s h ( k e y ) hash() : key \to hash(key) hash():keyhash(key).
这里的 hash() 称作散列函数(hash function)。反过来,hash(key)也称作key散列地址(hashing address),亦即与关键码key相对应的桶在散列表中的秩。

在这里插入图片描述
假定关键码均为[0, R)范围内的整数。将词典中的词条数记作N,散列表长度记作M,于是通常有:R >> M > N
散列函数hash()的作用可理解为,将关键码空间[0, R)压缩为散列地址空间[0, M)

设计准则

确定性: 无论所含的数据项如何,词条E在散列表中的映射地址hash(E.key)必须完全取决于其关键码E.key
简单性: 映射过程自身不能过于复杂,唯此方能保证散列地址的计算可快速完成,从而保证查询或修改操作整体的O(1)期望执行时间。
覆盖性: 所有关键码经映射后应尽量覆盖整个地址空间[0, M),唯此方可充分利用有限的散列表空间。也就是说,函数hash()最好是满射。

散列冲突: 关键码不同的词条被映射到同一散列地址的情况。随机越强、规律性越弱的散列函数越好

除余法

将散列表长度M取作为素数,并将关键码key映射至key关于M整除的余数: h a s h ( k e y ) = k e y m o d    M hash(key) = key \mod M hash(key)=keymodM
在这里插入图片描述

采用除余法时必须将M选作素数,否则关键码被映射至[0, M)范围内的均匀度将大幅降低,发生冲突的概率将随M所含素因子的增多而迅速加大。

词条集中到散列表内少数若干桶中(或附近)的现象,称作词条的聚集(clustering)。
显然,好的散列函数应尽可能此类现象,而采用素数表长则是降低聚集发生概率的捷径。

一般地,散列表的长度M与词条关键码间隔T之间的最大公约数越大,发生冲突的可能性也将越大。
因此,若M取素数,则简便对于严格或大致等间隔的关键码序列,也不致出现冲突激增的情况,同时提高空间效率。

MAD 法

以素数为表长的除余法尽管可在一定程度上保证词条的均匀分布,但从关键码空间到散列地址空间映射的角度看,依然残留有某种连续性
比如,相邻关键码所对应的散列地址,总是彼此相邻;极小的关键码,通常都被集中映射到散列表的起始区段——其中特别地,0值居然是一个“不动点”,其散列地址总是0,而与散列表长度无关。

在这里插入图片描述
例如,在如图(a)所示,将关键码:{ 2011, 2012, 2013, 2014, 2015, 2016 } 插入长度为M = 17的空散列表后,这组词条将存放至地址连续的6个桶中。尽管这里没有任何关键码的冲突,却具有就“更高阶”的均匀性。

MAD法将关键码key映射为: ( a × k e y + b ) m o d    M (a \times key + b ) \mod M (a×key+b)modM,其中M仍为素数,a > 0,b > 0,且 a mod M != 0
尽管运算量略有增加,但只要常数a和b选取得当,MAD法可以很好地克服除余法原有的连续性缺陷。

按MAD法的散列结果将图(b)所示,a = 31和b = 2时,各关键码散列的均匀性相对于图(a)有了很大的改善。

伪随机数法

越是随机、越是没有规律,就越是好的散列函数。
任何一个(伪)随机数发生器,本身即是一个好的散列函数。比如,可直接使用C/C++语言提供的rand()函数,将关键码key映射至桶地址:rand(key) mod M
其中rand(key)为系统定义的第key个(伪)随机数。

由于不同计算环境所提供的(伪)随机数发生器不尽相同,故在将某一系统中生成的散列表移植到另一系统时,必须格外小心。

散列:排解冲突

散列表的基本构思,可以概括为:开辟物理地址连续的桶数组ht[] ,借助散列函数hash() ,将词条关键码 key 映射为桶地址hash(key).

无论散列函数设计得如何巧妙,也不可能保证不同的关键码之间互不冲突。

冲突及其排解

多槽位法

将彼此冲突的每一组词条组织为一个小规模的子词典,分别存放于它们共同对应的桶单元中。例如,统一将各桶细分为更小的称作槽位(slot)的若干单元,每一组槽位可组织为向量或列表。
在这里插入图片描述

通过槽位细分排解散列冲突:上图将各桶细分为四个槽位。只要相互冲突的各组关键码不超过4个,即可分别保存于对应桶单元内的不同槽位。
针对关键码key的任一操作都将转化为对一组槽位的操作。

多槽位法的缺陷: 可能所有(或接近所有)的词条都冲突于单个桶单元,其余所有的桶都处于空闲状态。

独立链法

令相互冲突的每组词条构成小规模的子词典,不过 采用列表(而非向量) 来实现各子词典。
在这里插入图片描述

利用建立独立链排解散列冲突:上图令各桶内相互冲突的词条串接成一个列表。
相对于多槽位法,独立链法可更为灵活地动态调整各子词典的容量和规模,从而有效地降低空间消耗。但在查找过程中一旦发生冲突,则需要遍历整个列表,导致查找成本的增加。

公共溢出法

在原散列表(图(a))之外另设一个词典结构D_overflow图(b)),一旦在插入词条时发生冲突就将该词条转存至D_overflow 中。就效果而言,D_overflow 相当于一个存放冲突词条的公共缓冲池,该方法也因此得名。
在这里插入图片描述

利用公共溢出区解决散列冲突:策略构思简单、易于实现,在冲突不甚频繁的场合不失为一种好的选择。

闭散列策略

尽管就逻辑结构而言,独立链等策略便捷而紧凑,但绝非上策。仅仅依靠基本的散列表结构,且就地排解冲突,反而是更好的选择。
若新词条与已有词条冲突,则只允许在散列表内部为其寻找另一空桶。如此,各桶并非注定只能存放特定的一组词条;从理论上讲,每个桶单元都有可能存放任一词条。
因为散列地址空间对所有词条开放,故这一新的策略亦称作开放定址;同时,因可用的散列地址仅限于散列表所覆盖的范围之内,故亦称作闭散列。相应地,此前的策略亦称作封闭定址开散列

线性试探法

开放定址策略最基本的一种形式是:在插入关键码key时,若发现桶单元ht[hash(key)]已被占用,则转而试探桶单元ht[hash(key) + 1];若ht[hash(key) + 1]也被占用,则继续试探ht[hash(key) + 2];…;如此不断,直到发现一个可用空桶。

为确保桶地址的合法,最后还需统一对M取模。因此准确地,第i次试探的桶单元应为:ht[(hash(key)+i) mod M], i=1, 2, 3,...

被试探的桶单元在物理空间上依次连贯,其地址构成等差数列。

查找链

采用开放地址策略时,散列表中每一组相互冲突的词条都将被视作一个有序序列,对其中任何一员的查找都需借助这一序列。对应的查找过程,可能终止于三种情况:
1)在当前桶单元命中目标关键码,则成功返回;
2)当前桶单元非空,但其中关键码与目标关键码不等,则须转入下一桶单元继续试探;
3)当前桶单元为空,则查找以失败返回。
在这里插入图片描述
例如,M = 17的散列表,设采用除余法定址,采用线性试探法排解冲突。
若从空表开始,依次插入5个相互冲突的关键码 { 2011, 2028, 2045, 2062, 2079 },则结果应如图(a)所示。此后,针对其中任一关键码的查找都将从:ht[hash(key)] = ht[5] 出发,试探各相邻的桶单元。可见,与这组关键码对应的桶单元ht[5, 10)构成一个有序序列,对其中任一关键码的查找都将沿该序列顺序进行,故该序列亦称作查找链

沿查找链试探的过程,与对应关键码此前的插入过程完全一致。
对于长度为n的查找链,失败查找长度就是n + 1;在等概率假设下,平均成功查找长度为 ⌈ n / 2 ⌉ \lceil n/2 \rceil n/2

尽管相互冲突的关键码必属于同一查找链,但反过来,同一查找链中的关键码却未必相互冲突——多组各自冲突的关键码所对应的查找链,有可能相互交织和重叠。

局部性

线性试探法中组成各查找链的词条,在物理上保持一定的连贯性,具有良好的数据局部性,故系统缓存的作用可以充分发挥,查找过程中几乎无需I/O操作。尽管闭散列策略同时也会在一定程度上增加冲突发生的可能,但只要散列表的规模不是很小,装填因子不是很大,则相对于I/O负担的降低而言,这些问题都将微不足道。

相对于独立链等开散列策略,闭散列策略的实际应用更为广泛。

懒惰删除

查找链中任何一环的缺失,都会导致后续词条因无法抵达而丢失,表现为有时无法找到实际已存在的词条。因此若采用开放定址策略,则在执行删除操作时,需同时做特别的调整。
在这里插入图片描述
为每个桶另设一个标志位,指示该桶尽管目前为空,但此前确曾存放过词条。
在将桶ht[9]作此标记(以X示意)之后,对后继词条的查找仍可照常进行,而不致中断。这一方法既可保证查找链的完整,同时所需的时间成本也极其低廉,称作懒惰删除法

设有懒惰删除标志位的桶,应与普通的空桶一样参与插入操作。

聚集现象

线性试探法虽然简明紧凑,但各查找链均由物理地址连续的桶单元组成,因而会加剧关键码的聚集趋势
线性试探法会加剧聚集现象,而平斱试探法则会快速跳离聚集区段。
在这里插入图片描述

平方试探法

在这里插入图片描述

在试探过程中若连续发生冲突,则按如下规则确定第j次试探的桶地址: ( h a s h ( k e y ) + j 2 )  mod  M , j = 0 , 1 , 2 , . . . (hash(key) + j^2 ) \space \text{mod}\space M, j = 0, 1, 2, ... (hash(key)+j2) mod M,j=0,1,2,...

局部性

平方试探法之所以能够有效地缓解聚集现象,是因为充分利用了平方函数的特点——顺着查找链,试探位置的间距将以线性(而不再是常数1的)速度增长。于是,一旦发生冲突,即可“聪明地”尽快“跳离”关键码聚集的区段。

确保试探必然终止

线性试探法中,只要散列表中尚有空桶,则试探过程至多遍历全表一遍,必然终止。
平方试探法存在空桶却永远无法抵达。

好消息是:只要散列表长度M为素数且装填因子 λ ≤ 50 % \lambda \leq 50\% λ50%,则平方试探迟早必将终止于某个空桶。

装填因子(load factor)是指哈希表中元素的数量除以哈希表的大小。 在词典中,装填因子可以用来衡量词典的使用效率。这里也可以将散列表中非空桶的数目与桶单元总数的比值称作装填因子

随机试探法

借助(伪)随机数发生器来确定试探位置。具体地,第j次试探的桶地址取作:rand(j) mod M ...rand(i)为系统定义的第j个(伪)随机数)。同样地,在跨平台协同的场合,出于兼容性的考虑,这一策略也须慎用。

再散列

需要选取一个适宜的二级散列函数 hash 2 ( ) \text{hash}_2 () hash2(),一旦在插入词条 (key, value) 时发现 h t [ hash ( k e y ) ] ht[\text{hash}(key)] ht[hash(key)] 已被占用,则以 hash 2 ( k e y ) \text{hash}_2 (key) hash2(key) 为偏移增量继续尝试,直到发现一个空桶。
被尝试的桶地址依次应为:
[ hash ( k e y ) + 1 × hash 2 ( k e y ) ] % M [\text{hash}(key) + 1 \times \text{hash}_2 (key)] \% M [hash(key)+1×hash2(key)]%M
[ hash ( k e y ) + 2 × hash 2 ( k e y ) ] % M [\text{hash}(key) + 2 \times \text{hash}_2 (key)] \% M [hash(key)+2×hash2(key)]%M
[ hash ( k e y ) + 3 × hash 2 ( k e y ) ] % M [\text{hash}(key) + 3 \times \text{hash}_2 (key)] \% M [hash(key)+3×hash2(key)]%M
hash 2 ( k e y ) = 1 \text{hash}_2(key) = 1 hash2(key)=1 时即是线性试探法

桶排序

给定[0, M)内的n个互异整数( n ≤ M n \leq M nM),如何高效地对其排序?
M = 10n = 5的一个实例:
在这里插入图片描述

使用最简单的散列函数hash(key) = key,将这些整数视作关键码并逐一插入散列表中。最后,顺序遍历一趟该散列表,依次输出非空桶中存放的关键码,即可得到原整数集合的排序结果。

算法借助一组桶单元实现对一组关键码的分拣,故称作桶排序
该算法所用散列表共占O(M)空间。散列表的创建和初始化耗时O(M),将所有关键码插入散列表耗时O(n),依次读出非空桶中的关键码耗时O(M),故总体运行时间为O(n + M)

允许重复

若允许输入整数重复,又该如何高效地实现排序?
在这里插入图片描述

这次需要处理散列冲突。采用独立链法排解冲突。在将所有整数作为关键码插入散列表之后,只需一趟顺序遍历将各非空桶中
的独立链依次串接起来,即可得到完整的排序结果。而且只要在串联时留意链表方向,甚至可以确保排序结果的稳定,故如此实现的桶排序算法属于稳定算法。

依然只需为维护散列表而使用O(M)的额外空间;算法各步骤所耗费的时间也与前一算法相同,总体运行时间亦为O(n + M)

最大间隙

任意n个互异点都将实轴切割为n + 1段,除去最外侧无界的两段,其余有界的n - 1段中何者最大?
若将相邻点对之间的距离视作间隙,则该问题可直观地表述为,找出其中的最大间隙

普通算法: 先将各点按坐标排序;再顺序遍历,依次计算出各相邻点对之间的间隙;遍历过程中只需不断更新最大间隙的记录,则最终必将得到全局的最大间隙。
第一步常规排序即需O(nlogn)时间,所以在最坏情况下总体运行时间将不可能少于这一下界。
在这里插入图片描述

散列算法: 通过一趟顺序扫描找到最靠左和最靠右的点,将其坐标分别记作lohi;然后,建立一个长度为n的散列表,并使用散列函数 hash ( x ) = ⌊ ( n − 1 ) ∗ ( x − l o ) / ( h i − l o ) ⌋ \text{hash}(x) = \lfloor(n - 1) * (x - lo) / (hi - lo)\rfloor hash(x)=⌊(n1)(xlo)/(hilo)⌋ 将各点分别插入对应的桶单元,其中x为各点的坐标值,hash(x)为对应的桶编号:相当于将有效区间[lo, hi)均匀地划分为宽度w = (hi - lo) / (n - 1)n - 1个左闭右开区间,分别对应于第0n - 2号桶单元;另外,hi独自占用第n - 1号桶。
然后,对散列表做一趟遍历,在每个非空桶(黑色)内部确定最靠左和最靠右的点,并删除所有的空桶(白色)。最后,只需再顺序扫描一趟散列表,即可确定相邻非空桶之间的间隙,记录并报告其中的最大者,即为全局的最大间隙。
在最坏情况下,累计运行时间也不超过O(n)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1081727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

cpp文件操作

文件操作 数据流 在cpp中,流(stream)是一个抽象概念,用于描述如何从一个位置到又一个位置传输数据。流主要用于I/O操作。 数据流包括两大类:1. 输入流(istream):数据从某个源流入程序, 2. 输出流(ostrea…

CCF CSP认证历年题目自练Day28

题目一 试题编号: 202109-1 试题名称: 数组推导 时间限制: 1.0s 内存限制: 512.0MB 样例1输入 6 0 0 5 5 10 10 样例1输出 30 15 样例2输入 7 10 20 30 40 50 60 75 样例2输出 285 285 题目分析(个人理解&#…

yml显示不了小树叶图标解决办法

问题描述 在项目中,idea新建yml文件不显示小绿叶图标 1、解决办法一 找到下图设置:file -> settings -> Plugins 查看spring boot插件是非已安装(按照下图步骤查看) 2、解决办法二 ctrlalts,去File Types查…

记一次使用vue-markdown在vue中解析markdown格式文件,并自动生成目录大纲

先上效果图 如图所示,在网页中,能直接解析markdown文档,并且生成目录大纲,也支持点击目录标题跳转到对应栏目中,下面就来讲讲是如何实现此功能的。 1、下载vue-markdown yarn add vue-markdown 2、在页面中渲染markdo…

MySQL数据生成工具mysql_random_data_load

在看MySQL文章的时候偶然发现生成数据的工具,此处直接将软件作者的文档贴了过来,说明了使用方式及下载地址 Random data generator for MySQL Many times in my job I need to generate random data for a specific table in order to reproduce an is…

2023.10 秋爽版 java 软件授权激活 架构 java代码混淆 按日期授权 不联网

什么是代码混淆? 代码混淆是一种技术,用于在不改变代码功能的情况下,通过改变代码的结构和逻辑,使之变得更难理解和分析,从而增加反向工程的难度。 为什么要进行代码混淆? 在Java应用程序中,…

MyBatisPlus(十七)通用枚举

说明 MyBatisPlus 优雅地使用枚举类型。 声明通用枚举属性 使用 EnumValue 注解枚举属性 package com.example.web.enumeration;import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonValue; import lombok.AllArgsConstru…

安装Android SDK点击SDK Manager.exe一闪而退完美解决方案

如上图,我们点击 “SDK Manager.exe” 总是一闪而退。 1.查看提示说Detect whether Java SE Development Kit is installed,检查你的JDK是否安装。 2.在cmd里看了,java -version 和javac -version都是有显示版本的。说明安装以及环境配置成…

ES6介绍

1:ES6声明变量 1.变量var声明变量的问题 ES5 可以重复声明变量可以先使用再声明造成全局变量污染 2.let声明变量特点 ES6 不能先使用再说明不能重复定义一个变量具有块级作用域 3.const声明变量特点 ES6 不能先使用再说明一旦声明必须赋值赋值之后不能修改具有块级…

0144 文件管理

目录 4.文件管理 4.1文件系统基础 4.2目录 4.3文件系统 部分习题 4.文件管理 4.1文件系统基础 4.2目录 4.3文件系统 部分习题 1.UNIX操作系统忠,输入/输出设备视为() A.普通文件 B.目录文件 C.索引文件 D.特殊文…

cesium 地图蒙版遮罩效果

示例代码 <!DOCTYPE html> <html lang"en"><head><!-- Use correct character set. --><meta charset"utf-8" /><!-- Tell IE to use the latest, best version. --><meta http-equiv"X-UA-Compatible"…

快速排序 ← PPT

【算法代码】https://blog.csdn.net/hnjzsyjyj/article/details/127825125

JavaScript (下)

1.面向对象 在 Java 中我们学习过面向对象&#xff0c;核心思想是万物皆对象。在 JavaScript 中同样也有面向对象。思想类似。 把相关的数据和方法组织为一个整体来看待&#xff0c;从更高的层次来进行系统建模&#xff0c;更贴近事物的自然运行模式 1.类的定义和使用 格式…

Java二叉树超详解(常用方法介绍)(2)

二叉树中的常用方法 静态二叉树的手动创建 这里我们先给出二叉树结点的信息(这里是内部类)&#xff1a; static class TreeNode {public char val;public TreeNode left;//左孩子的引用public TreeNode right;//右孩子的引用public TreeNode(char val) {this.val val;}} 手动…

嵌入式系统开发【深入浅出】 UART 与 USART

目录 UART: 通用串行异步收发器 串行通信的时序 8N1&#xff1a;8位数据位 N没有校验位 1停止位1位 中断控制 编程重点 引言&#xff1a; 串口通讯(Serial Communication)是一种设备间非常常用的串行通讯方式&#xff0c;并且大部分电子设备都支持该通讯方式&#xff0c;也…

TensorFlow入门(二十一、softmax算法与损失函数)

在实际使用softmax计算loss时,有一些关键地方与具体用法需要注意: 交叉熵是十分常用的,且在TensorFlow中被封装成了多个版本。多版本中,有的公式里直接带了交叉熵,有的需要自己单独手写公式求出。如果区分不清楚,在构建模型时,一旦出现问题将很难分析是模型的问题还是交叉熵的使…

【 数据结构:堆(Heap)】大根堆、小根堆、堆的向上调整算法、向下调整算法 及 堆的功能实现!

前言 本系列文章【数据结构】默认会使用 C/C 进行设计实现&#xff01;其他语言的实现方式请参照分析设计思路自行实现&#xff01; 注[1]&#xff1a;文章属于学习总结&#xff0c;相对于课本教材而言&#xff0c;不具有相应顺序性&#xff01;&#xff08;可在合集中自行查看…

C++: 继承

学习目标 1.继承的概念及定义 2.基类和派生类对象赋值转换(切片) 3.继承中的作用域(隐藏/重定义) 4.派生类的默认成员函数 5.继承与友元 6.继承与静态成员 7.菱形继承与菱形虚拟继承 8.总结 1.继承的概念及定义 1.1概念 继承: 它允许你创建一个新的类&#xff08;称为子类或派…

小程序uView2.X框架upload组件上传方法总结+避坑

呈现效果: 1.1单图片上传 1.2多图片上传 前言:相信很多人写小程序会用到uView框架,总体感觉还算OK吧,只能这么说,肯定也会遇到图片视频上传,如果用到这个upload组件相信你,肯定遇到各种各样的问题,这是我个人总结的单图片和多图片上传方法. uView2.X框架:uView 2.0 - 全面兼容…

JavaSE学习值之--String类

&#x1f495;"不要同情自己&#xff0c;同情自己是卑劣懦夫的勾当&#xff01;"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;JavaSE学习值之--String类 目录 前言&#xff1a; 一.String类 1.String类的属性 2.字符串的构造 注意&#xf…