Python 编程1000例(17):查找算法——哈希查找算法


本系列文章通过 1000(一篇文章表示 1 个实例) 个实例 ,为读者提供较为详细的练习题目,以便读者举一反三,深度学习。本系列的文章涉及到 Python 知识点包括:Python 语言基础、运算符和表达式、语句和程序结构、列表和元组、字典和集合、字符串、正则表达式、函数、面向对象编程、模块和包、异常处理和程序调试、文件和目录操作、数据库编程、界面编程、网络编程、WEB 编程、进程和线程、网络爬虫、游戏编程等知识点,由易到难,由浅入深,一步步打下坚实的编程基础。

本系列文章涉及的算法包括搜索、回溯、递归、排序、迭代、贪心、分治和动态规划等,涉及的数据结构包括字符串、列表、指针、区间、队列、矩阵、堆栈、链表、哈希表、线段树、二叉树、二叉搜索树和图结构等。

本系列文章是笔者为适应当前教育改革的创新要求,更好地践行语言类课程,满足实践教学与创新能力培养的需要,阅读大量书籍、各大互联网公司的面试算法、LintCode、LeetCode、九章算法和结合笔者近几年项目经验编写的系列文章,精选了 1000 个趣味性、实用性强的应用实例,从不同难度、不同算法、不同类型和不同数据结构等方面,将实际算法进行总结,希望为 Python 编程人员抛砖引玉。由于笔者经验与水平有限,博文中疏漏及不妥之处在所难免,衷心地希望各位读者在评论区多提宝贵意见及具体的修改建议,以便笔者进一步修改和完善。

一、哈希查找算法

哈希查找算法是使用哈希函数来计算一个键值所对应的地址,进而建立哈希表格,然后利用哈希函数来查找到各个键值存放在表格中的地址。简单来说,就是把一些复杂的数据,通过某种函数映射(与数学中的映射概念一样)关系,映射成更加易于查找的方式。哈希查找算法的查找速度与数据多少无关,完美的哈希查找算法一般都可以做到一次读取完成查找。

就像生活中,要想找到自己想要的物品,最好的办法就是把该物品固定在某个地方,每次需要用到它的时候就去对应的地方找,用完之后再放回原处。哈希查找法就是这样的一种算法,例如,在一本书中查找内容,首先翻开这本书的目录,然后在目录上找到想要的内容,最后直接根据对应的页码就能找到所需要的内容。

哈希查找算法具有保密性高的特点,因此哈希查找算法常被应用在数据压缩和加解密方面。常见的哈希算法有除留取余法、平方取中法以及折叠法。在讲解这三种算法之前,首先需要了解 哈希表哈希函数 的概念。

1. 哈希表和哈希函数

哈希表是存储键值和键值所对应地址的一种数据集合。哈希表中的位置,一般称为槽位,每个槽位都能保存一个数据元素并以一个整数命名(从 0 开始)。这样我们就有了0号槽位、1号槽位等。起始时,哈希表里没有数据,槽位是空的,这样在构建哈希表时,可以把槽位的值都初始化成 None,如下图所示。
在这里插入图片描述
这是一个大小为 11 的哈希表,或者说有 n 个槽位的哈希表,n 为0~10。上图中的元素和保存的槽位之间的映射关系,称为哈希函数。哈希函数接受一个元素作为参数,返回一个0到n-1的整数作为槽位名。说明:每种哈希算法的哈希函数和哈希表,会在每种哈希算法中介绍。

2. 除留余数法

除留余数法是哈希算法中最简单的一种算法。它是将每个数据除以某个常数后,取余数来当索引。除留取余法所对应的哈希函数形式如下:

h(item)=item % num

参数说明:

  1. item:每个数据。
  2. num:一个常数,一般会选择一个质数,如下面的例子中取质数 11。

例如,将整数集:54、26、93、17、77、31 中每个数据都除以 11,所得的余数作为哈希值,哈希值如下表所示。
在这里插入图片描述
注意:除留余数法一般以某种形式存于所有哈希函数中,因为它的运算结果一定在槽位范围内。哈希值计算出来之后,就要把元素插入到哈希表中指定的位置,如下图所示。
在这里插入图片描述
则对应的哈希函数也得到了哈希值,如:h(54)=10,h(26)=4,h(93)=5,h(17)=6,h(77)=0,h(31)=9。

3. 折叠法

对给定的数据集,哈希函数将每个元素映射为单个槽位,称为 完美哈希函数。但是对于任意一个数据集合,没有系统能构造出完美哈希函数。例如,在上述除留余数法的例子中再加上一个数据 44,该数字除以 11 后,得到的余数是 0,这与数据 77 的哈希值相同。遇到这种情况时,解决办法之一就是扩大哈希表,但是这种做法太浪费空间。因此又有了扩展除留取余数的方案,也就是折叠法。

折叠法是将数据分成一串数据,先将数据拆成几部分,再把它们加起来作为哈希地址。例如,有这样一串数据:5205211314,将这串数据中的数字两两分一组,如下图所示:
在这里插入图片描述
然后将拆的数据进行相加,如下图所示:
在这里插入图片描述
相加之后得到的数值就是这串数据的哈希地址,如果设定槽位是 11,用除留余数法将哈希地址除以 11,得到的值是 6,而数据 6 就是这串数据的哈希值,这种做法称为 移动折叠法

