字典是支持基于关键码的数据存储与检索的数据结构,也被成为查找表、映射或关联表。
有关检索效率的评价标准,通常考虑的是在一次完整检索过程中比较关键码的平均次数,通常称为平均检索长度(Average Search Length, ASL),其定义是(其中n为字典中的数据项数):
ASL = ∑p*c,(从i=0,n-1),其中c和p分别为第i项数据元素的检索长度和检索概率。
如果字典中各元素的检索概率相等,即p = 1/n, 那么ASL = 1/n *∑c。
字典和索引
作为一种数据存储结构,支持在字典里存储一批数据项
提供支持数据检索的功能,设法维护从关键码找到相关数据的联系信息。
字典线性表实现
用list实现,其中的key和value的关联可以用tuple和list实现。插入时可以用append实现,时间复杂度为O(1);删除可以在定位后用list的pop操作实现,需要找到位置所在,时间复杂度为O(n);检索的操作也是从前到后扫描,复杂度也是O(n)。
基于线性表实现的字典的优缺点很明显:
1、实现简单,适用于任意关键码类型。
2、平均检索效率低(线性时间),表长度n比较大时,检索比较耗时。
3、删除操作的效率比较低,不太适合频繁变动的字典。
有序线性表和二分法检索
当使用有序线性表实现字典时,可以使用二分法来检索。
在 插入和删除表中元素时,insert和delete都必须保持字典中的元素有序,所以每次操作以后都要移动其他元素,所以平均时间都是O(n),search利用有序的性质,采用二分法,时间复杂度为O(log N),。所以有序线性表实现的字典适合数据变动不大的字典。
可以用二叉树来表示二分法检索的过程,成为二分法检索过程的判定树。再加上各种检索不成功的情况,就是二叉判定树的扩充二叉树。
哈希和哈希表
哈希的思想和应用
先思考一个问题,什么时候基于关键码能最快找到所需的数据?
如果数据能连续存储,而关键码就是存储数据的地址(或下标),这样就能在O(1)时间找到。
也就引出了哈希表的思想:如果一种关键码不能或者不适合作为数据存储的下标,可以考虑通过一个变化(计算)把它们映射到一种下标。这样做就把基于关键码的检索变为了基于整数下标的直接元素访问。
以哈希表的思想实现字典,具体方法是:
1、选定一个整数的下标范围(如0-N)的顺序表
2、选定一个从实际关键码集合到上述下标范围的适当映射h:
在需要存入关键码数据为key的数据时,将其存入表中第h(key)个位置;
遇到以key为关键码的检索时,直接去找表中第h(key)个位置的元素。
哈希技术:设计和性质
哈希函数 h 是一个从大集合到小集合的映射。|KEY|>>| INDEX |,所以它显然不可能是单射,必然会出现多个不同的关键码映射到同一个下标的情况,即key1≠key2, 但是h(key1) = h(key2)。这就是我们常说的冲突或碰撞。
当哈希表中的元素越多,出现冲突的可能性就越大,用负载因子来衡量这种情况:
负载因子α = 哈希表中当时的实际数据项数 / 哈希表的基本存储区的容量。
负载因子越大,出现冲突的可能性就越大。如果扩大哈希表的存储空间,就可以降低其负载因子,减小出现冲突的概率。但是负载因子越小,哈希表中空闲空间的比例也就越大,造成空间的浪费。
哈希函数
在设计哈希函数时,最重要的考虑就是尽可能的减少出现冲突的可能性:
1、函数应能把关键码映射到值域index中尽可能大的区域
2、不同关键码的哈希值在index里均匀分布,有可能减少冲突
3、函数的计算比较简单,减小开销
用于整数关键码的哈希方法
1、数字分析法:对于给定的关键码集合,分析所有关键字中各位数字的出现频率,从中选出分布情况较好的若干数字作为哈希函数的值。
2、折叠法:将较长的关键码切分为几段,通过某种运算将它们合并。例如用加法并舍弃进位的运算,或者二进制串运算。
3、中平方法:先求出关键码的平方,然后取中间的几位作为哈希值。
4、除余法:关键码key是整数,用key除以某个不大于哈希表长度n(n通常取2的某个幂数)的整数p(小于n的最大素数),以得到的余数(或者余数加 m)作为哈希地址。(key % p + m)
5、基数转换法:取一个正整数r, 把关键码看作是基数为r的数(r进制的数),将其转为十进制或二进制数。通常r取素数以减小规律性。
对于整数关键码,哈希函数的设计有两方面的要求:把较长的关键码映射到较小的区间;尽可能消除关键码和映射值之间明显的规律。
如果是字符串作为关键码,可以把一个字符看做一个整数(直接用字符的编码),把一个字符串看做以某个整数为基数的“整数”。通过基数转化法把字符串转换成整数,再用取余法把结果归入哈希表的下标范围。
冲突消解
冲突的消解一般有两类:内消解方法(在基本的存储区内部解决冲突问题)和外消解方法(在基本的存储区外部解决冲突)。
内消解的基本方法称为开地址法:在准备插入数据并发现冲突时,设法在基本存储区里为需要插入的数据项另行安排一个地址。
一般都基于冲突地址顺延插入:H = (h(key) + d)% p ,d是一个从1开始的递增序列。如果h(key)的位置空闲,就直接插入,否则就逐个试探一个个位置H,找到第一个空闲位置就插入。
1、取D= 1, 2, 3, 4、、、,简单的整数序列,这种方法称为线性探查
2、另外设计一个哈希函数h2,另d = i * h2(key), 称为双哈希探查。
不过这两种方法都不能解决随着数据的增加,冲突越来越严重的问题。(基本存储空间有限)
检索和删除:
在开地址法的哈希表上做检索:
1、调用哈希函数,求出key对应的哈希地址
2、检查相应的存储位置,如果该位置没有数据项,就说明没有
3、检索位置有数据,就比较key和该位置的关键码,如果匹配就检索成功
4、否则根据哈希表的探查序列找到下一个地址,回到第二步。
删除:
不在被删除的元素位置放置空位标志,而是存入另一个特殊标志,在执行检索操作时,将这个标志看做有元素并继续向下探查;而执行插入操作时,则把这个标志看做空位,把新元素存入这里。
外消解技术有溢出区方法和桶哈希
溢出区方法:当插入关键码的哈希位置没有数据时,可以直接插入;发生冲突时将相应数据和关键码一起存入溢出区。数据在溢出区里顺序排列。
对应的检索和删除操作也是先找到哈希位置,如果那里有元素但关键码不匹配,就到溢出区顺序检索,直到找到要找的关键码,或者确定相应数据不存在。
桶哈希:数据项不存发在哈希表的基本存储区,哈希表里保存的是对数据项的引用。
拉链法:哈希表中的每个index都对应一个链接的结点表,具有相同哈希值的数据项都保存在这个哈希值对应的链表里。
通过顺序表的直接位置映射和链表的顺序操作完成:插入操作需要先找到key对应的位置,然后插入到相应链表中;检索关键码时,先通过哈希函数找到相应的链表,然后顺序检查;删除和检索类似。
python中的字典和集合都是基于哈希表实现的,所以我们在使用中最好是使用自带的字典和集合,效率更高。