算法杂谈 : 学一学 Hash

1,879 阅读11分钟

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

之前了解 MySQL 索引的时候 , 看到说 Hash 索引相对于 B-Tree 索引可以直接定位到数据 , 更快 .

由此我产生了一个疑问 , Hash 是如何生成 , 又是如何寻址的 ?

针对这个疑问 , 我试着重新学习了一遍 Hash.

二 . Hash 原理

2.1 Hash 简述

目的 :

通过 Hash 算法将一组字符压缩到固定长度 , 由于计算机上的每个文件最终都只是可以以二进制形式表示的数据,哈希算法可以对这些数据进行复杂的计算,并输出一个固定长度的字符串作为计算结果

用途 : 信息摘要 , 文件校验和 , 完整性 , 安全校验 , 信息加密 , 文件索引 , 文件历史判断

基础用法 Hash 最基础的,也是最常见的用法就是配合数组的下标索引

image.png

针对数据列 11,12,13,14,15 , 使他们对 10 进行取模 , 就能得到 {1,2,3,4,5} , 从而将分别存储在数组或哈希表中的位置{1,2,3,4,5}

Hash 冲突 及 Hash 函数 Hash 冲突: 同一对象只会有一个 Hash 结果 , 不同对象可能会有同一个 Hash 结果

例如上面的 21 , 22 ,23 , 同样可以取模到 1,2,3 , 这就是 Hash 冲突 .

Hash 冲突可以通过 Hash 算法和数据结构进行解决 , 常见的解决方案有 1)分离链接法; 2)开放地址法 , 后文详细看看

2.2 Hash 算法案例 - MD5

Hash 函数即为 Hash 算法 ,通过该算法 , 业界最常见的 Hash 算法为 MD5 , 这是一个很典型的案例 , 也凸显了 Hash 算法的要点 , 那么就从 MD5 来分析一下 , Hash 算法的实现方式 .

推荐一篇原创# MD5算法原理及实现

// MD5 的功能 : 
为每个输入值生成一个固定长度为128位的字符串,并使用标准的单向操作在几个循环中计算确定的输出值

// MD5 算法的要点 : (PS > 详细过程可以看上面那一篇文档)
- Step 1 : 数据填充
- Step 2 : 添加消息长度
- Step 3 : 初始化变量
- Step 4 : 数据处理


// 名词 : 大端和小端
在内存中存储这两个字节有两种方法:一种是将低序字节存储在起始地址,这称为小端(little-endian)字节序;另一种方法是将高序字节存储在起始地址,这称为大端(big-endian)字节序

整体流程文档里面也很清楚了 , 这里通过一个流程图让我们的思路更清晰 :

Step 1 和 Step 2 : 数据填充与长度补充

image.png

Step 2 : 数据计算

image.png

Step 3 : 循环处理后整合

image.png

可以看到 , Hash 得结果是一套标注的数和一套相同的运算方式 , 将数据进行压缩得到的 , 由多到少 , 就必然会出现冲突

MD5 现状

了解到 MD5 的原理后 , 也不难理解 MD5 的现状了 , MD5 已经不再是无法破解的了 . 固定长度就意味着会有碰撞 , 而 MD5 经历了如此之长的时间轴 , 通过以往历史进行穷举 , 是可以破解出对应的源数据的.

三. Hash 离散函数的演进

对于 MD5 的穷举现状 , 还可以选择更多的 Hash 函数

// MD2 ,MD3 , MD4 , MD5
- MD5即Message-Digest Algorithm 5(信息-摘要算法5)
- 固定输出 128// 安全哈希算法(Secure Hashing Algorithm,SHA1)
最早提出的标准,将输出值的长度固定在 160 位 , 相对于 MD5 , SHA1 主要是增加了输出长度 , 单向操作的次数和复杂性 , 并不能真正的解决攻击问题

// SH2
- SHA-2 SHA-224、SHA-256、SHA-384,和SHA-512

// SHA3 : 基于 sponge construct 的算法
- 使用随机排列来吸收并输出数据,同时为将来用于哈希算法的输入值提供随机性


// SHA256
- 特币协议(的工作量证明)需要重复运行两遍 SHA256 算法 , 旨在抵御长度扩展攻击

// RIPEMD-160 :
- 160 位加密哈希函数

