字典和散列是两种很重要的数据结构,C++里面的unordered_map使用哈希表做底层,但是要问这两种数据结构底层是什么样的,可能很多人回答不上来。
1.字典
字典是一种关联的数据结构,不仅能够存储数据,又能高效的查找数据。字典是一种集合,这种集合中的每个元素由两部分组成,分别为关键码(也成键)和属相(也称值)组成,及字典是一种<关键吗,属性>对的集合。map是STL中用来实现关联数组的容器类型。
字典表项的存放位置及其关键码之间的对应关系可以用关键码及表项位置指针这样一个二元组来表示。这个二元组构成了搜索某一指定表项的索引项。
实现字典一般要借助各种树形搜索结构,从而实现效率上的提升。例如红黑树、AVL树、字典树。
1. 1字典的基本操作
- 插入具有给定关键码及对应属性的元素。
- 在字典中寻找具有指定关键码的元素
- 从字典中删除具有给定关键码的元素
1.2搜索运算
静态搜索:搜索结构在插入与删除等操作后不发生改变。这种方式方法简单,但算法效率不高,而且容易发生溢出。例如,排队检票
动态搜索:搜索结构在执行插入删除等操作时可自动调整自身状态,以保证每次搜索运算都有较高的效率。这种方式可大大减少搜索时间、提高算法执行效率,而且不容易发生溢出。例如,图书馆找书。
2.散列(哈希表)
2.1什么是散列
哈希在表项的存储位置和关键码之间建立一个确定的对应函数关系(Hash()
,哈希函数),以使每个关键码与结构中的唯一存储位置相对应
A
d
d
r
e
s
s
=
H
a
s
h
(
R
e
c
o
r
d
.
k
e
y
)
Address = Hash( Record.key)
Address=Hash(Record.key)。
可见在存储表项时,通过函数计算存储位置,并按此位置存放,这种方法称为散列方法,用散列方法构建的数据结构称为散列表,散列表是一种支持数据快速插入、删除及查找等操作的数据结构。散列表中存储的元素依然是二元组,该二元组由关键码和属性组成。散列表的特别之处是表中元素的排列顺序。散列表中每个元素的存储位置都是由一个叫散列函数的公式决定,也就是散列方法中使用的函数就是所谓的散列函数。
关键码集合都要比散列表地址集合大得多,所以有可能经过散列函数的计算把不同的关键码映射到同一个散列地址上。好的散列函数应当使元素在散列表中呈现均匀存储,而不是堆叠在某个特定的区域。
散列函数需要研究的一个主要问题是,对于给定的一个关键码集合,选择一个计算简单且地址分布均匀的散列函数,避免或尽量减少冲突。 当然由于关键码集合比散列表地址集合大,因此冲突在所难免,于是散列函数需要研究的另一个主要问题是拟定解决冲突的方案。
散列表可以用平均常数时间实现插入和查找操作。
2.2散列函数
-
直接定值法
H a s h ( k e y ) = a ∗ k e y + b Hash(key) = a * key + b Hash(key)=a∗key+b, a 、 b 为 常 数 a、b为常数 a、b为常数
优点:实现方式简单,算法时间复杂度小,而且不会产生冲突。
缺点:要求散列地址空间的大小与关键码集合的大小一致,一般很难实现,而且也没有意义啊。 -
除留余数法
H a s h ( k e y ) = k e y Hash(key) = key % k,key<=m,k <= m 且k为质数 Hash(key)=key
优点:有效缩减散列地址空间的大小。
缺点:极易发生冲突 -
平方取中法
A.利用一定的编码规则,将关键码转换成标识符;
B.求出标识符的内码表示,计算内码的平方值;
C.取中间x位作为元素最终的散列地址 ;
优点:有较强随机性 -
乘余取整法
-
折叠法
(2)字符串散列
- KDR散列
//并非最好的散列函数,但是比较有效的散列函数
unsigned long BKDRHash(const string& str)
{
unsigned long seed = 31;
unsigned long hashval = 0;
for (int i = 0; i < str.size(); i++) {
hashval = hashval * seed + str[i]; //无符号算数运算
}
return hashval % HASHSIZE;
}
- RS散列
//利用很小的额外开销提供了随机的散列结果,对字符串关键字的散列有明显的改进
unsigned long RSHash(const string& str)
{
unsigned long a = 31415, b = 27183;
unsigned long hashval = 0;
for (int i = 0; i < str.size(); i++) {
hashval = (hashval * a + str[i]) % HASHSIZE;
a = a * b % (HASHSIZE - 1); //采用伪随机系数代替固定基数,并且在算法执行过程中不断产生这些系数
}
}
- FNV散列
2.3冲突解决
2.3.1开放定址法
当冲突发生时,使用某种探查技术在散列表中形成一个探查序列。沿此序列逐个单元查找,直到找到给定的关键字,或者碰到一个开放的地址(该地址单元是空)为止。
a)线性探查法
将散列表
T
[
0
,
…
,
m
−
1
]
T[0,…,m-1]
T[0,…,m−1]看成一个循环向量,若初始探查地址为
d
d
d,
h
a
s
h
(
k
e
y
)
=
d
hash(key) = d
hash(key)=d,则最长探查序列为
d
,
d
+
1
,
d
+
2
,
…
,
m
−
1
,
0
,
1
,
…
,
d
−
1
d,d+1,d+2,…,m -1,0,1,…,d-1
d,d+1,d+2,…,m−1,0,1,…,d−1
b)二次探查法
c)双重散列法
开放定址法中最好的方法之一。使用两个散列函数Hash()
和ReHash()
。使用该方法,第一个散列函数Hash()按表项的关键码key计算表项所在的位置。一旦发生冲突,则利用第2个散列函数ReHash()
计算表项的下一个位置的移位量。定义ReHash()
的方法很多,但无论采用什么方法定义,都必须使ReHash()
的值和m
互为素数。
2.3.2 开散列法(拉链法)
将所有关键字为同义词的节点链接在同一个单链表中。
优点:
-
a)拉链法直接明了,且无堆积现象,解决了非同义词之间的冲突问题,平均查找长度及搜索复杂度有所降低
-
b)拉链法在各链表上的结点空间是动态申请的,具有更好的扩展性。
-
c)在拉链法构造的散列表中,删除节点的操作易于实现。只要删除链表上相应的节点即可。而对开放定址法来讲,删除节点不能简单地将被删除的位置为空,否则将截断在它之后填入散列表的同义词节点的查找路径。这是因为在开放定址法中,空地址单元都是查找失败的条件。因此在用开放定址法处理冲突的散列表上执行删除操作,只能在被删除节点上做删除标记,而不能真正删除节点(重新整理也可以吧???)
-
d)尽管拉链法相对于开放定址法有很多优点,但是当节点规模较小是选择线性探查法还是一个比较明智的选择,因为在这种情况下使用拉链法会浪费较多空间。
掘金 **进击的小喽啰**同步发布