题目分析
这个题目非常有价值,小伙伴们包括我也常常会出现一些问题,就是非常清楚哈希表的使用方法,但是让自己定义哈希表时就两眼抹黑,不知道如何下手。这也是年轻人普遍存在的问题,就像被制裁一样,为什么被制裁?就是因为我们拿到了一些上层的技术,直接使用非常方便,不去研究底层的原理,一旦不提供给我们上层技术,那么我们建造的大楼就会摇摇欲坠。这里也不过多深入讨论这个社会现象,因此小伙伴们在享受胜利果实的时候,一定要清楚底层的原理,这样才能在这个行业走得更远。
投机取巧法
这个题目定义为简单题,可能是使用投机取巧法,因为所有值都是在[0, 1000000]以内,而且全都是整型,因此我们直接建立一个长度为1000001的数组,键就是索引,值就是索引对应的值即可。
这个方法只能解决这个问题,如果键的范围特别大则无法使用这种方法。因此这个方法不是一个通用解法。
算法每次操作的**时间复杂度为$O(1)$,空间复杂度为$O(m)$**,m为数据范围。
1 | class MyHashMap(object): |
开放寻址法
一个较好的方法是开放寻址法,就像办理业务一样,每一个键都对应一个服务台,如果有两个键对应同一个服务台,即发生了冲突,则顺次寻找下一个服务台。
初始时都默认为一个不会被取到的值,在这里就默认为-1,说明没有人占用该服务台。
插入键值对时:如果某个哈希值为k,则k服务台被占用,当下一次某个哈希值为k时,依次向下寻找,如果该键不是-1,说明被占用,比较两个键是否相等,如果相等则替换值即可,如果不相等继续向下寻找。找到第一个空位置-1时,将该位置放入键值对即可。
获取值时:当键对应的哈希值为k时,依次向下寻找,如果该键不是-1,说明被占用,比较两个键是否相等,如果相等则返回值即可,如果不相等继续向下寻找。找到第一个空位置-1时,说明没有找到该元素,返回-1。
删除键值对时:这个要特别注意,当键对应的哈希值为k时,依次向下寻找,如果该键不是-1,说明被占用,比较两个键是否相等,如果相等则将键值对改为-2,不能改成-1,小伙伴们想一想为什么?如果不相等继续向下寻找,找到第一个空位置-1时,说明没有找到该元素,不用删除操作,直接return即可。
如果改成-1,那么后面的就会被切断了,举个例子,(2, 2),(5, 5),(8, 8)中的键都对应哈希值10,那么插入时,dic[10] = (2, 2),dic[11] = (5, 5), dic[12] = (8, 8),如果删除了(5, 5),则先检查dic[10],发现键不相等,则继续寻找dic[11],发现匹配,则将dic[11]改成[-1, -1],那么下一次查找(8, 8)时,也是先检查dic[10],发现不相等,继续寻找dic[11],发现-1,则不继续寻找了。这就会产生问题。因此要改成-2。
因此这种方法的局限性也很容易的看出,就是会浪费大量内存,需要内存数量大于操作次数。
算法最坏情况下时间复杂度为$O(n)$,均摊每次操作的时间复杂度为$O(1)$,空间复杂度为$O(n)$**,其中n为操作次数。
1 | def my_hash(key): |
拉链法
拉链法是我认为最优的解法,不会浪费大量内存,但是需要用链表保存,需要额外的空间存放指针,而且代码写起来较为繁琐。
其解决冲突的思想是,将每一个哈希值对应一个链表,节点的值为一个二元组,第一个元素为键,第二个元素为值。
插入键值对时:如果某个哈希值为k,寻找第k个链表,从头到尾进行遍历,如果链表某个节点的第一个元素等于键,则直接将第二个元素修改为值即可,如果遍历到链表尾部,则在尾部插入一个节点,该节点的值为键值对即可。
获取值时:如果某个哈希值为k,寻找第k个链表,从头到尾进行遍历,如果链表某个节点的第一个元素等于键,则直接将第二个元素返回即可,如果遍历到链表尾部,则返回-1。
删除键值对时:如果某个哈希值为k,寻找第k个链表,从头到尾进行遍历,如果链表某个节点的第一个元素等于键,则直接将该节点的上一个节点指向该节点的下一个节点即可,如果遍历到链表尾部,则没有找到该键值对,不需要进行任何操作。
算法最坏情况下时间复杂度为$O(n)$,均摊每次操作的时间复杂度为$O(1)$,空间复杂度为$O(n)$**,其中n为操作次数。
1 | class Linklist: |
刷题总结
这个题目真是太妙了,考察了常用数据结构的创建,也许今后小伙伴们可以直接使用dict数据结构,不需要手动创建,但是摆脱拿过来直接用的思维惰性是非常非常重要的。