朋友们好,我是你们的朋友mCell
今天想跟大家聊一个在计算机世界里几乎无处不在,但又总让人感觉有点"不明觉厉"的概念------哈希(Hash) 。我会用一个你绝对想不到的厨房电器,带你走进哈希的世界。
榨汁机:哈希算法的"祖师爷"
想象一下,你有一台超屌的榨汁机。
不管你往里面扔的是一个苹果、一根香蕉,还是一把菠菜,甚至是(如果你有钱任性的话)一整颗榴莲,这台榨汁机"咔咔咔"一顿操作,最后出来的都是啥?
一杯果汁。
对吧?这杯果汁有几个特点:
- 定长输出:不管你扔进去的水果有多大、多复杂,出来的果汁总是一杯。你扔一个樱桃是一杯,扔一个西瓜也是一杯(假设杯子够大)。
- 不可逆性:我给你一杯混合果汁,你能完美地把里面的苹果、香蕉、菠菜再给我变回去吗?Looking my eyes,显然不能!
- 确定性:只要你扔进去的东西完全一样(比如都是两个特定品种的苹果和半根香蕉),那么榨出来的果汁,味道、颜色、浓度也一定是完全一样的。
好了,恭喜你,你已经理解了哈希最核心的思想!
计算机里的哈希函数(Hash Function) ,就是这么一台"信息榨汁机"。
哈希的核心概念:将任意长度的输入(Input),通过一个特定的算法(哈希函数),转换成一个固定长度的输出(Output),这个输出就是"哈希值"或"摘要"(Digest)。
这个过程就像榨汁,把复杂多变的信息,压缩成一个简短、固定长度的"指纹"。
哈希在计算机世界的"高光时刻"
理解了基本概念,咱们来看看这台"榨汁机"在计算机科学里是怎么大放异彩的。
1. 网站用户密码保护:给你的密码"易容"
你以为你注册网站时,你的密码"123456"就原封不动地躺在人家数据库里吗?要是这样,那可就太"裸奔"了!一旦数据库泄露,所有用户的密码都一览无余。
负责任的网站,后台存的其实是你密码的"哈希值"。
当你登录时,系统把你输入的密码用同样的哈希算法再"榨一遍汁",然后比较两次的"果汁"(哈希值)是不是一样。如果一样,登录成功!
这样一来,即使黑客拖走了数据库,他也只能看到一堆乱码一样的哈希值,无法反推出你的原始密码。这就是哈希的不可逆性在起作用。
用 Go 语言简单示意一下这个过程(这里用 bcrypt
库,它是专门为密码哈希设计的,能自动加盐,更安全):
go
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
password := "mySuperSecretPassword123"
// 1. 当用户注册时,我们将密码哈希化
// `bcrypt.GenerateFromPassword` 会自动加"盐",增加破解难度
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
panic(err)
}
fmt.Println("存储在数据库的哈希值:", string(hashedPassword))
// 2. 当用户登录时,我们比较输入的密码和存储的哈希值
// 用户输入的密码是 "mySuperSecretPassword123"
loginAttempt := "mySuperSecretPassword123"
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(loginAttempt))
if err == nil {
fmt.Println("密码正确,登录成功!")
} else {
fmt.Println("密码错误!")
}
}
2. 文件完整性校验:下载的学习资料完整吗?
你从网上下载一个很大的文件,比如一个操作系统镜像。你怎么知道在下载过程中,文件没有损坏,或者没有被中间人篡改呢?
发布者通常会提供一个叫做 SHA256 或 MD5 的字符串,这就是文件的哈希值。
你下载完文件后,在本地用同样的哈希算法(比如 SHA256)也给文件"榨个汁",看看你得到的哈希值和官方提供的是不是一模一样。如果一个字节都不差,那就说明文件安然无恙,可以放心使用。这就是哈希的确定性在守护数据。
用 Go 语言计算一个字符串的 SHA256 值:
go
package main
import (
"crypto/sha256"
"fmt"
)
func main() {
data := "这是一段重要的学习资料,一个字节都不能错!"
// 创建一个新的SHA256哈希对象
hasher := sha256.New()
// 写入数据
hasher.Write([]byte(data))
// 计算哈希值
hashValue := hasher.Sum(nil)
// 以十六进制格式打印
fmt.Printf("数据的SHA256哈希值: %x\n", hashValue)
}
终极进化:哈希表(Hash Table)------ 告别"夺命连环call"
好了,终于到了我们的重头戏------哈希表。这可能是哈希思想最牛、最广泛的应用了。
在编程中,我们最常用的数据结构之一是数组(Array) 。数组就像一个有编号的储物柜,东西都按顺序放好。
但数组有个烦人的问题:如果你想找某个东西,但你不知道它的编号,你该怎么办?
只能一个一个柜子打开找。
比如,我想在一个存了一亿个用户信息的数组里,找到名叫"张三"的用户。最坏的情况下,我可能要从第一个找到第一亿个,这简直是"夺命连环call",效率太低了!
这时候,**哈希表(Hash Table / Map)**闪亮登场!
哈希表说:"别一个个找了,我直接告诉你'张三'在哪!"
它是怎么做到的?其实,哈希表的底层骨架,依然是一个数组。但它多了一个神奇的"引路人"------哈希函数。
当你往哈希表里存东西时,比如 (key: "张三", value: "用户数据...")
,它会这样做:
- "榨汁" :对
key
("张三")进行哈希运算,得到一个哈希值(比如2857399
)。 - "找位置" :用这个哈希值通过一个取模运算(比如
2857399 % 数组长度
)得到一个数组的索引(比如7
)。 - "放东西" :把
value
("用户数据...")直接存到数组索引为7
的位置。
看到了吗?当我们想找"张三"的数据时,只需要重复一遍上面的"榨汁"和"找位置"操作,就能瞬间定位到数组的 7
号索引,直接把数据取出来!根本不需要遍历!
这种"指哪打哪"的特性,让哈希表的查找、插入、删除操作的平均时间复杂度达到了惊人的 O(1) ,也就是"常数时间",几乎不随数据量的增减而变化。
在 Go 语言里,这个牛X的数据结构就是内置的 map
。
go
package main
import "fmt"
func main() {
// 创建一个哈希表(map),key是string,value是string
// 想象成一个电话簿
phoneBook := make(map[string]string)
// 存储键值对,就像把联系人存进电话簿
phoneBook["张三"] = "13800138000"
phoneBook["李四"] = "13900139000"
phoneBook["王五"] = "13700137000"
phoneBook["mCell"] = "0000000001" // (●'◡'●)
// 查找"mCell"的电话
// Go 会在底层对 "mCell" 这个key进行哈希
// 然后快速定位到数据位置
mCellPhone, found := phoneBook["mCell"]
if found {
fmt.Println("找到了!mCell的电话是:", mCellPhone)
} else {
fmt.Println("没找到这个人。")
}
// 这操作几乎是瞬时的,不管电话簿里有4个人还是400万人
}
当然,你可能会问:"如果不同的 key(比如'李逵'和'李鬼')'榨汁'后算出的数组位置一样怎么办?" 这个问题叫做哈希冲突(Hash Collision) ,这确实是个问题,虽然发生的概率如找到两颗相同的沙砾,但聪明的计算机科学家们为了防止这种事件发生也设计了很多解决方案,比如"链地址法"(在那个位置上挂一个链条,把冲突的人都串上去)或者"开放寻址法"(在这个位置被人占了?那我往后找个空位),这里就不展开了,因为我还没研究(bushi
参考文献
- The Go Programming Language. (n.d.). Go maps in action . Retrieved from go.dev/blog/maps (Go语言官方博客中关于 map 实现的深入介绍)