zoukankan      html  css  js  c++  java
  • HashMap(python实现原理)

    一、什么是字典?

    字典是一堆key、value配对组成的元素的集合。字典是一个可变容器,可以存储任意类型对象。

    二、字典是否是有序的?

    在python3.6之前,字典是无序的,但是python3.7+,字典是有序的。在python3.7中,字典有序正式成为语言特性。

    三、字典的各种操作时间复杂度?

    字典的查询、添加、删除的平均时间复杂度都是O(1),相比列表与元组,性能更优。

    四、字典的实现原理

    1. python3.6及之前的版本

    字典的底层是维护了一张哈希表,我们可以把哈希表看成一个二维数组,哈希表中的每一个元素又存储了哈希值(hash)、键(key)、值(value)3个元素。

    enteies = [
        ['--', '--', '--'],
        [hash, key, value],
        ['--', '--', '--'],
        ['--', '--', '--'],
        [hash, key, value],
    ]

     由上可见哈希表的存储结构,我们也可以看出,元素之间有一些空元素,我们通过增加一个元素来讲解具体实现。

    • 计算key的hash值【hash(key)】,在和mask做与操作【mask=字典的最小长度(DictMinSize)-1】,运算后会得到一个数字index,这个index就是要插入enteies哈希表中的下标位置。
    • 若index下标位置已经被占用,则会判断enteies的key是否与要插入的key相等。
    • 如果key已经存在,则更新vlaue值
    • 如果key不存在,就表示hash冲突,会继续向下寻找空位置

    以上介绍了老字典的实现过程,下面我们带入具体的数值来介绍。

    # 给字典添加一个值,key为hello, value为word
    mydict['hello'] = 'word'
    
    # 假设是一个空列表,hash表初始如下
    enteies = [
        ['--', '--', '--'],
        ['--', '--', '--'],
        ['--', '--', '--'],
        ['--', '--', '--'],
        ['--', '--', '--'],
    ]
    注:以下计算值为假设值,不等于实际值
    hash_value = hash('hallo') # 假设值为12345
    index = hash_value & (len(enteies) - 1) # 假设index值计算后等于3, 
    
    # 下面会将值存在enteies中
    enteies = [
        ['--', '--', '--'],
        ['--', '--', '--'],
        ['--', '--', '--'],
        [12345, 'hello', 'word'],  # index=3
        ['--', '--', '--'],
    ]
    
    # 我们继续向字典中添加值
    my_dict['color'] = 'green'
    
    hash_value = hash('color')  # 假设值为 同样为12345
    index = hash_value & ( len(enteies) - 1)  # 假设index值计算后同样等于3
    
    # 下面会将值存在enteies中
    enteies = [
        ['--', '--', '--'],
        ['--', '--', '--'],
        ['--', '--', '--'],
        [12345, 'hello', 'word'],  # 由于index=3的位置已经被占用,且key不一样,所以判定位hash冲突,继续向下寻找
        [12345, 'color', 'green'],  # 找到空余位置,则保存
    ]

    上诉就是字典的插入过程,理解了直接,其实删除和查询也是差不多的方法。我们可以看到,不同的key计算出的index是不一样的,在enteies中插入的位置不一样,在entiese中插入的位置就不一样,所以我们在遍历字典的时候,字典的顺序与我们插入的顺序是不一样的。

    我们可以发现,enteies表是稀疏的,随着我们插入的值不同,表会越来越稀疏(hash表也是一个会动态扩展长度的,每一次扩展,都会重新计算所有的key和hash值),所以新的字典的实现就出现了。

    2. python3.7+后的新实现方法

    老版的字典使用一张hash表,新字典在此基础上,再使用了一张新的indices表来辅助。

    indices = [None, None, index, None, index, None, index]
    enteies = [
        [hash0, key0, value0],
        [hash1, key1, value1],
        [hash2, key2, value2]
    ]

    具体实现过程:

    • 计算key的hash值【hash(key)】,在和mask做与操作,运算后得到一个数字【index】,这个index就是要插入的indices的下标位置
    • 得到index后,会找到indices的位置,但是此位置不是存的hash值,hash值、key、value存在enteies表中,indices的index存到对应在enteies中存放数据的下标
    • 如果出现hash冲突,按照老字典的处理方式处理
    # 给字典添加一个值,key为hello,value为word
    my_dict['hello'] = 'word'
    
    # 假设是一个空列表,hash表初始如下
    indices = [None, None, None, None, None, None]
    enteies = []
    
    hash_value = hash('hello')  # 假设值为 12343543
    index = hash_value & ( len(indices) - 1) # 假设index值计算后等于3
    
    # 会找到indices的index为3的位置,并插入enteies的长度
    indices = [None, None, None, 0, None, None]
    # 此时enteies会插入第一个元素
    enteies = [
        [12343543, 'hello', 'word']
    ]
    
    # 我们继续向字典中添加值
    my_dict['haimeimei'] = 'lihua'
    
    hash_value = hash('haimeimei')  # 假设值为 34323545
    index = hash_value & ( len(indices) - 1)  # 假设index值计算后等于 0
    
    # 会找到indices的index为0的位置,并插入enteies的长度
    indices = [1, None, None, 0, None, None]
    # 此时enteies会插入第一个元素
    enteies = [
        [12343543, 'hello', 'word'],
        [34323545, 'haimeimei', 'lihua']
    ]
    

     查询字典的具体过程:

    # 下面是一个字典与字典的存储
    more_dict = {'name': '张三', 'sex': '男', 'age': 10, 'birth': '2019-01-01'}
    
    # 数据实际存储
    indices = [None, 2, None, 0, None, None, 1, None, 3]
    enteies = [
        [34353243, 'name', '张三'],
        [34354545, 'sex', '男'],
        [23343199, 'age', 10],
        [00956542, 'birth', '2019-01-01'],
    ]
    
    print(more_dict['age'])  # 当我们执行这句时
    
    hash_value = hash('age')  # 假设值为 23343199
    index = hash_value & ( len(indices) - 1)  # index = 1
    
    entey_index = indices[1]  # 数据在enteies的位置是2
    value = enteies[entey_index]  # 所以找到值为 enteies[2]
    

     由上可以看出,新字典存储数据的hash表并不会稀疏,由indices来维护具体存储的位置,enteies中的数据的顺序跟插入数据的前后是一样的,所以字典是有序的。

    五、时间复杂度说明

    我们在上面提到了,字典的平均时间复杂度是O(1),因为字典是通过哈希算法来实现的,哈希算法不可避免的问题就是hash冲突,Python字典发生哈希冲突时,会向下寻找空余位置,直到找到位置。如果在计算key的hash值时,如果一直找不到空余位置,则字典的时间复杂度就变成了O(n)了,所以Python的哈希算法就显得非常重要了。Python字典的哈希算法,会尽量保证哈希值计算出的index是平均分布且每一个值之间有剩余位置,例如:

    [index, None, None, None, index, None, None, None]
    

     index尽量只为 0, 3, 5, 7类似值,保证在发生哈希冲突时,能很快的找到空余位置。

    六、字典的key能使用什么值?

    Python字典的key可以使用str, int, tuple等不变数据类型。因为字典是通过hash算法来计算key的值在进行字典操作的,所以key必须为可哈希的,list不能作为字典的key,因为list是可变的。

    l1 = [1]
    l2 = [1, 2]
    
    test_d = {l1: '1', l2: '2'}
    
    # 如果此时把l1.append(2),则字典的两个key hash出来的结果是一样的
    那么字典该给你返回"1",还是"2"呢?
    l1.append(2)
    

     字典的删除操作

    对于删除操作,python会暂时对这个位置的元素,赋一个特殊的值,等到重新调整哈希表大小的时候,再将其删除

    不难理解,哈希冲突的发生,往往会降低字典和集合操作的速度,因此,为了保证其高效性,字典内的哈希表,通常会保证至少留有1/3的剩余空间。随着元素不断插入,当剩余空间小于1/3时,python会重新获取更大的内存空间,扩充哈希表,不过,这种情况下,表内所有的元素位置会被重新排放

    虽然哈希冲突和哈希表大小的调整,都会导致速度缓慢,但是这种情况发生的次数极少,所以,平均情况下,这仍能保证插入、查找和删除的时间复杂度都为O(1)

    七、解决哈希冲突的方法

     1.开放寻址法:如果哈希函数计算出来的index位置已经有值,则可以向后探查新的位置来存储这个值。

    • 线性探查:如果位置i被占用,则探查i+1, i+2...
    • 二次探查:如果位置i被占用,则探查i+1^2, i-1^2, i+2^2,i-2^2..
    • 二度哈希:有n个哈希函数,当使用第一个hash函数h1发生冲突时,则尝试使用h2,h3..

    2.拉链法:hash表每个位置都链接一个链表,当冲突发生时,冲突的元素将被加到该位置链表的最后

    拉链法解决hash冲突代码实现

    # 链表类
    class LinkList:
        class Node:
            def __init__(self, item=None):
                self.item = item
                self.next = None
    
        class LinkListIterator:
            def __init__(self, node):
                self.node = node
    
            def __next__(self):
                if self.node:
                    cur_node = self.node
                    self.node = cur_node.next
                    return cur_node.item
                else:
                    raise StopIteration
    
            def __iter__(self):
                return self
    
        def __init__(self, iterable=None):
            self.head = None
            self.tail = None
            if iterable:
                self.extend(iterable)
    
        def append(self, obj):
            s = LinkList.Node(obj)
            if not self.head:
                self.head = s
                self.tail = s
            else:
                self.tail.next = s
                self.tail = s
    
        def extend(self, iterable):
            for obj in iterable:
                self.append(obj)
    
        def find(self, obj):
            for n in self:
                if n == obj:
                    return True
            else:
                return False
    
        def __iter__(self):
            return self.LinkListIterator(self.head)
    
        def __repr__(self):
            return "<<"+", ".join(map(str, self))+">>"
    
    
    # 类似于集合的结构
    class HashTable:
        def __init__(self, size=101):
            self.size = size
            self.T = [LinkList() for i in range(self.size)]
    
        def h(self, k):
            return k % self.size
    
        def insert(self, k):
            i = self.h(k)
            if self.find(k):
                print("Duplicated Insert")
            else:
                self.T[i].append(k)
    
        def find(self, k):
            i = self.h(k)
            return self.T[i].find(k)
    
    
    ht = HashTable()
    
    ht.insert(0)
    ht.insert(1)
    ht.insert(3)
    ht.insert(102)
    ht.insert(508)
    
    print(",".join(map(str, ht.T)))
    print(ht.find(203))

    常见hash函数:

    • 除法哈希法:h(k) = k % m
    • 乘法哈希法
    • 全域哈希法
     
  • 相关阅读:
    《那些年啊,那些事——一个程序员的奋斗史》——30
    《那些年啊,那些事——一个程序员的奋斗史》——33
    《那些年啊,那些事——一个程序员的奋斗史》——31
    《那些年啊,那些事——一个程序员的奋斗史》——31
    《那些年啊,那些事——一个程序员的奋斗史》——32
    《那些年啊,那些事——一个程序员的奋斗史》——32
    《那些年啊,那些事——一个程序员的奋斗史》——32
    《那些年啊,那些事——一个程序员的奋斗史》——34
    《那些年啊,那些事——一个程序员的奋斗史》——33
    《那些年啊,那些事——一个程序员的奋斗史》——31
  • 原文地址:https://www.cnblogs.com/Xuuuuuu/p/13894009.html
Copyright © 2011-2022 走看看