数据结构与算法系列二十三(哈希算法)

160 阅读7分钟

1.考考你

实际项目开发,你们的项目中是不是经常要保存用户信息。比如用户注册,关键信息用户名称和用户密码。那么你还记得用户密码,你都是如何处理的吗?

如果你做过大数据的开发,你一定知道在MapreReduce的shuffle阶段,有一个步骤是分区,默认策略是:HashPartition,即哈希分区。

如果你熟系分布式系统,你一定经常听到这么几个词语:负载均衡、数据分片、分布式存储等等。

如果你不是一个开发人员,你说以上你都不知道,你的日常生活就是吃吃吃、玩玩玩。那也不要紧,你一定用过某某某BT工具,下载过很多的movies对吧。

以上种种,都跟我们今天的主角有关系,它就是:哈希算法

#考考你:
1.你知道什么是哈希算法吗?
2.你知道哈希算法的应用场景都有哪些吗?
3.你知道为什么一定会有哈希冲突吗?

2.初识哈希算法

2.1.你熟悉的案例

我们先用一个你熟悉的案例,回顾一下在实际项目中,你经常用到哈希算法的场景。该案例是用户注册,通过md5加密用户密码的案例。你一定很熟悉有没有。

2.1.1.md5工具类

package com.anan.algorithm.hash;
​
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
​
/**
 * md5加密工具类
 */
public class MD5Util {
​
    /**
     * 普通加密方式:将明文密码,通过md5加密
     * @param password
     * @return
     */
    public static String stringToMD5(String password) {
        byte[] secretBytes = null;
        try {
​
            // 获取md5摘要算法
            MessageDigest md5 = MessageDigest.getInstance("md5");
​
            // 加密,获取哈希值(二进制数组)
            secretBytes = md5.digest(password.getBytes());
            System.out.println("md5哈希值长度(请记住这个长度值):" + secretBytes.length);
​
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("没有获取到对应的算法!");
        }
        // 将哈希值,转换成十六进制
        String md5code = new BigInteger(1, secretBytes).toString(16);
        // 如果长度不足32位,补0统一为32位
        for (int i = 0; i < 32 - md5code.length(); i++) {
            md5code = "0" + md5code;
        }
​
        return md5code;
​
    }
​
    /**
     * 结合盐值方式:将明文密码,通过md5加密
     * @param password
     * @param salt
     * @return
     */
    public static String stringToMD5(String password,String salt) {
        byte[] secretBytes = null;
        try {
​
            // 获取md5摘要算法
            MessageDigest md5 = MessageDigest.getInstance("md5");
​
            // 加密,获取哈希值(二进制数组)
            /**
             * 特殊说明:
             *  1.案例处理比较简单,直接将盐值,追加到密码的结尾部分
             *  2.实际项目中我们可以考虑:
             *      2.1.盐值是随机数(每个用户密码对应的盐值不一样)
             *      2.2.盐值,与真实密码按照某种规律混合
             */
            password = password + salt;
            secretBytes = md5.digest(password.getBytes());
            System.out.println("md5哈希值长度(请记住这个长度值):" + secretBytes.length);
​
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("没有获取到对应的算法!");
        }
        // 将哈希值,转换成十六进制
        String md5code = new BigInteger(1, secretBytes).toString(16);
        // 如果长度不足32位,补0统一为32位
        for (int i = 0; i < 32 - md5code.length(); i++) {
            md5code = "0" + md5code;
        }
​
        return md5code;
​
    }
}

2.1.2.加密用户密码

package com.anan.algorithm.hash;
​
/**
 * 哈希案例
 */
public class HashDemo {
​
    public static void main(String[] args) {
        // 1.获取用户信息
        String username = "小明";
        String passwordStr = "123456";
        System.out.println("1.用户小明的密码明文是:【" + passwordStr + "】");
​
        // 2.通过md5普通方式加密
        String md5Password1 = MD5Util.stringToMD5(passwordStr);
        System.out.println("2.通过md5普通方式加密,小明的密码变成了:【" + md5Password1 + "】");
​
        // 3.通过md5结合盐值方式加密
        String salt = "hehe";
        String md5Password2 = MD5Util.stringToMD5(passwordStr,salt);
        System.out.println("3.通过md5结合盐值方式加密,小明的密码变成了:【" + md5Password2 + "】");
    }
}

