Python底层实现一个简易的HashMap(dict)


前前言

在上一片文章中写了对比Python、Java、JavaScript中list, dict的使用,dict就是一个映射集合,在Java中有类似的是HashMap。

上篇文章只是讲了dict的语法层面,只是讲到dict如何增删改查,基本没什么技术含量,这篇文章深入理解Python中的dict(HashMap)底层原理,并且手动封装写一个简易的HashMap(dict).

前言

Python中有一种基础数据结构叫做dict, dict是多个key-value 映射组成的。

在Java中有一个非常类似的工具类,叫做HashMap,也是很多个key-value映射组成的。

python中的dict是作为一种基础数据类型,底层的实现是用C/C++写的,是看不到源码的。而Java中的HashMap是通过基本数据类型封装而成的。

我们也可以使用Python中的基本数据类型封装一个HashMap(不用dict)。

HashMap的作用

HashMap的存在是为了快速访问,通过key可以快速访问到value,并且时间复杂度是O(1)。

那么怎样通过key查找value可以快速查找呢?

学数据结构时学过数组和链表两种线性表,数组可以通过下标随机访问,时间复杂度是O(1),而链表需要遍历来访问,时间复杂度是O(n)。

如果key-value键值对是简单的线性排列,如下:

或者:

那么通过key访问value必须要遍历整个列表,时间复杂度是O(n)。

那么有没有办法可以通过key快速访问到value呢?

这就是hash表。

Hash表原理

能做到随机访问的数据结构只有数组,通过数组下标可以访问内容,时间复杂度是O(1)。

那么现在是需要通过key来访问value,可以把key-value键值对放到特定的数组位置中让key和数组下标有关联,也就是通过key可以计算出数组下标,那么给定一个key,可以迅速定位到value在数组那个元素。

这就是Hash表通过key访问value时间复杂度为O(1)的原理。

Java中HashMap的实现原理

Java中的HashMap结构是这样的:

首先初始化了一个数组,放入key-value键值对时,先计算key的Hash值,再将hash值计算得到数组下标,然后放入数组中(其实是将引用给了数组)。

如果两个元素key经过hash计算,再计算得到数组下标相同(这也叫Hash冲突),那么一个数组节点不可能放两个元素,因此就在该数组节点再放一个链表,将冲突的元素放入链表中

JDK1.7处理Hash冲突是使用的这种链表的方式,而JDK 1.8是使用链表+红黑树的形式,因为加入链表过长,也需要一个个元素遍历,效率也不高。

要想实现一个HashMap,有一下几点要考虑:

  1. 怎样通过Hash值计算得到数组下标?
  2. Hash冲突怎样处理?(使用链表处理用头插法还是尾插法?)
  3. 链表过长怎么办?扩容?如何扩容?

对于问题1,通过Hash值计算出数组下标,这就需要计算出来的结果很平均,而且不能越界。 常规方法可以是取余数,比如数组长度为N,Hash值是x,那么下标应该是x%N。但是取余数效率不高,JDK中使用的是位运算:

static int indexFor(int h, int length) {
   
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

这里有个条件是数组长度必须是2的幂次,因为这样才可以通过位运算得到正确结果。
而这个位运算为什么可以达到效果呢? 因为length是2的幂,也就是最高位是1,其他低位都是0,而length-1就所有位都是1,然后使用&运算,就把h的低位取了出来,然后结果的范围是比length小的。
比如:length = 16,h = 20

length :    0001 0000
length-1:   0000 1111
h :         0001 0100
length&h:   0000 0100   ==  4

对于问题2,JDK1.7中处理Hash冲突是使用的链表,将冲突元素放入到链表中去,并且是使用的头插法插入元素。

对于问题3, 当Hash表中冲突元素过多,链表很长时,那么访问效率就会很慢(因为链表需要都遍历一遍),就需要扩容,将链表变短,让元素在Hash表中更加分散一些。
根据JDK1.7 HashMap的源码,数组默认初始长度是16,当HashMap中元素达到一个阈值,就会扩容,JDK 1.7中这个阈值是默认是75%,当元素个数达到75%时就会扩容。

使用Python实现HashMap

Talk is cheap, show me the code.

这里我仿照JDK1.7中的HashMap源码进行改写了一下,简化了一些地方,实现了一个Python版本的HashMap。

"""
    Entry表示一个Key-value键值对节点,在Python中的dict里面叫做items。
"""
class Entry():
    def __init__(self, hash = 0, key = 0, value = 0, next = None):
        self.hash = hash
        self.key = key
        self.value = value
        self.next = next


class HashMap():
    """
        初始化默认HashMap的容量是16,加载因子是0.75,也就是阈值是16 * 0.75 = 12,就会扩容
    """
    def __init__(self, cap
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python中的字典(dict)是一种用于存储键-值对的数据结构。字典的底层实现使用了哈希表(hash table)来实现快速的查找和插入操作。 具体来说,Python的字典使用了散列表(hash table)作为底层数据结构。散列表是一种通过计算键的哈希值来确定其在内存中的存储位置的数据结构。通过将键映射到其对应的哈希值,字典可以在常数时间复杂度下执行插入和查找操作。 当我们向字典中插入一个键-值对时,Python首先计算键的哈希值,并使用哈希值作为索引来查找对应的存储位置。如果该位置为空,则将键-值对存储在该位置上;如果该位置已经存在其他键-值对,则发生了哈希冲突。在发生哈希冲突时,Python使用开放寻址法或者链地址法来解决冲突。 开放寻址法是一种解决冲突的方法,它会尝试在散列表中寻找一个空闲的位置来存储冲突的键-值对。如果冲突的键-值对不能直接存储在计算得到的索引位置上,开放寻址法会根据某种策略继续寻找下一个位置,直到找到一个空闲的位置。 链地址法是另一种解决冲突的方法,它使用链表来存储冲突的键-值对。当发生哈希冲突时,Python会在冲突的位置上存储一个链表的头节点,并将冲突的键-值对添加到链表中。这样,多个键-值对可以共享同一个位置,从而解决了哈希冲突的问题。 总结起来,Python的字典底层实现使用了哈希表,通过散列表来实现快速的插入和查找操作。当发生哈希冲突时,Python使用开放寻址法或者链地址法来解决冲突。这种实现方式使得字典在大部分情况下具有很高的性能,并且可以支持大量的键-值对。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值