// CRC系列


// Mac : 消息认证码-Message authentication code
- 加上密钥作为消息一部分的Hash函数




针对结果 , 可以看看这个网站 Hasj 在线计算

Hash 函数分类

// 加法 Hash

// 位运算 Hash

// 乘法 Hash

// 除法 Hash

// 查表 Hash

四 . HashMap 为什么叫 HashMap ?

那么 HashMap 是如何实现自己的 Hash 的呢 ? 在查找的时候又是如何查找的呢 ?

从 HashMap 的源码文档中说了这样一句话 , HashMap 实现为基本操作(get和put)提供恒定时间的性能 , 散列函数将元素适当地分散到桶中。对集合视图的迭代需要与HashMap实例的“容量”(桶的数量)加上它的大小(键-值映射的数量)成比例的时间.

那么 , HashMap 是如何实现这个的呢 ?

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        // 注意此处的 >>> first = tab[(n - 1) & hash]
        (first = tab[(n - 1) & hash]) != null) {
        
        // 如果根据hash定位到数组位置的第一个元素就是要查找的元素,则直接返回。此时为O(1)
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                
            // 从这里可以看出 , 实际上还是通过循环去查找对应的 Hash 是否匹配    
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

所以 , Java 中对 hash 的使用主要是通过数组 + Hash 下标来实现 O(1) 的时间复杂度处理.

来看一下 Object 对象中 HashCode 的相关源码 :

// s[0]*31^(n-1) + s[1]* 31^(n-2) + … + s[n-1]
// 0-n 表示字符串第一个字符到最后一个字符
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

名词解析 : 二次扰动和泊松分布

相信大多数人都对这2个概念有所耳闻 , HashMap 的二次扰动和泊松分布又是为了解决什么问题

泊松分布 :

提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小

可以看一下这篇文章 : blog.csdn.net/ccnt_2012/a…

简单点说 , 就是既能不浪费空间 , 又可以避免空间不够.

二次扰动 :

这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的

二次扰动又叫双哈希法 , 还有其他的方式例如 : (hash1(key) + i * hash2(key)) % TABLE_SIZE , 其中hash1()和 hash2()是哈希函数 , TABLE _ size 是哈希表的大小 .

当 Hash 冲突较大的时候 , 可以通过增加 i 来解决.

下面看一下解决Hash冲突得系统方法

分离链接法

链接 : 其思想是使哈希表的每个单元格指向具有相同哈希函数值的记录链表

简单点说 , 就是相同 Hash 值得归纳到一个链表中 (PS : 后续还可以扩展到红黑树等等 , 具体看 HashMap 相关知识点)

// 优点 : 
1)易于实现
2)哈希表永远不会填满
3)对散列函数或加载因子不太敏感
4)主要用于不知道插入或删除密钥的数量和频率的情况

// 缺点 : 
1)链接的缓存性能不好,因为键存储使用链表。
2)空间浪费(哈希表的某些部分从未使用)
3)如果链变长,那么在最坏的情况下搜索时间可以变成 o (n)(PS : 需要一直向内索引)
4)为链接使用额外的空间


开放寻址法

在开放地址中,所有元素都存储在散列表本身中。因此,在任何时候,表的大小必须大于或等于键的总数

简单点说 , 开放寻址就类似于多次处理 ,直到找到空槽 :

If slot hash(x) % S is full, then we try (hash(x) + 1) % S
If (hash(x) + 1) % S is also full, then we try (hash(x) + 2) % S
If (hash(x) + 2) % S is also full, then we try (hash(x) + 3) % S 

// PS : 这里会涉及到一个问题 , search 如何判断 >
- Search(k) : 当查找一个元素时,要检查所有的表项,直到找到所需的元素,或者最终发现元素不在表中
- Delete (k) : 删除键的插槽被特别标记为“删除”。插入操作可以在已删除的插槽中插入项,但搜索不会在已删除的插槽中停止。

image.png

相关参考文档 www.geeksforgeeks.org/hashing-dat…

五 . MySQL 的 Hash 索引又是个什么索引?

了解了 HashMap , 再来看一下 MySQL 的 Hash 索引 ,它又是如何处理的呢 ?

先来看一下 Hash 索引的概念 :