有些折叠法多了一步,就是在相加之前,对数据进行奇数或偶数反转,再将反转后的数字相加。下图所示就是将奇数反转的过程,再将反转后的数据相加,得到的 159 也称为哈希地址。
在这里插入图片描述
此时设定槽位是 11,将哈希地址除以 11,得到的值是 5,数据 5 就是这个数据的哈希值。接下来介绍将偶数反转的情况,如下图所示。
在这里插入图片描述
将上图中反转后的数据相加,得到的 105 也称为哈希地址。如果设定槽位是 11,除留余数法将哈希地址除以 11,得到的值是 6,数据 6 就是这个数据的哈希值。奇数/偶数反转这种折叠法称为 边界折叠法

4. 平方取中法

平方取中法是先将各个数据平方,将平方后数据取中间的某段数字作为索引,例如,对于整数集 54,26,93,17,77,31,平方取中法如下:

步骤1:先将各个数据平方,得到的值如下:

54=2916
26=676
93=8649
17=289
77=5929
31=961

步骤2:取以上平方值的中间数,即取十位和百位,得到该整数集中数据的哈希地址分别为:

91 67 64 28 92 96

步骤3:若设置槽位是 11,将步骤 2 得到的数据分别除以 11 留余数,得到的哈希值分别为:

3 1 9 6 4 8

得到的对应关系如下图所示:
在这里插入图片描述

5. 碰撞与溢出问题

哈希算法的理想情况是所有数据经过哈希函数运算后得到不同的值,但是在实际情况中即使得到的哈希值不同,也有可能得到相同的地址,这种问题被称为 碰撞问题。使用哈希算法,将数据放到哈希表中存储数据的对应位置,若该位置满了,就会溢出,这种问题被称为 溢出问题。存在问题就需要解决问题,如何解决碰撞问题与溢出问题很重要。由于常见的解决问题方法有多种,故在后续博文中进行更新。

二、实例:用哈希查找算法查找七色光颜色

具体代码如下:

class HashTable(object):  # 创建哈希表
    def __init__(self):
        self.size = 11  # 哈希表长度
        self.throw = [None] * self.size  # 哈希表数据键初始化
        self.data = [None] * self.size  # 哈希表数据值初始化

    # 假定最终将有一个空槽,除非 key 已经存在于  self. throw中。它计算原始
    # 哈希值,如果该槽不为空,则迭代 rehash 函数,直到出现空槽。如果非空槽已经包含key,
    # 则旧数据值将替换为新数据值
    def put(self, key, value):  # 输出键值
        hashvalue = self.hashfunction(key, len(self.throw))  # 创建哈希值
        if self.throw[hashvalue] is None:
            self.throw[hashvalue] = key  # 将key值给哈希表的throw
            self.data[hashvalue] = value  # 将value值给哈希表的data
        else:
            if self.throw[hashvalue] == key:
                self.data[hashvalue] = value
            else:
                nextslot = self.rehash(hashvalue, len(self.throw))
                while self.throw[nextslot] is not None and self.throw[nextslot] != key:
                    nextslot = self.rehash(nextslot, len(self.throw))
                if self.throw[nextslot] is None:
                    self.throw[nextslot] = key
                    self.data[nextslot] = value
                else:
                    self.data[nextslot] = value

    def rehash(self, oldhash, size):
        return (oldhash + 1) % size

    def hashfunction(self, key, size):
        return key % size

    # 从计算初始哈希值开始。如果值不在初始槽中,则 rehash 用
    # 于定位下一个可能的位置。
    def get(self, key):
        startslot = self.hashfunction(key, len(self.throw))
        data = None
        found = False
        stop = False
        pos = startslot
        while pos is not None and not found and not stop:
            if self.throw[pos] == key:
                found = True
                data = self.data[pos]
            else:
                pos = self.rehash(pos, len(self.throw))
                # 回到了原点,表示找遍了没有找到
                if pos == startslot:
                    stop = True
        return data

    # 重载载 __getitem__ 和 __setitem__ 方法以允许使用 [] 访问
    def __getitem__(self, key):
        return self.get(key)

    def __setitem__(self, key, value):
        return self.put(key, value)


H = HashTable()  # 创建哈希表
H[16] = "红"  # 给哈希表赋值
H[28] = "橙"
H[32] = "黄"
H[14] = "绿"
H[56] = "青"
H[36] = "蓝"
H[71] = "紫"
print("key的数据是:", H.throw)  # 输出键key
print("value的数据是:", H.data)  # 输出值value
print("结果是:", H[28])  # 根据key=28查value
print("结果是:", H[71])  # 根据key=71查value
print("结果是:", H[93])  # 根据key=93查value

程序执行结果如下图所示:
在这里插入图片描述
感谢您阅读本篇博文,希望本文能成为您编程路上的领航者。祝您阅读愉快!


在这里插入图片描述

    好书不厌读百回,熟读课思子自知。而我想要成为全场最靓的仔,就必须坚持通过学习来获取更多知识,用知识改变命运,用博客见证成长,用行动证明我在努力。
    如果我的博客对你有帮助、如果你喜欢我的博客内容,请 点赞评论收藏 一键三连哦!听说点赞的人运气不会太差,每一天都会元气满满呦!如果实在要白嫖的话,那祝你开心每一天,欢迎常来我博客看看。
 编码不易,大家的支持就是我坚持下去的动力。点赞后不要忘了 关注 我哦!

猜你喜欢

转载自blog.csdn.net/xw1680/article/details/115408795