散列表
我们前面已经说过数组这个数据结构,他非常的方便,使用连续的内存和下标来操作元素。
但是这难以满足一些其他的需求,比如我们需要将两个对象进行一一对应,没错我们常用的字典就是其中的一种类型。
字典这种结构使用键(key)---值(value)的对应关系,我们可以根据键获得对应的值。
a = {"liu":98,"zhang":99}
a["liu"]
# 98
我们可以假设的思考一下该如何实现这种结构。我们首先来思考最简单的方法,使用两个数组一一对应,每次输入一个键的时候则找到对应的位置,并获取另一个数组中同位置的元素,大概意思就是下图:
然而效率应该是比较低的,因为我们需要遍历所有元素找到该键对应的值,而且这个方法需要两个数组。我们干脆使用一个数组,每个元素存储字典的一对键和值。
虽然数组变为一个了,但是元素的查找还是很费劲,需要一一遍历。我们知道数组可以使用下标访问,那么我们能不能把问题继续转化一下。我们知道所有数据在计算机中都是数字,可以被用来计算。那么,我们是不是也可以将键转换为数字,将得到的数字作为下标呢?
然而一个不可避免的问题就是,尽管字典要求是键不能相同,但是总会有那么小的机会使得相同的键的f(键)的数值是一样的。所以我们不能都仅仅依靠f(键)来定位下标,还需要将键也存储起来,除此之外遍历的时候也需要知道每组的键和值不是。所以一个字典的基本样子是这样的:
跟上一个构想不同的是,我们的元素寻找不需要遍历,可以直接根据键计算得出,方便了不少呢。
首先,通过函数f将键转换为数字下标,然后将值存储在矩阵对应的下标出,这样我们实现了一个非常有趣的突破:
1. 仅需要一个数组
2 可以根据键直接定位值,复杂度为O(1)
当然我们也看到了这种方法的一些问题:
1. 数组初始化的大小是多少?
2. 不同的键需要得到不同的下标,这样才能安全的使用字典。关键在于我们函数f如何设置,才能合理的将不同的键尽可能的映射为不同的数字下标
哈希函数
我们的散列表,又称为哈希(hash)表,进而这个函数被称为哈希函数,使用中通常是H来表示。
hash的英文原意是“混杂”、“拼凑”、“重新表述”的意思,比较符合这个数据结构的含义吧。
哈尔函数的关键在于,不同的键尽可能的对应不用的下标值,通常采用的方法有一下几种:
-
直接定址法
例如:每日的订单,由于编号不重复,哈希函数可以取关键字自身的后几位部分。
- 除留余数法
当数字值过大时,我们也可以尝试取关键字的除法余数作为哈希值
H(key)=key MOD p (p<=m)
其中数组长度为m p为小于m的数字,这样的得到的余数一定在m范围内
- 数字分析法
数学分析法的关键在于,分析数据中相同和不同的部分,在截取数据作为哈希值时要避免重复出现概率高的部分,使用较低的部分作为哈希值。
举个例子,学校的所有学生的编号为:入学年号+生日+班级+班级序号
2001-19951103-07-52
2005-19971012-14-12
这其中如果只能取四位作为哈希值,我想大家也会适当的抉择一下,哪些留下来是比较好的
-
平方取中法
取关键字平方后的中间几位为哈希地址。
- 折叠法
将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址,这方法称为折叠法。
例如:每一种西文图书都有一个国际标准图书编号,它是一个10位的十进制数字,若要以它作关键字建立一个哈希表,当馆藏书种类不到10,000时,可采用此法构造一个四位数的哈希函数。
- 随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即
H(key)=random(key),其中random为随机函数。通常用于关键字长度不等时采用此法。
Python中的字典
首先是一个小的总结,(字典对象的核心是散列表。散列表是一个稀疏数组(总是有空白元素的数组,因为会自动扩容,不会让很多元素都被占用),数组的每个单元叫做 bucket。每个 bucket 有两部分:一个是键对象的引用,一个是值对象的引用。所有 bucket 结构和大小一致,我们可以通过偏移量来读取指定 bucket。当数组大小被占用到一定程度时,就会自动扩容,并将所有的数据复制到新的数组中)
我们仅简单的说了说概念,现在来看一下实际的字典是什么样的,我们还和数组做了比对
我们的a是一个字典,存储的是键值对,说明存储中键和值都是一起存储的。而b是一个列表,仅是由下标来标识的。然而上文说过,散列表说不定就会发生不同键得到同一个数值,导致冲突问题。为应对这个问题,提出了好几个比较有成效的方法,具体的会在下一篇会在讲解。