2.1.3.执行结果

D:\02teach\01soft\jdk8\bin\java 
    com.anan.algorithm.hash.HashDemo
1.用户小明的密码明文是:【123456】
md5哈希值长度(请记住这个长度值):16
2.通过md5普通方式加密,小明的密码变成了:【e10adc3949ba59abbe56e057f20f883e】
md5哈希值长度(请记住这个长度值):16
3.通过md5结合盐值方式加密,小明的密码变成了:【0f37da9922c1ff4ad808fc9775f1729】
​
Process finished with exit code 0

2.2.关于哈希算法

【2.1.节】的案例,是哈希算法应用较多的其中一个场景,而且你一定很熟悉对吧。那么接下来我们一起来看一下,到底什么是哈希算法?它都有什么特点?

#哈希算法定义:
1.哈希算法,是一种规则(废话,算法都是规则好吧)
2.那么哈希算法是什么样的规则呢?
3.它是将【任意长度】的二进制值串,映射为【固定长度】的二进制值串
4.关键词:
    明文:任意长度
    密文:固定长度
5.你说密文,即哈希值是固定长度,多长呢?
6.你还记得案例执行结果,我说过:
    md5哈希值长度(请记住这个长度值):16
7.也就是说哈希值的长度固定是:16个字节
​
8.总结:以上就是哈希算法的定义,你只要关注第3条就可以了
​
#哈希算法特点:
1.不可逆,从哈希值不能反向推导出原始值
    --你是不是经常听到md5是不可逆的
2.对输入数据敏感,哪怕只修改了原始数据一个bit,得到的哈希值也不同
    --123、与1234明明是亲兄弟
    --但是它们的哈希值却相差十万八千里
3.散列冲突概率要很小,对于不同的原始数据,哈希值相同的概率很小
    --这条好熟悉,学习散列表的时候,也有散列冲突
    --因为散列函数,是哈希算法的其中一个应用场景
4.哈希算法执行效率要很高,针对较长的文本,能快速计算出哈希值
    --世间万事万物,都是在寻求某种平衡
    --关于哈希算法,也是在安全,与性能之间寻求平衡
​
#哈希算法常见应用场景:
1.安全加密
2.唯一标识
3.数据校验
4.散列函数
5.负载均衡
6.数据分片
7.分布式存储
​

2.3.关于哈希冲突

在学习散列表的时候,我们说过不能避免散列冲突,也就是说散列冲突一定存在。我们知道散列函数,是哈希算法的其中一个应用场景。那么借助哈希算法这一节,我一起来搞明白为什么一定会存在哈希冲突

我们先分享一个故事,看完故事后,你就明白了。该故事有一个好听的名字:鸽巢原理(或者叫做抽屉原理)

#关于鸽巢原理:
1.想象一下,有10个鸽巢,有11只鸽子
2.要求每只鸽子,都要住进鸽巢休息
3.是不是总有一个鸽巢,至少要住进两只鸽子的情况发生
4.这就是我们说的哈希冲突的情况
​
#分析哈希冲突的本质:
1.你还记得,哈希算法的定义吗
2.将【任意长度】的二进制值串,映射为【固定长度】的二进制值串
3.你还记得,案例执行中,我说要记住的【固定长度】吗
4.md5哈希值长度(请记住这个长度值):16字节
​
5.也就是说针对哈希算法,密文长度是固定16个字节,128 个bit
6.1个字节=8 bit(比特),你一定还记得吧
​
7.那么回到哈希算法的定义,它有两个空间:
  7.1.明文空间:任意长度(空间无限)
  7.2.密文空间:128 bit(空间有限)
  7.3.从无限空间,映射到有限空间
  7.4.不就是11只鸽子,10个鸽巢的问题吗
  7.5.也因此,我们说哈希冲突是一定会存在的
  7.6.当然在实际应用中,我们无需过多担心
  7.7.因为通常2^128空间,足够我们使用了