• 数据结构和算法: 散列表


    散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表

    散列表的时间复杂度不是严格的O(1), 因为和多种因素有关, 比如散列函数, 还有就是如果采用链表法处理冲突, 那么最坏情况是所有数据都散列到一个链表中, 此时是O(n).

    hash函数有以下几种简单实现的方法

    • 取余法
      常见的对一个数进行取余操作
    • 分组求和法
      如一个电话436-555-4601, 分成2位数(43, 65, 55, 46, 01), 计算后对一个数进行取余
    • 平方取中法
      首先对该项平方, 然后提取一部分数字结果. 如要对21进行hash, 那么平方后是441, 取后两位在进行hash运算

    负载因子

    计算方法:散列表包含的元素数/位置总数, 比如一个数组有10个位置, 目前里面有4个元素, 那么填装因子就是0.4

    填装因子大于1表明元素数超出位置总数, 需要对数组进行拓容

    建议: 一旦填装因子大于0.7就调整散列表的长度

    要实现散列表, 散列函数很重要, 好的散列函数可以减少冲突

    冲突解决

    常用的解决hash冲突的方法有以下几种

    1. 开放寻址法(open addressing)
      如果出现了冲突, 就重新探测一个空位置插入. 当数据量较小, 装载因子小的时候适合采用开放寻址法, 因为当负载因子接近1的时候冲突率会很高, 所以使用内存会比较多.
      通过以下三种方法来再次找到空的槽:
      • 线性探测
        从原hash位置顺序移动, 直到找到一个空的槽后插入. 查找的时候也是先计算散列值, 对比是否相等, 如果不相等顺序向下查找, 如果下一个是空位, 说明不在散列表中
      • 二次探测
        类似线性探测, 但是会使用跳过值
      • 随机探测
        随机选择, 直到找到空槽
    2. 重新散列
      准备多个hash函数, 如果一个发生冲突, 执行下一个
    3. 链表法(chaining)
      每个槽都对应一个链表, 把所有散列值相同的元素放到对应的链表中. 插入的时间复杂度是O(1), 查找时O(k), k是该链表的长度. 比较适合大数据量的情况, 而且可以使用红黑树替代链表进行优化.

    模拟实现

    实现思路: 维护两个列表slotsdata, 通过散列函数来得到存储位置, 然后把key和value存入两个列表的对应位置

    # coding:utf-8
    
    
    class HashTable:
        """
        模拟实现python的字典结构
        """
    
        def __init__(self):
            self.size = 11  # 通过size来求余数. 
            self.slots = [None] * self.size  # 存键
            self.data = [None] * self.size  # 数据域, 存数据
    
        def put_data(self, key, data, slot):
            if self.slots[slot] == None:
                self.slots[slot] = key
                self.data[slot] = data
                return True
            else:
                if self.slots[slot] == key:
                    self.data[slot] = data
                    return True
                else:
                    return False
    
        def put(self, key, data):
            """
            通过hash函数得到对size取余之后的存储位置
            """
            slot = self.hash_function(key, self.size)
            result = self.put_data(key, data, slot)
            while not result:
                # 如果有冲突, 重新执行hash函数计算新的位置
                slot = self.rehash(slot, self.size)
                result = self.put_data(key, data, slot)
    
        def hash_function(self, key, size):
            return key % size
    
        def rehash(self, old_hash, size):
            """判断指针位置是否有冲突, 有的话加1后重新计算新的存储位置"""
            return (old_hash + 1) % size
    
        def get(self, key):
            """根据key得到原本的下标位置"""
            start_slot = self.hash_function(key, self.size)
            data = None
            stop = False
            found = False
            position = start_slot
            while self.slots[position] != None and not found and not stop:
                # 如果key能对应上说明没有冲突直接得到
                if self.slots[position] == key:
                    found = True
                    data = self.data[position]
                # 如果slot对应的值不等于key
                else:
                    position = self.rehash(position, self.size)
                    if position == start_slot:
                        stop = True
            return data
    
        def __getitem__(self, key):
            """根据下标获取元素"""
            return self.get(key)
    
        def __setitem__(self, key, data):
            """设置新的元素"""
            self.put(key, data)
    
    
    if __name__ == '__main__':
        table = HashTable()
        table[54] = 'cat'
        table[26] = 'dog'
        table[93] = 'lion'
        table[17] = "tiger"
        table[77] = "bird"
        table[44] = "goat"
        table[55] = "pig"
        table[20] = "chicken"
        table[22] = "chicken"
        table[18] = "ql"
        table[19] = "0999"
        print(table.slots)
        print(table.data)
        # 输出, 直接根据下标进行输出
        print(table[54])
        print(table[17])
    
    

    总结

    • 在最优情况下(没有冲突), 散列表可以达到 O(1) 时间复杂度, 出现冲突后时间复杂度就和负载因子相关了. 所以说使用Hash表进行搜索时并不是严格的O(1)时间复杂度

    • 为什么散列表经常和链表一起使用?
      散列表虽然查找, 删除和插入速度很快, 但是数据都是无序的. 可以结合链表让数据有序, 需要按顺序遍历散列表的数据时, 可以直接扫描练链表

    注意

    当使用容量不够时需要扩容, 此时需要注意容量大了后原始内容的散列值可能会变. 还要注意扩容过程中的速度问题, 可以参考渐进式扩容

  • 相关阅读:
    双谷人才财务管理(3)
    远程服务器上个人目录下python路径设置
    ubnutu16安装谷歌浏览器
    一个数组除了一个元素只出现一次,其他元素全都出现了三次,输出出现一次的元素
    一个整型数组里除了一个数字之外,其它的数字都出现了两次。请写程序找出这个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
    滑动窗口的最大值
    360
    拼多多2018/8/5算法工程师笔试
    最小的K个数 C++(BFPRT,堆排序)
    CCF201312-3 最大的矩形(100分)
  • 原文地址:https://www.cnblogs.com/zlone/p/11523510.html
Copyright © 2020-2023  润新知