参考文章:哈希表(Hash Table)实现原理(哈希映射HashMap、哈希函数(取余法、乘法法、加法法)、索引bucket桶、哈希冲突、链地址法、开放地址法(线性探测法)、负载因子、哈希表扩容)
文章目录
Rust字典(Rust HashMap、Rust BTreeMap)
在 Rust 中,常见的字典类型是 HashMap
和 BTreeMap
,它们都属于标准库中的 std::collections
模块。它们主要的区别在于底层实现和使用场景。下面是这两种类型的详细解释:
1. HashMap
- 实现
基于哈希表(hash table)实现,键通过哈希函数映射到不同的桶中。
- 特性
1. 快速查找:平均 O(1) 的时间复杂度,适用于需要频繁插入、删除和查找键值对的场景。
2. 无序:键值对的顺序不保证,哈希表的存储顺序是由哈希值决定的。
3. 可以动态调整大小,以应对负载因子变化。
- 用法
#![allow(dead_code)] // 忽略全局dead code,放在模块开头!
#![allow(unused_variables)] // 忽略未使用变量,放在模块开头!
// #[derive(Debug)]
fn main() {
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("a", 1);
map.insert("b", 2);
if let Some(value) = map.get("a") {
println!("Found: {}", value);
}
}
“桶”与“槽”
在哈希表(例如 Rust 的 HashMap
)的实现中,桶(bucket)是用来存储键值对的容器或位置。每个桶存储着哈希表中一个特定的“槽”中的元素,多个元素可能会被存放到同一个桶中。这种设计的目的是为了优化哈希表的性能,使得查找、插入和删除操作能够在常数时间复杂度(O(1))内完成。
如何理解桶?
-
哈希函数:当你插入一个键值对时,哈希表首先会通过一个哈希函数计算出该键的哈希值。哈希值是一个整数,表示该键在哈希表中的位置。
-
桶的数量:哈希表通常将桶的数量设置为一个固定的大小,这个大小可能是基于哈希表的当前大小动态扩展的。哈希表会根据哈希值将键映射到相应的桶。
-
桶的作用:每个桶是一个容器,桶中可以存放一个或多个键值对。哈希表通过哈希值来确定要查找哪个桶,进而查找该桶中的键值对。
-
桶冲突(碰撞):当多个键的哈希值相同,或者它们的哈希值被映射到相同的桶时,就会发生哈希碰撞。为了处理这种情况,哈希表通常会使用一种方法来将多个元素存储在同一个桶中,常见的解决方案有:
- 链式法(Chaining):每个桶实际上是一个链表(或者其他数据结构),多个键值对就可以在同一个桶中按链表的形式存储。
- 开放寻址法(Open Addressing):当发生碰撞时,哈希表会寻找另一个空桶来存储这个元素。
例子:假设我们要插入一些键值对
假设有一个简单的哈希表,桶的数量是 5(这里只是为了简化说明),哈希函数计算出来的哈希值是一个数字。键 "apple"
的哈希值计算为 2,键 "banana"
的哈希值计算为 2(假设发生碰撞),那么它们都会被放入桶 2 中。
桶 0: []
桶 1: []
桶 2: [("apple", 1), ("banana", 2)] <-- 这里发生了哈希碰撞
桶 3: []
桶 4: []
为了有效管理这些碰撞,HashMap
会使用链表等数据结构来存储多个哈希值相同的键值对。
为什么桶重要?
桶的设计使得哈希表能够有效地组织数据并在平均常数时间内执行操作。如果没有桶,哈希表可能会退化成一个线性查找的结构,失去哈希表的效率优势。
在 Rust 的 HashMap
中,桶的数量和哈希函数一起决定了哈希表的性能。当碰撞较多时,哈希表可能会进行扩展,增加桶的数量,以保持较好的性能。
2. BTreeMap
- 实现
基于平衡二叉树(通常是红黑树)实现,确保键按顺序排列。
- 特性
1. 有序:键值对按键的排序顺序存储(默认是升序),可以进行范围查询等操作。
2. 查找性能较差:查找、插入、删除的时间复杂度是 O(log n),比哈希表稍慢。
3. 适用于需要键有顺序的场景,如按键排序或按范围查询。
- 用法
#![allow(dead_code)] // 忽略全局dead code,放在模块开头!
#![allow(unused_variables)] // 忽略未使用变量,放在模块开头!
// #[derive(Debug)]
fn main() {
use std::collections::BTreeMap;
let mut map = BTreeMap::new();
map.insert("apple", 3);
map.insert("banana", 5);
for (key, value) in &map {
println!("{}: {}", key, value);
}
}
为什么代码中key
的类型是&&str
?
map
是一个BTreeMap<&'static str, i32>
类型的变量。- 通过
&map
引用,Rust 会遍历一个&BTreeMap<&'static str, i32>
。 BTreeMap
中的键值对是(key, value)
,其中key
是&'static str
,但是由于你在for
循环中遍历的是&map
,所以key
的类型实际上是&&'static str
。
总结对比
- HashMap
更适用于对性能要求较高、但对键顺序不敏感的情况。
- BTreeMap
更适用于需要键排序或进行范围查询的情况,性能略逊色于 HashMap
。
选择哪种类型,取决于你的具体需求:是更关心插入查找速度,还是需要按顺序操作数据。
在 Rust 中,BTreeMap
是一个有序的映射类型,它按键排序。在你的代码中,map.insert("apple", 3)
和 map.insert("banana", 5)
的键是字符串字面量 (&str
类型)。
为什么 key
的类型是 &&str
?
首先,让我们分析这段代码的关键部分:
use std::collections::BTreeMap;
let mut map = BTreeMap::new();
map.insert("apple", 3);
map.insert("banana", 5);
for (key, value) in &map {
println!("{}: {}", key, value);
}
BTreeMap 的键值存储
BTreeMap
存储的是键值对。在你的例子中,map
存储的是&str
类型的键和i32
类型的值。- 由于 Rust 的字符串字面量(比如
"apple"
)是静态的,因此它们的类型是&'static str
。这意味着它们是对内存中某个静态位置的引用,类型是&'static str
。
为何会有 &&str
的类型?
- 在
BTreeMap
中,键是通过引用存储的,因此每个键的类型实际上是&'static str
。 - 然而,
for (key, value) in &map
循环时,&map
是对BTreeMap
的一个引用。因此,key
的类型将会是&&str
。
这是因为:
举个例子:
&map
的类型是 &BTreeMap<&'static str, i32>
,所以你获取的每个 key
是 &&'static str
(即引用的引用)。
如何避免这个情况?
你可以使用 *key
来解引用 &&str
类型的 key
,使其变为 &str
:
for (key, value) in &map {
println!("{}: {}", *key, value); // 解引用 key
}
这样,key
就会是 &str
类型而不是 &&str
。
总结
key
的类型是 &&str
是因为你正在通过 &map
引用来遍历 BTreeMap
,所以每个 key
实际上是 BTreeMap
中的键(&'static str
)的引用,而你正在访问的是引用的引用,即 &&str
。通过解引用 *key
,你可以获取原始的 &str
类型。