// MySQL 对 Hash 索引的支持程度 :
MyISAM 本身是不支持 Hash 索引的 , 而 InnerDB 实际上也不是一个完全的 Hash 索引 
- 它是根据 B-Tree 进行的自建 , 是一个自适应哈希.
- 也就是说 , InnerDB 实际上不存在 Hash 索引 , 这个在 MySQL 8.0 官方文档中也有体现 :

+----------------+--------------------------------+
| Storage Engine |    Permissible Index Types     |
+----------------+--------------------------------+
| MyISAM         | BTREE                          |
| InnoDB         | BTREE                          |
| MEMORY/HEAP    | HASH, BTREE                    |
| NDB            | BTREE, HASH (see note in text) |
+----------------+--------------------------------+


// 特点 : 
- 精准匹配才会真实的使用到 Hash
- 每一个索引单元 , 都会计算出一个 Hash 值
- hash索引包括键值、hash码和指针
- Hash索引的结构十分紧凑 , 速度很快

// 缺点 : 
- Hash 索引存在 Hash 冲突 , Hash 索引不能建立在重复性很多的列上
- Hash 索引存放的是 Hash 值 , 故需要二次查找(先查对应行 , 再查对应数据)才能定位到最终对象
- Hash 索引不能进行外排序
- Hash 索引只支持精确查找

可以看到 , 特点和缺点也大部分是 Hash 的特性 . 那么 MySQL 是如何处理 Hash 的 呢 ?

MySQL InnerDB 中的 Hash 索引为自适应哈希索引 , 先看下官方文档 自适应Hash

自适应哈希索引使 InnoDB 能够在缓冲池具有适当工作负载和足够内存组合的系统上执行更像内存数据库/服务器的操作而不会牺牲事务特性或可靠性。

自适应哈希索引由 innodb_adaptive_hash_index 变量启用,或者在服务器启动时通过:
--skip-innodb-adaptive-hash-index 关闭

// 自适应索引的特点 : 
- 使用索引键的前缀构建哈希索引
- 自适应 Hash 可以讲热点数据建立到 Hash 表中


六 . Hash 在安全领域的作用

之前说过哈希是不可逆的,因此仅仅通过哈希算法和文件哈希的结果是不能重建文件的内容的。但是,它可以0在不知道两个文件的内容的情况下确定两个文件是否相同。

早期的反病毒软件

传统的反病毒解决方案完全依赖散列值来确定一个文件是否是恶意的,而不检查该文件的内容或行为 , 通过保持一个内部数据库的散列值属于已知的恶意软件来做到这一点。在扫描系统时,AV 引擎为用户机器上的每个可执行文件计算一个散列值,并测试其数据库中是否有匹配。

但是相对而言 , 这种方式也很容易被破解 , 哪怕相差一个字符 , 整体的 Hash 也有很大差距

签名和摘要

签名和摘要是 Hash 最常用的操作 , 可以有效的确定一个文件、网站或下载是真实的 , 同时还可以应用在证书和 SSL 中

Hash 搜索

由于 Hash 的长度是固定的 , 可以通过保存文件 Hash 来实现文件历史 , 判断文件是否存在或者在过去是否存在

散列索引数据 , 哈希值可用于将数据映射到哈希表中的单个"桶"。每个桶都有一个惟一的 ID,作为指向原始数据的指针。这将创建一个比原始数据小得多的索引,从而可以更有效地搜索和访问值

密码加密

对密码进行不可解得加密 , 校验得时候按照同样的方式进行加密即可

区块链

比特币和以太坊都对 Hash 由一定的应用

矿工的注意力集中在一连串的数字上。这个数字被追加到前一个块的哈希内容之后,然后对该内容进行哈希处理。如果这个新散列小于或等于目标散列,那么它就被接受为解决方案,矿工得到奖励,块被添加到区块链 , 块链事务的验证过程依赖于使用算法哈希加密的数据。

总结

至此对 Hash 有了一个初步的了解 , 文章不深 , 还有点水 , 尽量把相关知识点进行了整理和学习 , 期望对别人也有所帮助

参考文档 :

blog.csdn.net/z_ryan/arti…

zhuanlan.zhihu.com/p/106941474

fangjian0423.github.io/2016/03/12/…

segmentfault.com/a/119000000…