哈希表的Python实现

参考

中国大学MOOC慕课(数据结构与算法Python版)
CSDN博客

哈希表(散列表)

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

实际上,哈希表与Python内置的数据结构字典十分相似,都是以键值对的抽象形式存储数据。而哈希表的关键是构造哈希函数hashfunction。该函数接受用户输入的键值对中的键作为输入,返回值作为索引(或称之为槽)。根据这个索引,我们可以将用户输入的键与值分别存入两个列表中,因为共用一个索引所以,实际上键值对在各自列表中所在的位置是相同的。

因此问题的关键在于寻找合适的哈希函数。一般来说我们使用取余法建立hash函数。即 hashvalue=key%hashsize。比如一个长度为11的哈希表,一共有0~10,11个槽位,那么任意键除以11取余得到的值都必然在0到10之中。由此通过哈希函数我们获得了该键值对的共同索引。即用户输入的键与值在其各自列表的位置。

当然,细心的话很快就会发现这套方案是有漏洞的,即可能某一个键输入哈希表后得到的余数相同。即哈希值相同,第二个相同的哈希值对应的键值对如果按照相同的操作步骤的话会覆盖第一个相同的哈希值所对应的键值对,这种情况称之为“碰撞”。我们称一个不存在碰撞的哈希表称之为完美哈希表。完美哈希表理论上来说其实是不存在的,但是实际上我们可以通过一些方式构在近似完美的哈希表(可以广泛的应用于一致性检验技术中)。

那么如何解决碰撞就成了一个亟待解决的问题。这里有两种方法,分别称之为openaddressing开放地址法与链表法。开放地址法是实际上最简单也最实用的方法,即当该索引(哈希值)已经被使用过的时候,这时候有两种情况:
① 用户新输入的键与之前的键是完全一样的,所以求得的哈希值也一样。比如11与11对11求余数都为0。
② 用户输入的键与原来哈希值对应的键不一样,但是经过哈希函数求余后一样比如22与11对11求余数都为0。

针对第一种情况,我们理解为,用户想要覆盖原来的键值对。比如用户一开始输入了11:0,后面又输入11:1。所以这个时候0号槽或者说哈希值为0的时候对应的值应该要由原来的0变成1。因此,我们需要做的仅仅是覆盖原来的value即可。

针对第二种情况,比如11:0,22:1,显然用户想要存入的是两个键值对。我们不能直接覆盖,而是要将22:1这一对新存入到一个空位置里面,只要哈希表的负载因子不为1,即哈希表没有被填满。那么就一定有空槽。至于如何找空槽,开放地址法提供给我们线性查找的方法(linear probing)即从该哈希值开始像后查找空槽,查找完毕后若还是没有找到空槽则又从头开始查找直到找到为止。

这种查找方式显然简单便捷。不过可能会出现聚集clustering的问题。即假如同时又11,22,33三个键那么槽位0,1,2都被占满。不利于哈希表的高效使用,因为我们使用哈希表的初衷就是建立一个查找复杂度为O(1)的表。因此,我们可以使用跳跃式寻找空槽的方式。不过这种方法实际上是牺牲了查找时间,因为当我们想找键22对应的值(实际上不是哈希值为0,而被后延到哈希值x)的时候,我们首先看0号哈希值对应的键是否为22,如果不是,我们就要遍历整个哈希表寻找键22真正的位置。

另外,链式解决法告诉我们,每个槽不仅仅可以存储一个数据也可以存储一个链表甚至一个字典。这样相同的哈希值对应的键值对就可以都存入一个链表或者字典中,缺点是牺牲了存储空间。

Python实现

class HashTable():
    def __init__(self):# 初始化散列表
        self.size=11 # 长度为11
        self.slots=[None]*self.size # 生成slot列表,将所有槽(列表值)初始为None
        self.data=[None]*self.size # 生成data列表,将所有槽中数据设置为None
        # 两个列表中的值一一对应,如slot[1]与data[1]是一对
    def hashfunction(self,key): # 查表函数
        return key%self.size # 该函数的输入为值返回为值对应的槽

    def rehash(self,oldhashvalue): # 解决碰撞问题,寻找返回一个新的槽
        return (oldhashvalue+1)%self.size # 将原来的哈希值+1取余,寻找一个新的槽位

    def put(self,key,data): # 储存函数
        hashvalue=self.hashfunction(key) # 先获得槽,槽就是两个列表共用的索引

        if self.slots[hashvalue]==None: # 如果该槽为空则直接存入即可
            self.slots[hashvalue]=key # 将slots列表中对应的位置放入用户设置的键
            self.data[hashvalue]=data # 将data列表中对应位置放入用户设置的值
        else: # 如果该槽不为空,即哈希值相同且,该哈希值对应的键值对已经被占用了,那么分两种情况
            if self.slots[hashvalue]==key: # 如果是同一个键
                self.data[hashvalue]=data # 直接将原有的数据覆盖为新数据
            else: # 如果不是同一个键则开始寻找某一个哈希值(键值对为None)
                nextslot=self.rehash(hashvalue) # 获取下一个搜寻的槽位
                while self.slots[nextslot] is not None and self.slots[nextslot] is not key:
                    # 当下一个待搜寻的槽位不是空且,该槽位对应的键与新插入的键不是一个键时,再接着寻找
                    nextslot=self.rehash(hashvalue)
                if self.slots[nextslot]==None:# 如果找到一个空槽则填入
                    self.slots[nextslot]=key
                    self.data[nextslot]=data
                else:# 如果发现一个槽位的键与用户输入的键一致则只需要覆盖数据即可
                    self.data[nextslot]=data

    def get(self,key):# 输入键寻找值
        startslot=self.hashfunction(key)
        data=None
        stop=False
        found=False
        position=startslot
        while self.slots[position] is not None and not found and not stop:
            if self.slots[position]==key:
                found=True
                data=self.data[position]
            else:
                position=self.rehash(position)# 接着寻找
                if position==startslot:# 直到,找了一遍回到最初寻找的位置,都没有找到为止
                    stop=True
        return data
    
    #def __getitem__(self,key):
        #return self.get(key)附加功能

    #def __setitem__(self,key,data):
        #self.put(key,data)
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值