哈希函数如何工作 ?

news2024/11/15 14:06:40

动动发财的小手,点个赞吧!

作为一名程序员,您每天都会使用哈希函数。它们在数据库中用于优化查询,在数据结构中用于使速度更快,在安全性中用于保证数据安全。几乎每次与技术的交互都会以某种方式涉及哈希函数。

哈希函数是基础函数,而且无处不在。但什么是哈希函数,它们如何工作?

这篇文章[1]中,我们将揭开哈希函数的神秘面纱。我们将从查看一个简单的哈希函数开始,然后我们将学习如何测试哈希函数是否好用,然后我们将查看哈希函数的实际使用:哈希映射。

什么是哈希函数?

哈希函数是接受输入(通常是字符串)并生成数字的函数。如果您使用相同的输入多次调用哈希函数,它将始终返回相同的数字,并且返回的数字始终在承诺的范围内。该范围取决于哈希函数,有些使用 32 位整数(即 0 到 40 亿),有些则更大。

如果我们用 JavaScript 编写一个虚拟哈希函数,它可能如下所示:

function hash(input{
  return 0;
}

即使不知道哈希函数是如何使用的,这个哈希函数毫无用处也不足为奇。让我们看看如何衡量哈希函数的好坏,然后我们将深入探讨如何在哈希映射中使用它们。

哈希函数的优点是什么?

由于输入可以是任何字符串,但返回的数字在某个承诺的范围内,因此两个不同的输入可能会返回相同的数字。这称为“冲突”,好的哈希函数会尝试尽量减少它们产生的冲突数量。

但完全消除碰撞是不可能的。如果我们编写一个返回 0 到 7 范围内的数字的哈希函数,并为其提供 9 个唯一输入,则可以保证至少发生 1 次冲突。

alt

为了可视化碰撞,我将使用网格。网格的每个方块将代表哈希函数输出的数字。这是一个 8x2 网格的示例。单击网格以增加示例哈希输出值,并查看我们如何将其映射到网格方块。看看当你得到的数字大于网格方块的数量时会发生什么。

alt

每次我们对一个值进行哈希处理时,我们都会使其网格上相应的方块变暗一点。这个想法是创建一种简单的方法来查看哈希函数如何避免冲突。我们正在寻找的是一个良好、均匀的分布。如果我们有深色方块的团块或图案,我们就会知道哈希函数不好。

这是一个很好的观察。你说得完全正确,我们将在网格上创建“伪碰撞”。不过没关系,因为如果哈希函数很好,我们仍然会看到均匀分布。每个平方增加 100 与每个平方增加 1 一样都是好的分布。如果我们有一个经常发生冲突的糟糕哈希函数,那仍然会很突出。我们很快就会看到这一点。

让我们采用一个更大的网格并对 1,000 个随机生成的字符串进行哈希处理。您可以单击网格来对一组新的随机输入进行散列,网格将以动画方式向您显示每个输入被散列并放置在网格上。

alt

这些值很好并且分布均匀,因为我们使用了一个很好的、众所周知的哈希函数,称为 murmur3。这种哈希值在现实世界中被广泛使用,因为它具有良好的分布性,同时速度也非常非常快。

如果我们使用了错误的哈希函数,我们的网格会是什么样子?

function hash(input{
  let hash = 0;
  for (let c of input) {
    hash += c.charCodeAt(0);
  }
  return hash % 1000000;
}

该哈希函数循环遍历给定的字符串并对每个字符的数值求和。然后,它使用模运算符 (%) 确保该值介于 0 和 1000000 之间。我们将此哈希函数称为 stringSum。

这是在网格上。提醒一下,这是我们正在散列的 1,000 个随机生成的字符串。

alt

这看起来与 murmur3 并没有什么不同。是什么赋予了?

问题是我们要进行哈希处理的字符串是随机的。让我们看看当给定的输入不是随机的时每个函数如何执行:从 1 到 1000 的数字转换为字符串。

alt

现在问题更加清楚了。当输入不是随机的时, stringSum 的输出形成一个模式。然而,我们的 murmur3 网格看起来与随机值的网格相同。

如果我们对前 1,000 个最常见的英语单词进行哈希处理,效果如何:

alt

它更微妙,但我们确实在 stringSum 网格上看到了一种模式。和往常一样, murmur3 看起来和往常一样。

这就是一个好的哈希函数的力量:无论输入如何,输出都是均匀分布的。让我们讨论另一种可视化这一点的方法,然后讨论它的重要性。

雪崩效应

评估哈希函数的另一种方法是基于所谓的“雪崩效应”。这是指当输入的一位发生变化时,输出值中的多少位发生变化。要说哈希函数具有良好的雪崩效应,输入中的单个位翻转应该会导致输出位平均翻转 50%。

正是这个属性帮助哈希函数避免在网格中形成模式。如果输入的微小变化导致输出的微小变化,您就会得到模式。模式表明分布不良且冲突率较高。

下面,我们通过显示两个 8 位二进制数来可视化雪崩效应。顶部数字是输入值,底部数字是 murmur3 输出值。单击它可翻转输入中的一位。输出中发生变化的位将显示为绿色,保持不变的位将显示为红色。

alt

murmur3 表现不错,但您会注意到有时翻转的位少于 50%,有时翻转的位更多。没关系,只要平均是 50% 就可以了。

让我们看看 stringSum 的表现如何。

alt

嗯,这很尴尬。输出等于输入,因此每次只有一位翻转。这确实有意义,因为 stringSum 只是对字符串中每个字符的数值进行求和。此示例仅对单个字符的等效值进行哈希处理,这意味着输出将始终与输入相同。

为什么这一切都很重要

我们已经花时间了解了一些确定哈希函数是否良好的方法,但我们没有花任何时间讨论它的重要性。让我们通过讨论哈希图来解决这个问题。

要理解哈希映射,我们首先必须了解映射是什么。映射是一种允许您存储键值对的数据结构。这是一个 JavaScript 示例:

let map = new Map();
map.set("hello""world");
console.log(map.get("hello"));

这里我们采用一个键值对(“hello”→“world”)并将其存储在地图中。然后我们打印出与键“hello”相关的值,即“world”。

一个更有趣的现实用例是查找字谜词。字谜词是指两个不同的单词包含相同的字母,例如“antlers”和“rentals”或“article”和“recital”。如果您有一个单词列表并且想要查找所有字谜词,您可以按字母顺序对每个单词中的字母进行排序,并将其用作映射中的键。

let words = [
  "antlers",
  "rentals",
  "sternal",
  "article",
  "recital",
  "flamboyant",
]

let map = new Map();

for (let word of words) {
  let key = word
    .split('')
    .sort()
    .join('');

  if (!map.has(key)) {
    map.set(key, []);
  }
  map.get(key).push(word);
}

此代码生成具有以下结构的映射:

{
  "aelnrst": [
    "antlers",
    "rentals",
    "sternal"
  ],
  "aceilrt": [
    "article",
    "recital"
  ],
  "aabflmnoty": [
    "flamboyant"
  ]
}

实现我们自己的简单哈希图

哈希映射是众多映射实现中的一种,实现哈希映射的方法有很多种。最简单的方法,也是我们将要演示的方法,是使用列表的列表。内部列表在现实世界中通常被称为“桶”,因此我们在这里也这么称呼它们。对键使用哈希函数来确定将键值对存储在哪个桶中,然后将键值对添加到该桶中。

让我们看一下 JavaScript 中的简单哈希映射实现。我们将自下而上地进行讨论,因此在进行 set 和 get 实现之前我们将看到一些实用方法。

class HashMap {
  constructor() {
    this.bs = [[], [], []];
  }
}

我们首先创建一个 HashMap 类,该类带有一个设置 3 个存储桶的构造函数。我们使用 3 个存储桶和短变量名称 bs,以便此代码可以在屏幕较小的设备上很好地显示。实际上,您可以拥有任意数量的存储桶(以及更好的变量名称)。

class HashMap {
  // ...
  bucket(key) {
    let h = murmur3(key);
    return this.bs[
      h % this.bs.length
    ];
  }
}

Bucket 方法在传入的键上使用 murmur3 来查找要使用的存储桶。这是我们的哈希映射代码中唯一使用哈希函数的地方。

class HashMap {
  // ...
  entry(bucket, key) {
    for (let e of bucket) {
      if (e.key === key) {
        return e;
      }
    }
    return null;
  }
}

Entry 方法采用一个存储桶和一个键,并扫描该存储桶,直到找到具有给定键的条目。如果未找到条目,则返回 null。

class HashMap {
  // ...
  set(key, value) {
    let b = this.bucket(key);
    let e = this.entry(b, key);
    if (e) {
      e.value = value;
      return;
    }
    b.push({ key, value });
  }
}

set 方法是我们应该从之前的 JavaScript Map 示例中认识到的第一个方法。它需要一个键值对并将其存储在我们的哈希映射中。它通过使用我们之前创建的存储桶和条目方法来实现这一点。如果找到条目,则其值将被覆盖。如果未找到条目,则将键值对添加到映射中。在 JavaScript 中,{ key, value } 是 { key: key, value: value } 的简写形式。

class HashMap {
  // ...
  get(key) {
    let b = this.bucket(key);
    let e = this.entry(b, key);
    if (e) {
      return e.value;
    }
    return null;
  }
}

get 方法与 set 非常相似。它使用bucket和entry来查找与传入的key相关的entry,就像set一样。如果找到条目,则返回其值。如果没有找到,则返回 null。

这是相当多的代码。您应该从中了解的是,我们的哈希映射是一个列表列表,并且哈希函数用于知道要从哪个列表中存储和检索给定的键。

这是该哈希图的实际操作的直观表示。单击存储桶上的任意位置,使用我们的 set 方法添加新的键值对。为了保持可视化简单,如果一个存储桶“溢出”,则所有存储桶都将被重置。

alt

因为我们使用 murmur3 作为哈希函数,所以您应该看到存储桶之间的良好分布。预计您会看到一些不平衡,但通常应该相当均匀。

为了从哈希映射中获取值,我们首先对键进行哈希计算,以确定该值将位于哪个存储桶中。然后,我们必须将要搜索的键与存储桶中的所有键进行比较。

我们通过散列最小化了这个搜索步骤,这也是 murmur3 进行速度优化的原因。哈希函数越快,我们找到合适的存储桶进行搜索的速度就越快,哈希映射的整体速度就越快。

这也是为什么减少碰撞如此重要的原因。如果我们确实决定使用本文开头始终返回 0 的虚拟哈希函数,我们会将所有键值对放入第一个存储桶中。找到任何东西可能意味着我们必须检查哈希映射中的所有值。有了好的散列函数和良好的分布,我们就可以将搜索量减少到 1/N,其中 N 是桶的数量。 让我们看看 stringSum 是如何做的。

alt

有趣的是, stringSum 似乎可以很好地分配值。您会注意到一种模式,但整体分布看起来不错。

没那么快,哈斯基。我们需要讨论一个严重的问题。这些连续数字的分布看起来不错,但我们已经看到 stringSum 没有良好的雪崩效应。这结局并不好。

现实世界的碰撞

让我们看一下 2 个现实世界的数据集:IP 地址和英语单词。我要做的是获取 100,000,000 个随机 IP 地址和 466,550 个英语单词,使用 murmur3 和 stringSum 对所有这些进行哈希处理,然后看看我们得到了多少次冲突。

alt

当我们真正使用哈希映射时,我们通常不会在其中存储随机值。我们可以想象计算我们在服务器的速率限制代码中看到某个 IP 地址的次数。或者通过代码计算历史上书籍中单词的出现次数,以跟踪它们的起源和受欢迎程度。 stringSum 对于这些应用程序来说很糟糕,因为它的冲突率极高。

人为制造的碰撞

现在轮到 murmur3 带来一些坏消息了。我们需要担心的不仅仅是输入相似性引起的冲突。看一下这个。

alt

这里发生了什么事?为什么所有这些乱码字符串都会散列到相同的数字?

我对 141 万亿个随机字符串进行哈希处理,以找到在使用 murmur3 时哈希到数字 1228476406 的值。哈希函数必须始终为特定输入返回相同的输出,因此可以通过强力查找冲突。

是的,我只花了 25 分钟。计算机速度很快。

如果您的软件根据用户输入构建哈希映射,那么很容易发生冲突的坏人可能会造成毁灭性的后果。以 HTTP 标头为例。 HTTP 请求如下所示:

GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: google.com

您不必理解所有单词,只需了解第一行是所请求的路径,所有其他行都是标头即可。标头是键:值对,因此 HTTP 服务器倾向于使用映射来存储它们。没有什么可以阻止我们传递我们想要的任何标头,因此我们可以非常刻薄地传递我们知道会导致冲突的标头。这会显着降低服务器速度。

这也不是理论上的。如果您搜索“HashDoS”,您会发现更多这样的示例。这在 2000 年代中期确实是一件大事。 有几种方法可以缓解 HTTP 服务器特有的这种情况:例如,忽略乱七八糟的标头键并限制您存储的标头数量。但像 murmur3 这样的现代哈希函数提供了一种更通用的解决方案:随机化。

在本文前面,我们展示了一些哈希函数实现的示例。这些实现采用一个参数:输入。许多现代哈希函数都采用第二个参数:种子(有时称为盐)。在 murmur3 的例子中,这个种子是一个数字。

到目前为止,我们一直使用 0 作为种子。让我们看看当我们使用种子 1 时我收集到的碰撞会发生什么。

alt

就这样,0比1,碰撞就消失了。这就是种子的目的:它以不可预测的方式随机化哈希函数的输出。它如何实现这一点超出了本文的范围,所有哈希函数都以自己的方式实现这一点。

对于相同的输入,哈希函数仍然返回相同的输出,只是输入是输入和种子的组合。与一颗种子发生碰撞的物体在使用另一颗种子时不应发生碰撞。编程语言通常会在进程启动时生成一个随机数用作种子,因此每次运行程序时种子都是不同的。作为一个不知道种子的坏人,我现在不可能可靠地造成伤害。

如果您仔细观察上面的可视化和之前的可视化,您会发现它们是被散列的相同值,但它们产生不同的散列值。这意味着,如果您使用一个种子散列一个值,并且希望将来能够与它进行比较,则需要确保使用相同的种子。

不同种子具有不同的值不会影响哈希映射用例,因为哈希映射仅在程序运行期间有效。如果您在程序的生命周期中使用相同的种子,您的哈希映射将继续正常工作。如果您曾经将哈希值存储在程序之外(例如文件中),则需要小心了解使用的种子。

总结

我们已经介绍了哈希函数是什么、衡量它好坏的一些方法、它不好时会发生什么,以及它们可能被坏人破坏的一些方法。

哈希函数的范围很广,在这篇文章中我们实际上只触及了表面。我们还没有讨论加密与非加密散列,我们只触及了散列函数的数千个用例中的一个,并且我们还没有讨论现代散列函数实际上是如何工作的。

Reference

[1]

Source: https://samwho.dev/hashing/

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/811055.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

生命在于学习——APP渗透学习笔记

一、app渗透篇 1、Android 简介 自从 Android 被谷歌收购(2005 年),谷歌已经完成了整个开发,在过去的 9 年里,尤其是在安全方面,有很多变化。 现在,它是世界上最广泛使用的智能手机平台&#…

代码、低代码、无代码开发触手可及的低代码平台源码

基于moleculer微服务架构开发的低代码平台源码,代码、低代码、无代码开发触手可及。 一、低代码平台系统功能 【公司信息】 管理员可通过页面顶部设置菜单或者应用程序中设置应用进入到后台设置页面。 在公司信息页面可进行基础信息修改,启用用户自助…

我在CSDN创作的第五十天

这篇文章主要是写给自己的,是对自己现在阶段的一个认识,6月10号,我在CSDN发布了第一篇文章,距离现在不多不少,刚刚好是50天,期间创作的都是C语言的一些内容,我创作的文章也都是我现在所学的知识…

Django系列之DRF简单使用

基于ModelViewSets的简单使用 models.py from django.db import modelsclass AuthorDetail(models.Model):gender models.CharField(max_length8)birthday models.DateField()telephone models.BigIntegerField()addr models.CharField(max_length64)class Author(models…

手机的python怎么运行文件,python在手机上怎么运行

大家好,小编来为大家解答以下问题,手机上的python怎么运行程序,手机的python怎么运行文件,今天让我们一起来看看吧! 1、python程序怎么在手机上运行 python语言应用很广泛,自己也很喜欢使用它,其…

自学网络安全(黑客)入门

自学网络安全入门可以按照以下步骤进行: 确定学习目标:网络安全是一个广泛的领域,包括密码学、网络防御、漏洞利用等方面。确定自己想要学习的具体方向,可以更好地规划学习路线。 学习基础知识:网络安全的基础知识包括…

Leetcode周赛 | 2023-7-30--我真是个废物

2023-7-30--我真是个废物 题1体会我的代码 题2体会我的代码 题3体会我的代码 题1 体会 根本没想到用双指针。原因是,没想到还要用一个字典去维护子数组中各个数字的出现频次,以及出现频次不小于1 (也就是大于0) 的数字个数。 这里的双重循环也很巧妙&am…

牛客 排座椅(贪心)

上课的时候总有一些同学和前后左右的人交头接耳,这是令小学班主任十分头疼的一件事情。不过,班主任小雪发现了一些有趣的现象,当同学们的座次确定下来之后,只有有限的D对同学上课时会交头接耳。 同学们在教室中坐成了 M 行 N 列&…

【每日一题Day281】LC142链表 Ⅱ| 快慢指针 哈希表

环形链表 Ⅱ【LC142】 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使…

用JavaScript和HTML实现聊天页面和功能(超详细)

文章目录 🚀一、介绍🚀二、开始编码🔎2.1 创建一个HTML文件🔎2.2 编写样式🔎2.3 完善聊天页面🔎2.4 编写按钮逻辑🔎2.5 测试聊天效果🔎2.6 优化对话显示🔎2.7 设置一个自…

Cesium地形裁剪

基于纹理映射实现的裁剪,反裁剪也可以。比判断点是否在多边形内裁剪性能要好的多,缺点是在边缘锯齿感会比较明显。 3DTileset 的裁剪也和这样一样用纹理映射实现了,效果一样

单链表的多语言表达:C++、Java、Python、Go、Rust

单链表 是一种链式数据结构,由一个头节点和一些指向下一个节点的指针组成。每个节点包含一个数据元素和指向下一个节点的指针。头节点没有数据,只用于表示链表的开始位置。 单链表的主要操作包括: 添加元素:在链表的头部添加新…

【指针二:穿越编程边界的超能力】

本章重点 5. 函数指针 6. 函数指针数组 7. 指向函数指针数组的指针 8. 回调函数 9. 指针和数组面试题的解析 五、函数指针 首先看一段代码: 输出的是两个地址相同,这两个相同的地址都是 test 函数的地址。 那我们的函数的地址要想保存起来,怎…

LeetCode63. 不同路径 II

63. 不同路径 II 文章目录 [63. 不同路径 II](https://leetcode.cn/problems/unique-paths-ii/)一、题目二、题解方法一:二维数组动态规划方法二:一维数组动态规划 一、题目 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “…

【Linux】线程互斥 -- 互斥锁 | 死锁 | 线程安全

引入互斥初识锁互斥量mutex锁原理解析 可重入VS线程安全STL中的容器是否是线程安全的? 死锁 引入 我们写一个多线程同时访问一个全局变量的情况(抢票系统),看看会出什么bug: // 共享资源, 火车票 int tickets 10000; //新线程执行方法 vo…

rtthread的idle线程不应该长时间堵塞

RT-Thread是一个实时嵌入式操作系统,它的空闲线程(Idle Thread)是在系统中没有其他任务需要执行时运行的线程。空闲线程通常用于执行一些低优先级的任务或者进行系统的休眠等操作。 RT-Thread的空闲线程不能在线程中堵塞的原因是为了确保系统…

将多张图片拼接成图像矩阵图

用Python实现将12张图片排列成n*m的图像矩阵图 目录 引言环境准备代码实现效果演示总结 引言 在图像处理和图像展示的应用中,将多张图片排列成一个图像矩阵图是一个常见的需求。本博客介绍如何使用Python实现将12张图片排列成n*m的图像矩阵图。 环境准备 为了实…

【动态规划刷题 2】不同路径不同路径 II

不同路径 62 . 不同路径 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 问总共有多少条不同的…

iPhone 安装 iOS 17公测版(Public Beta)

文章目录 步骤1. 备份iPhone资料步骤2. 申请iOS 17 公测Beta 资格步骤3. 下载iOS 16 Beta 公测描述档步骤4. 选择iOS 17 Beta 公测描述档更新项目步骤5. 升级iOS 17 Public Beta 公开测试版 苹果已经开始向大众释出首个iOS 17 公开测试版/ 公测版( iOS 17 Public Beta)&#xf…

【C++】类和对象 - 上

目录 1. 面向过程和面向对象初步认识2. 类的引入3. 类的定义4. 类的访问限定符及封装4.1 访问限定符4.2 封装 5. 类的作用域6. 类的实例化7. 类对象模型7.1 如何计算类的大小7.2 类对象的存储方式猜测7.3 结构体内存对齐规则 8. this指针8.1 引出8.2 this指针的特性 总结 1. 面…