JFLong  

author : Feilong

date : 2021-6-12

env : python3.7.3 [GCC]

mail : 1203356648@qq.com

实验目的

  • 复习链表的创建、插入和删除操作
  • 理解python内置数据类型set的实现原理
  • 了解工业界对于数据容器的应用标准
  • 了解Java语言中集合类型HashSet、TreeSet(二叉树实现)
  • 了解C++语言中集合类型unordered_set

 

实验原理

哈希集合的特点:

  • 元素无序
  • 元素互异
  • 采用哈希表实现

 

为了实现哈希集合这一数据结构,有以下几个关键问题需要解决:

 

哈希函数:能够将集合中任意可能的元素映射到一个固定范围的整数值,并将该元素存储到整数值对应的地址上。

 

冲突处理:由于不同元素可能映射到相同的整数值,因此需要在整数值出现「冲突」时,需要进行冲突处理。

总的来说,有以下几种策略解决冲突:

 

链地址法:为每个哈希值维护一个链表,并将具有相同哈希值的元素都放入这一链表当中。

 

开放地址法:当发现哈希值 h 处产生冲突时,根据某种策略,从 h 出发找到下一个不冲突的位置。例如,一种最简单的策略是,不断地检查 h+1,h+2,h+3,… 这些整数对应的位置。

 

再哈希法:当发现哈希冲突后,使用另一个哈希函数产生一个新的地址。

 

扩容:当哈希表元素过多时,冲突的概率将越来越大,而在哈希表中查询一个元素的效率也会越来越低。因此,需要开辟一块更大的空间,来缓解哈希表中发生的冲突。

以上内容读者可以自行翻阅数据结构的教材,本题解不再阐述,而是直接给出一个最简单的哈希表实现。

 

实验内容

链地址法

假设哈希表的大小为base,则可以设计一个简单的哈希函数:

hash(x)= x mod base

 

我们开辟一个大小为base的数组,数组的每个位置是一个链表。

当计算出哈希值之后,就插入到对应位置的链表当中。

由于我们使用整数除法作为哈希函数,为了尽可能避免冲突,应当将 base 取为一个质数。在这里,我们取base=1009。

 1 class ListNode:
 2     def __init__(self, val, _next=None):
 3         self.val = val
 4         self.next = _next
 5 
 6 
 7 class MyHashSet:
 8 
 9 
10     def __init__(self):
11         """
12         Initialize your data structure here.
13         """
14         self.base = 1009
15         self.data = [None] * self.base
16 
17 
18     def __hash(self, key: int):
19         return key % self.base
20 
21 
22     def add(self, key: int) -> None:
23         head = self.data[self.__hash(key)]
24         
25         # 在键对应的空间 存储链表(头节点)
26         if not head:
27             self.data[self.__hash(key)] = ListNode(key)
28             return
29 
30 
31         # 定位到链表尾部
32         cur = head
33         while cur:
34             prev = cur 
35             if cur.val == key:
36                 return
37             cur = cur.next
38 
39 
40         # 插入节点(key)
41         prev.next = ListNode(key)
42         return
43 
44 
45     def remove(self, key: int) -> None:
46         head = self.data[self.__hash(key)]
47         if not head:
48             return
49 
50 
51         # 分三种情况讨论 对应空间头节点 头节点的值是否等于key
52         elif head.val == key:
53             self.data[self.__hash(key)] = head.next
54             return
55 
56 
57         # 节点在链表中 执行定位删除操作
58         cur = head 
59         while cur.next:
60             prev = cur 
61             cur = cur.next
62             if cur.val == key:
63                 prev.next = cur.next
64                 return 
65         return
66 
67 
68     def contains(self, key: int) -> bool:
69         """
70         Returns true if this set contains the specified element
71         """
72         head = self.data[self.__hash(key)]
73         cur = head 
74         # 遍历链表 依次判断
75         while cur:
76             if cur.val == key:
77                 return True
78             cur = cur.next
79 
80 
81         return False
Note:Python中还可以灵活地采用二维链表来模拟
class MyHashSet:


    def __init__(self):
        """
        Initialize your data structure here.
        """
        
        self.base = 769
        self.data = [[]]*self.base


    def hash(self,key:int) -> int:
        return key % self.base


    def add(self, key: int) -> None:
        hash_key = self.hash(key)
        if key not in self.data[hash_key]:
            self.data[hash_key].append(key)


    def remove(self, key: int) -> None:
        hash_key = self.hash(key)
        if key in self.data[hash_key]:
            self.data[hash_key].remove(key)


    def contains(self, key: int) -> bool:
        """
        Returns true if this set contains the specified element
        """
        hash_key = self.hash(key)
        return key in self.data[hash_key]

 

复杂度分析

T(n) = O(\frac{n}{b}) \quad

S(n) = O(n+b)

n 是元素数量 b是链表数量(发生冲突的元素数量)

当冲突产生的时候 插入与删除只需要两步

定位到hash值对应的空间 O(1)

遍历长度为 的列表就可以完成 O(m) m = n/b 平均均匀分布的情况下

实验总结

维护键对应的链表 可以节约一定的空间 避免数组预分配带来的性能制约

采用二维列表来实现链地址法 优点是可以动态扩容 缺点是空间开销增大

 

Hash表为什么需要使用mod素数?

从素数定理出发,我们可以知道素数有如下性质
素数定理:在初等数学中有一个基本定理,任意一个大于1的自然数,要么本身就是质数,要么可以分解为几个质数之积,这种分解本身,具有唯一性
在知道素数的性质后,回过头来看Hash表,我们将元素放在Hash表当中,需要解决的一个问题就是尽量解决冲突。
 
参考blog给出了一份实验,结论表明:模数的因子会影响数列的冲突,而且因子越多,冲突的可能性就越大。而素数的因子恰好只有1和其本身,就非常适合用于解决冲突。比如 2 4 6 8 10 12这6个数,如果对 6 取余 得到 2 4 0 2 4 0 只会得到3种HASH值,6的因子有1,2,6。冲突会很多。如果对 7 取余 得到 2 4 6 1 3 5 得到6种HASH值,而7的因子只有1,7。
由3可知,即使1的因子最小,但是在实际中并不用,因为mod1相当于不解决冲突。而初始化的的数组就会非常大。
Hash的用途很多,我们在使用Ngnix做负载均衡的时候,同样用的也是Hash的方式。总的来说,要是数据分布均匀一些,在这种时候就可以考虑使用Hash的方式对数据进行处理。

 

实验参考

https://zhuanlan.zhihu.com/p/123261109 Java集合: HashSet 哈希集详解

https://leetcode-cn.com/problems/design-hashset/solution/she-ji-ha-xi-ji-he-by-leetcode-solution-xp4t/ leetcode官方题解

<<算法>>第四版 章节 “查找”

posted on 2025-04-24 17:02  鱼孜千  阅读(82)  评论(0)    收藏  举报