解密 Rust 标准库 collections 模块下的那些动态数据结构(HashMap、HashSet、链表、二叉堆等等)

楔子

Rust 标准库的 collections 模块里面,实现了很多的数据结构,比如 HashMap、BtreeMap、HashSet,甚至还有链表、二叉堆等等,这些结构很多其它语言并没有提供,而是需要自己实现。但 Rust 不同,因为这些结构也比较常用,于是官方帮我们实现了,只不过放在了标准库当中,用的时候需要导入。

下面就来分别介绍一下这些数据结构。

HashMap

HashMap 可以理解为 Python 的字典,里面存储了键值对的映射,基于 key 可以很方便的查找到 value。

创建 HashMap 实例

use std::collections::HashMap;

fn main() {
    // 因为后续要添加键值对,所以需要使用 mut 关键字
    let mut girl: HashMap<String, String> = HashMap::new();
    let mut girl = HashMap::<String, String>::new();
}

我们知道哈希表是采用空间换时间的策略,哈希表最多维持 \(\frac{2}{3}\) 满,如果超过了这个界限,那么就意味着该扩容了,要将申请一个更大的哈希表。所以哈希表是有容量的,要是我们从一开始就知道这个哈希表的键值对数量会增长的特别快,那么也可以在创建的时候就指定容量,这样就不用频繁扩容了。

use std::collections::HashMap;

fn main() {
    // 创建的时候就给了 100 的容量
    let mut data: HashMap<String, String> = HashMap::with_capacity(100usize);
}

当然也可以基于已有数据创建哈希表。

use std::collections::HashMap;

fn main() {
    let girl = HashMap::from(
        [("name", "罗小云"), ("age", "18"), ("gender", "female")]
    );
    println!("{:?}", girl);  // {"name": "罗小云", "age": "18", "gender": "female"}
  
    let vec = vec![("name", "罗小云"), ("age", "18"), ("gender", "female")];
    // HashMap::from 里面如果接收数组,那么必须是静态数组
    // 因此如果你有一个动态数组,那么还可以将其转成迭代器,然后通过 collect() 转成 HashMap
    let girl = vec.into_iter().collect::<HashMap<_, _>>();
    println!("{:?}", girl);  // {"name": "罗小云", "age": "18", "gender": "female"}  
}

所以迭代器的 collect 方法非常强大,很多数据结构都可以转换。但需要注意的是,我们创建迭代器的时候使用的是 iter_into 方法,前面说过,创建迭代器有以下三种方式:

  • seq.iter():创建迭代器,注意:迭代器是惰性的,此时尚未遍历,但内部已经以不可变引用的形式指向了 seq。后续调用 collect 才开始遍历,会拿到 seq 每个元素的不可变引用;
  • seq.iter_mut():和 iter() 相同,但迭代器内部会以可变引用的形式指向 seq。后续调用 collect 遍历时,拿到的是 seq 每个元素的可变引用。不管是 iter 还是 iter_mut,创建迭代器之后,原本的 seq 均不受影响;
  • seq.into_iter():创建迭代器,但内部不再是 seq 的引用,而是会转移 seq 的所有权。后续调用 collect 遍历时,拿到的也是 seq 的每个元素(可 Copy 就拷贝一份,不可 Copy 就转移所有权),而不再是引用。所以调用 seq.into_iter() 之后,如果 seq 不是可 Copy 的,那么就不能再使用了;

这里创建迭代器必须使用 into_iter,返回的迭代器类型是 Iterator<Item=(K, V)>,其中 K 和 V 是元组中的元素类型。因为调用 collect 创建 HashMap 时,需要拿到元素的所有权。

如果使用 iter,那么返回的迭代器类型是 Iterator<Item=&(K, V)>,遍历得到的是引用,而不是元组本身,而 Rust 不允许基于引用创建 HashMap。如果希望创建完 HashMap 之后,原有的集合还可以使用,那么就 clone 一份。

use std::collections::HashMap;

fn main() {
    let tuples = [(1, "one"), (2, "two"), (3, "three")];
    // 不可以调用 iter()
    let map = tuples.into_iter().collect::<HashMap<_, _>>();
    // 因为 tuples 可 Copy,所以会拷贝一份,没有影响
    println!("{:?}", tuples);  // [(1, "one"), (2, "two"), (3, "three")]

    let tuples = [(1, "one".to_string()), (2, "two".to_string()), (3, "three".to_string())];
    // 此时 tuples 就不是可 Copy 的,因为里面出现了 String,在调用完 into_iter 之后,tuples 就不可以使用了
    // 如果希望后续能正常使用,那么需要 clone 一份
    let map = tuples.clone().into_iter().collect::<HashMap<_, _>>();
    println!("{:?}", tuples);  // [(1, "one"), (2, "two"), (3, "three")]

    // 当然啦,此时还可以使用 HashMap::from,此时同样会剥夺 tuples 的所有权
    // 如果它不是可 Copy 的,并且后续还希望继续用,那么同样需要 clone 一份
    let map = HashMap::from(tuples.clone());
    println!("{:?}", tuples);  // [(1, "one"), (2, "two"), (3, "three")]


    // 动态数组申请在堆上,如果希望后续能继续使用,那么也要 clone 一份
    let tuples = vec![(1, "one"), (2, "two"), (3, "three")];
    let map = tuples.clone().into_iter().collect::<HashMap<_, _>>();
    println!("{:?}", tuples);  // [(1, "one"), (2, "two"), (3, "three")]
}

以上就是 HashMap 的几种创建方式。

查看 HashMap 的长度和容量

use std::collections::HashMap;

fn main() {
    // HashMap 内部带了两个泛型字段,所以在 HashMap 后面加上 ::<T, W> 指定具体的类型
    // 再比如函数也定义了泛型,比如 collect,它内部带了一个泛型,所以通过 collect::<T> 指定具体的类型
    // 当然你也可以不这么做,而是在变量后面指定类型,这样 Rust 也可以推断出泛型代表的具体类型
    let map = HashMap::<String, String>::with_capacity(100);
    println!("{} {}", map.len(), map.capacity());  // 0 112

    // 还可以通过 is_empty 判断 HashMap 是否为空
    println!("{:?}", map.is_empty());  // true
}

容量为 100 其实表示的是容量不小于 100,这里给了 112 大小的容量。

查看 HashMap 所有的键、值

use std::collections::HashMap;

fn main() {
    let girl = HashMap::from(
        [("name", "罗小云"), ("age", "18"), ("gender", "female")]
    );
    // 返回一个 keys 对象
    let keys = girl.keys();
    println!("{:?}", keys);  // ["name", "age", "gender"]
    // 可以转成动态数组
    println!("{:?}", keys.collect::<Vec<_>>());  // ["gender", "name", "age"]

    // 返回一个 Values 对象
    let values = girl.values();
    println!("{:?}", values);  // ["18", "罗小云", "female"]
    println!("{:?}", values.collect::<Vec<_>>());  // ["18", "罗小云", "female"]
}

注意:返回的时候顺序不固定。

遍历 HashMap

use std::collections::HashMap;

fn main() {
    let mut girl = HashMap::from(
        [("name", "罗小云"), ("age", "18"), ("gender", "female")]
    );
    for (&key, &val) in girl.iter() {
        println!("key = {}, value = {}", key, val)
    }
    /*
    key = gender, value = female
    key = name, value = 罗小云
    key = age, value = 18
     */

    for (&key, val) in girl.iter_mut() {
        if key == "age" {
            // val 是 &str 的可变引用,*val 则是解引用得到 &str
            // 然后解析成 u8 再加 1
            // 将结果转成 String 并获取切片
            *val = "19";
        }
    }
    println!("{:?}", girl);  // {"name": "罗小云", "gender": "female", "age": "19"}
}

删除 HashMap 中不满足条件的元素

use std::collections::HashMap;

fn main() {
    let mut map = (0..10).map(|x: i32| (x, x.pow(2))).collect::<HashMap<_, _>>();
    println!("{:?}", map);
    /*
    {9: 81, 4: 16, 7: 49, 0: 0, 1: 1, 3: 9, 5: 25, 6: 36, 2: 4, 8: 64}
     */

    // 保留 key > 3,并且 value 为偶数的键值对
    map.retain(|&k, &mut v| k > 3 && (v & 1 == 0));
    println!("{:?}", map);  // {8: 64, 4: 16, 6: 36}
}

清空 HashMap,但会保留已分配的内存

use std::collections::HashMap;

fn main() {
    let mut map = (0..10).map(|x: i32| (x, x.pow(2))).collect::<HashMap<_, _>>();
    println!("{:?}", map);
    /*
    {9: 81, 4: 16, 7: 49, 0: 0, 1: 1, 3: 9, 5: 25, 6: 36, 2: 4, 8: 64}
     */

    map.clear();
    println!("{:?}", map);  // {}
}

为 HashMap 增加容量

假设某个 HashMap 还要存储 1w 个键值对,那么在存储的过程中必然会多次发生扩容,所以如果你能预知接下来要存储的元素个数,那么也可以手动扩容。

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<i32, i32> = HashMap::new();
    // 添加 30 个容量
    map.reserve(30);
}

缩小 HashMap 的容量

假设你添加了很多的键值对,然后又不断地删除,那么必然会产生内存浪费,因为键值对删了,但内存占用还在。所以 Rust 提供了两个 API,可以将 HashMap 的容量缩小到合适的大小。

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<i32, i32> = HashMap::new();
    map.reserve(100);
    println!("{:?}", map.capacity());  // 112
    // 容量意味着内存已经申请了,所以不需要的时候就会浪费
    map.shrink_to_fit();  // 缩小到合适的大小
    println!("{:?}", map.capacity());  // 0

    // 当然也可以指定一个下限,表示再怎么缩小,也不能低于这个下限
    map.reserve(100);
    println!("{:?}", map.capacity());  // 112
    map.shrink_to(10);
    println!("{:?}", map.capacity());  // 14
}

注意:调用 shrink_to 的时候,指定的容量下限应该小于当前的容量,如果大于,那么容量则保持不变。因为 shrink_to 用于缩容,不是扩容。

基于 key 获取 value

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<String, String> = HashMap::from(
        [("name".to_string(), "satori".to_string()),
         ("age".to_string(), "17".to_string()),
         ("gender".to_string(), "female".to_string())]
    );
    // 不管 key 是什么类型,都必须要传一个引用过去,这里 &str 和 &String 都是可以的
    // 如果 key 存在则返回 Some(&T),否则返回 None
    println!("{:?}", map.get("name"));  // Some("satori")
    println!("{:?}", map.get(&"name2".to_string()));  // None
    // value 是 String 类型,所以 get 的结果是 Some(&String)
    println!("{:?}", map.get("age") == Some(&"17".to_string()));  // true

    let mut map = HashMap::from([(1, 11), (2, 22)]);
    // map.get() 必须接收引用,返回的也是 Some(引用)
    println!("{:?}", map.get(&1) == Some(&11));  // true
}

当然,在获取 value 的同时也可以获取 key。

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<String, String> = HashMap::from(
        [("name".to_string(), "satori".to_string()),
            ("age".to_string(), "17".to_string()),
            ("gender".to_string(), "female".to_string())]
    );
    println!("{:?}", map.get_key_value("age") == Some((&"age".to_string(), &"17".to_string())));  // true
}

在调用 get 的时候,拿到的是引用,也可以调用 get_mut 拿到可变引用。

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<String, String> = HashMap::from(
        [("name".to_string(), "satori".to_string()),
            ("age".to_string(), "17".to_string()),
            ("gender".to_string(), "female".to_string())]
    );
    let value = map.get_mut("age").unwrap();
    *value = "18".to_string();
    println!("{:?}", map);  // {"gender": "female", "name": "satori", "age": "18"}
}

往 HashMap 里面插入键值对

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<String, String> = HashMap::new();
    // 如果 key 不存在,插入键值对并返回 None
    let v = map.insert("name".to_string(), "古明地觉".to_string());
    println!("{:?}", map);  // {"name": "古明地觉"}
    println!("{:?}", v);  // None

    // key 存在,更新 value 并返回旧的 value
    let v = map.insert("name".to_string(), "古明地恋".to_string());
    println!("{:?}", map);  // {"name": "古明地恋"}
    println!("{:?}", v);  // Some("古明地觉")
}

判断 HashMap 里面是否存在指定的 key

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<String, String> = HashMap::new();
    map.insert("name".to_string(), "古明地觉".to_string());
    println!("{:?}", map.contains_key("name"));  // true
    println!("{:?}", map.contains_key("name2"));  // false
}

删除 HashMap 里面指定的键值对

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<String, String> = HashMap::new();
    map.insert("name".to_string(), "古明地觉".to_string());
    println!("{:?}", map);  // {"name": "古明地觉"}
    // 会返回删除的键对应的值
    let v = map.remove("name");
    println!("{:?}", map);  // {}
    println!("{:?}", v);  // Some("古明地觉")
    // 如果键不存在,那么会返回 None
    let v = map.remove("name");
    println!("{:?}", v);  // None
}

在获取 value 的时候,因为不能夺走所有权(否则 HashMap 后续就没法用了),所以返回的都是 Option<&T>。但这里是删除,删除之后已经不存在于 HashMap 中了,所以会返回 Option<T>,此时会转移所有权,很好理解。

最后,我们可以基于数组创建 HashMap,那么同样也可以基于 HashMap 创建数组。

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<String, String> = HashMap::new();
    map.insert("name".to_string(), "古明地觉".to_string());
    map.insert("age".to_string(), "17".to_string());
    // 这里调用的 iter,所以会拿到 HashMap 的引用
    // 因此 Vector 里面元素也要是 (&String, &String)
    let vec = map.iter().collect::<Vec<(&String, &String)>>();
    println!("{:?}", vec);  // [("age", "17"), ("name", "古明地觉")]
    // HashMap 也不会受到影响
    println!("{:?}", map);  // {"name": "古明地觉", "age": "17"}

    // 如果希望 Vector 能够持有数据所有权,那么这里需要调用 into_iter
    // 此时拿到的就不再是 HashMap 的引用,而是会转移它的所有权
    let vec = map.into_iter().collect::<Vec<(String, String)>>();
    println!("{:?}", vec);  // [("age", "17"), ("name", "古明地觉")]
    // 后续不可以再使用 map 变量,因为它的值发生了移动
    // 如果希望两者互不影响,那么应该使用 map.clone().into_iter()
}

以上就是 HashMap 相关的内容,还是不复杂的。

HashSet

说完了 HashMap 之后再来看看 HashSet,如果把 HashMap 看作是 Python 的字典,那么 HashSet 就是 Python 的集合。HashSet 的存储原理和 HashMap 是一样的,只不过 HashMap 里面存储的是键值对,而 HashSet 里面存储的只有键(相当于值为空)。所以 HashSet 的作用就是去重,当你需要像动态数组一样自由添加元素,并且还不希望元素重复,那么使用 HashSet 再合适不过了。

创建 HashSet 实例

use std::collections::HashSet;

fn main() {
    // HashSet 的创建和 HashMap 类似
    let set = HashSet::<i32>::new();
    // 原理的 HashMap 类似,都是基于空间换时间的哈希表,所以也可以在创建时指定容量
    let set: HashSet<String> = HashSet::with_capacity(20);
    // 基于已有的数组进行创建
    let set = HashSet::from([1, 1, 1, 2, 2, 3]);
    // 可以看到实现了去重功能
    println!("{:?}", set);  // {3, 2, 1}
    // 假设有一个动态数组,我们希望将它去重
    let vec = vec!["a".to_string(), "a".to_string(), "a".to_string(),
                              "b".to_string(), "b".to_string(), "c".to_string()];
    // 如果是 Python 的话,那么直接 list(set(vec)) 即可,所以动态语言确实在编码方面要更方便
    let vec = vec.into_iter().collect::<HashSet<String>>().into_iter().collect::<Vec<String>>();
    println!("{:?}", vec);  // ["a", "b", "c"]
}

HashSet 有很多方法和 HashMap 都是类似的。

获取 HashSet 的长度和容量

use std::collections::HashSet;

fn main() {
    let set = HashSet::<i32>::new();
    println!("{} {}", set.len(), set.capacity());  // 0 0
}

获取 HashSet 的长度和容量

use std::collections::HashSet;

fn main() {
    let set = HashSet::<i32>::new();
    println!("{} {}", set.len(), set.capacity());  // 0 0
    // 如果判断长度是否为 0,还有一个专门的方法,就是判断它是否为空
    println!("{:?}", set.is_empty());  // true
}

删除 HashSet 中不满足条件的元素

use std::collections::HashSet;

fn main() {
    let mut set = (0..10).collect::<HashSet<i32>>();
    // 保留集合中所有的偶数
    set.retain(|n| n & 1 == 0);
    println!("{:?}", set);  // {0, 4, 6, 8, 2}
}

清空 HashSet 的所有元素

use std::collections::HashSet;

fn main() {
    let mut set = (0..10).collect::<HashSet<i32>>();
    println!("{:?}", set);  // {1, 7, 8, 3, 9, 4, 2, 6, 5, 0}
    set.clear();
    println!("{:?}", set);  // {}
}

为 HashSet 增加容量

use std::collections::HashSet;

fn main() {
    let mut set: HashSet<i32> = HashSet::new();
    println!("{}", set.capacity());  // 0
    // 添加 30 个容量
    set.reserve(30);
    println!("{}", set.capacity());  // 56
}

缩小 HashSet 容量

use std::collections::HashSet;

fn main() {
    let mut set: HashSet<i32> = HashSet::new();
    set.reserve(30);
    println!("{}", set.capacity());  // 56
    // 缩小到一个 Rust 编译器认为合适的容量
    set.shrink_to_fit();
    println!("{}", set.capacity());  // 0

    // 当然也可以指定一个下限,表示再怎么缩小,也不能低于这个下限
    set.reserve(100);
    set.shrink_to(50);
    println!("{}", set.capacity());  // 56
}

注意:调用 shrink_to 的时候,指定的容量下限应该小于当前的容量,如果大于,那么容量则保持不变。因为 shrink_to 用于缩容,不是扩容。

获取两个 HashSet 之间的交集、并集、差集、对称差集

use std::collections::HashSet;

fn main() {
    let mut set1: HashSet<i32> = HashSet::from([1, 2, 3, 4]);
    let mut set2: HashSet<i32> = HashSet::from([3, 4, 5, 6]);
    // 为了不影响原有的 HashSet,参数需要传递引用
    // 存在于 set1、并且存在于 set2,运算之后返回一个 Intersection 对象
    let inter = set1.intersection(&set2);
    // 存在于 set1、或者存在于 set2,返回一个 Union 对象
    let union = set1.union(&set2);
    // 存在于 set1、但不存在于 set2,返回一个 Difference 对象
    let diff = set1.difference(&set2);
    // 不存在于 set1、也不存在于 set2,返回一个 SymmetricDifference 对象
    let sym_diff = set1.symmetric_difference(&set2);
    // 注意:以上这些对象保存的都是对元素的引用
    // 如果不保存引用的话,那么当原始的 HashSet 里面存储的是不可 Copy 的数据时,就会转移所有权
    // 这样的话,集合运算之后,原始的 HashSet 就无法使用了

    // 因为 iter 内部保存的是引用,所以构建 HashSet 的时候,里面也必须是引用类型
    // 所以这里是 HashSet<&i32>,而不是 HashSet<i32>
    println!("{:?}", inter.collect::<HashSet<&i32>>());  // {4, 3}
    println!("{:?}", union.collect::<HashSet<&i32>>());  // {1, 5, 2, 3, 6, 4}
    println!("{:?}", diff.collect::<HashSet<&i32>>());  // {2, 1}
    println!("{:?}", sym_diff.collect::<HashSet<&i32>>());  // {2, 1, 5, 6}

    // 所以 collect 就是将迭代器里面的内容取出来,然后构建一个新的容器
    // 具体是什么容器则由我们来指定,只是容器里面的元素类型要和迭代器里面的元素类型保持一致

    // 那么问题来了,如果我希望构建的 HashSet 里面的元素不是引用,而是具体的值,该怎么做呢?
    let inter = set1.intersection(&set2);
    // inter 里面的是引用,通过调用 cloned 会将引用指向的值拷贝一份(深度拷贝)
    // 此时构建的 HashSet 里面存储的就是具体的值了,并且占据所有权
    println!("{:?}", inter.cloned().collect::<HashSet<i32>>());  // {3, 4}
}

当然啦,如果要进行集合运算的话,其实没必要这么麻烦。HashSet 提供了专门的操作符,来做这些事情。

use std::collections::HashSet;

fn main() {
    let mut set1: HashSet<i32> = HashSet::from([1, 2, 3, 4]);
    let mut set2: HashSet<i32> = HashSet::from([3, 4, 5, 6]);
    // 因为不能剥夺所有权,所以这里必须以引用的形式进行运算
    let inter = &set1 & &set2;
    let union = &set1 | &set2;
    let diff = &set1 - &set2;
    let sym_diff = &set1 ^ &set2;
    println!("{:?}", inter);  // {4, 3}
    println!("{:?}", union);  // {1, 5, 2, 6, 3, 4}
    println!("{:?}", diff);  // {1, 2}
    println!("{:?}", sym_diff);  // {1, 6, 2, 5}
}

需要注意的是,基于操作符运算的结果就是 HashSet,并且里面的元素不再是引用,而是具体的值。这里可能有人好奇了,HashSet 如果有效,那么里面的每个元素必须也要有效,那么当原始 HashSet 里面的元素不可 Copy时,不就只能转移所有权吗?那这样的话,运算之后,原有的 HashSet 就无法再用了,这和我们使用引用的目的相冲突啊。

所以在这里,Rust 会将内部的每个元素都做深度拷贝,咦,不是说 Rust 默认不会做深度拷贝吗?别急,我们看一下源代码。

比如 | 操作符实际上是调用了 bitor 方法,但 bitor 还是使用了 union,当然其它操作符也是如此,所以返回的确实是引用,没有做深度拷贝。但是注意里面的 cloned,我们说这个方法会将引用指向的值深度拷贝一份,所以使用运算符得到的就是 HashSet<T>。并且 Rust 默认不做深度拷贝这个结论也确实没有问题,这里做了深度拷贝是因为显式地调用了 cloned 方法。

这里需要再补充一下这个 cloned 方法,对于目前来说,如果迭代器遍历得到的是引用,那么 collect 之后生成的集合里面也要是引用。如果希望生成的集合里面存储的是值,那么有两种做法:

use std::collections::HashSet;

fn main() {
    let set1 = HashSet::from([1, 2, 3, 4, 5]);
    // 可以调用 set1.into_iter() 这样生成的迭代器内部保存的就不再是 set1 的引用,而是会转移所有权
    // 但这样 set1 后续就没法再用了,所以可以 clone 一份
    let set2 = set1.clone().into_iter().collect::<HashSet<i32>>();
    println!("{:?}", set2);  // {5, 1, 4, 3, 2}

    // set1.iter() 生成的迭代器内部保存的是 set1 的引用,那么遍历得到的也是引用,因此生成集合里面存储的应该也要是引用
    // 但是我们希望保存具体的值,于是在调用 collect 之前先调用一个 cloned
    // 这样后续在遍历的时候会将引用指向的值深度拷贝一份返回(当然对于 i32 来讲,深拷贝和浅拷贝没啥区别)
    let set3 = set1.iter().cloned().collect::<HashSet<i32>>();

所以调用 clone 方法是将具体的对象拷贝一份,而 cloned 方法是迭代器才有的,目的是在遍历的时候,将遍历出来的引用指向的值拷贝一份。如果 set1.iter() 之后调用的不是 cloned 而是 clone,那么含义就变了,这表示将迭代器本身拷贝一份,这样生成的集合里面的元素依旧是引用。

注意:cloned 方法是迭代器才有的,比如字符串就不可以调用 cloned 方法。

fn main() {
    // 现在有一个字符串,和一个字符串的引用
    let name = "古明地觉".to_string();
    let name_ref = &name;
    // 如果将字符串变量本身赋值给另一个变量,那么会转移所有权,为了不转移,我们需要调用 clone
    let name2 = name.clone();
    // 这样两个字符串都是有效的
    println!("{} {}", name, name2);  // 古明地觉 古明地觉

    // 但如果我们赋值的是引用,那么得到的也是引用
    let x = name_ref;  // 变量 x 也是 &String 类型

    // 但是我们不能直接 let name3 = *name_ref,因为字符串不可 Copy,而 *ref 这种方式只会拷贝栈上数据
    // 如果上面的语句成立,那么 Rust 只能转移所有权,因为我们没有显式指定深拷贝,那么默认就是浅拷贝
    // 但转移所有权之后,原来的变量就不能使用了,而我们之所以创建引用就是希望原有变量不失效
    // 所以转移所有权只能 let name3 = name,这种方式就明确指定了要转移所有权
    // let name3 = *name_ref 这种是不会转移所有权的,它要求数据必须是可 Copy 的
    // 因此如果希望将 String 拷贝一份,那么依旧需要调用 clone 方法
    // 注意:字符串没有 cloned 方法,它是迭代器才有的,这里调用 clone 依旧表示将字符串拷贝一份
    // 因为 clone 的第一个参数是 &self,所以即使是 String 调用,也会将引用拷贝一份传过去
    let name3 = name_ref.clone();
    println!("{}", name3);  // 古明地觉
}

以上我们就补充了一下 cloned 的用法。

判断 HashSet 是否包含某个元素

use std::collections::HashSet;

fn main() {
    let mut set: HashSet<i32> = HashSet::from([1, 2, 3, 4]);
    // 里面必须传递引用
    println!("{}", set.contains(&1));  // true
    println!("{}", set.contains(&11));  // false
}

往 HashSet 内部添加元素

use std::collections::HashSet;

fn main() {
    let mut set: HashSet<String> = HashSet::new();
    set.insert("古明地觉".to_string());
    println!("{:?}", set);  // {"古明地觉"}
    set.insert("古明地觉".to_string());
    println!("{:?}", set);  // {"古明地觉"}
}

删除 HashSet 内部的某个元素

use std::collections::HashSet;

fn main() {
    let mut set: HashSet<i32> = HashSet::from([1, 2, 3, 4]);
    // 里面必须传递引用,如果元素存在则删除成功,返回 true。否则删除失败返回 false
    println!("{:?}", set.remove(&2));  // true
    println!("{:?}", set);  // {4, 3, 1}
    println!("{:?}", set.remove(&2));  // false
    println!("{:?}", set);  // {4, 3, 1}
}

判断一个集合是否是另一个集合的子集 / 超集

use std::collections::HashSet;

fn main() {
    let mut set1: HashSet<i32> = HashSet::from([1, 2, 3]);
    let mut set2: HashSet<i32> = HashSet::from([1, 2, 3]);
    // 如果 set1 的元素都在 set2 中,那么 set1 就是 set2 的子集
    // 如果 set2 的元素都在 set1 中,那么 set1 就是 set2 的超集
    println!("{}", set1.is_subset(&set2));  // true
    println!("{}", set1.is_superset(&set2));  // true
}

判断两个集合是否相等

use std::collections::HashSet;

fn main() {
    let set1: HashSet<&str> = HashSet::from(["a", "b", "c"]);
    let set2: HashSet<&str> = HashSet::from(["a", "b", "c"]);
    println!("{}", &set1 == &set2);  // true
    println!("{}", &set1 != &set2);  // false
}

以上就是 HashSet 的一些 API。

再补充一下,除了 HashMap 和 HashSet 之外,还有 BTreeMap 和 BTreeSet,它们的用法和功能是差不多的,只是底层的数据实现不同。

数据结构

  • HashMap 和 HashSet:
    • 使用哈希表实现,哈希函数的质量对性能有很大的影响。
  • BTreeMap 和 BTreeSet:
    • 使用平衡二叉树(具体来说是 B 树)实现,它将元素存储在一个有序的树结构中,保持元素按键排序。

时间复杂度

  • HashMap 和 HashSet:
    • 平均查找、插入、删除操作的时间复杂度为 O(1),在最坏情况下(比如发生大量哈希冲突时),这些操作的时间复杂度会退化为 O(n)。
  • BTreeMap 和 BTreeSet:
    • 查找、插入、删除操作的时间复杂度一般为 O(log n)。

特点和使用场景

  • HashMap 和 HashSet:
    • 当不需要有序的键时是一个更好的选择;
    • 通常在处理大量数据且键的哈希分布均匀时有更好的性能;
    • 可以容忍一定程度的随机访问性能变化。
  • BTreeMap 和 BTreeSet:
    • 当需要一个总是有序的集合时是更好的选择,可以方便范围查找(例如找出所有键在某个区间内的元素);
    • 在对数据进行迭代操作时,BTreeMap 由于数据已排序,迭代效率更高;
    • 对于元素数量较小或查找次数远多于插入和删除次数的情况下表现较好。

内存使用

  • HashMap 和 HashSet:
    • 由于哈希表的性质,可能会有更多的内存开销用于处理冲突和维护哈希桶。
  • BTreeMap 和 BTreeSet:
    • 通常空间利用率更高,但因为需要维护树结构,可能会有额外的指针存储开销。

LinkedList

下面来看看 LinkedList,也就是链表,首先在查询元素方面,Vector 的表现肯定要强于链表,因为内存效率更高,能更高地利用 CPU 缓存。但如果要频繁地在头部插入元素,那么 Vector 会频繁进行元素的移动,但链表不需要。

所以具体使用哪一种结构,则取决于场景,下面来看看 LinkedList 的用法。

创建一个链表

use std::collections::LinkedList;

fn main() {
    // 初始化一个链表
    let link = LinkedList::<i32>::new();
    println!("{:?}", link);  // []
    // 或者基于已有数组创建链表
    let link = LinkedList::from([1, 2, 3]);
    println!("{:?}", link);  // [1, 2, 3]
    let link = vec![1, 2, 3].into_iter().collect::<LinkedList<i32>>();
    println!("{:?}", link);  // [1, 2, 3]
}

查看链表的长度

use std::collections::LinkedList;

fn main() {
    let link = LinkedList::from([1, 2, 3]);
    println!("{:?}", link.len());  // 3
    // 如果长度为 0,那么表示链表是空的
    println!("{:?}", link.is_empty())  // false
}

清空链表

use std::collections::LinkedList;

fn main() {
    let mut link = LinkedList::from([1, 2, 3]);
    println!("{:?}", link.len());  // 3
    link.clear();
    println!("{:?}", link.len());  // 0
}

检测链表是否包含某个元素

use std::collections::LinkedList;

fn main() {
    let mut link = LinkedList::from([1, 2, 3]);
    // 必须传递引用
    println!("{:?}", link.contains(&1));  // true
    println!("{:?}", link.contains(&11));  // false
}

往链表添加元素

use std::collections::LinkedList;

fn main() {
    let mut link = LinkedList::from([1, 2, 3]);
    println!("{:?}", link);  // [1, 2, 3]
    // 从头部添加元素
    link.push_front(4);
    // 从尾部添加元素
    link.push_back(5);
    println!("{:?}", link);  // [4, 1, 2, 3, 5]
}

从链表中移除元素

use std::collections::LinkedList;

fn main() {
    let mut link = LinkedList::from([1, 2, 3]);
    // 从链表头部移除元素,当链表为空时返回 None,否则返回 Some(T)
    let ele = link.pop_front();
    println!("{:?}", ele);  // Some(1)
    println!("{:?}", link);  // [2, 3]
    // 从链表尾部移除元素
    let ele = link.pop_back();
    println!("{:?}", ele);  // Some(3)
    println!("{:?}", link);  // [2]
}

获取链表的头部元素和尾部元素

use std::collections::LinkedList;

fn main() {
    let mut link = LinkedList::from([1, 2, 3]);
    // 获取链表的头部元素,存在返回 Some(&T),链表为空返回 None
    // 因为是获取元素,所以拿到的是引用,不是值
    let ele = link.front();
    println!("{:?}", ele);  // Some(1)

    // 获取尾部元素
    let ele = link.back();
    println!("{:?}", ele);  // Some(3)
}

上面在获取的时候,拿到的都是不可变引用,也可以获取可变引用。

fn main() {
    let mut link = LinkedList::from([1, 2, 3]);
    let ele = link.front_mut();
    if let Some(p) = ele {
        *p = 111;
    }
    println!("{:?}", link);  // [111, 2, 3]

    let ele = link.back_mut();
    if let Some(p) = ele {
        *p = 333;
    }
    println!("{:?}", link);  // [111, 2, 333]
}

基于指定索引将链表一分为二,并返回索引之后的链表(包含索引对应的节点)

use std::collections::LinkedList;

fn main() {
    let mut link = LinkedList::from([1, 2, 3, 4, 5]);
    let link = link.split_off(2);
    println!("{:?}", link);  // [3, 4, 5]
}

以上就是链表的一些操作。

VecDeque

聊完了链表之后,再来看看双端队列,它和链表一样,能够以 O(1) 的复杂度从头部和尾部添加元素,从而避免元素的移动。那它和链表之间有什么区别呢?在介绍链表的时候,我们只演示了如何获取头部和尾部的元素,中间的元素要怎么获取呢?所以链表不支持使用索引获取元素,但双端队列是支持的。

因此当你不太关注数据的读取,只关注数据的添加,那么使用链表。如果你想像链表一样自由添加数据,并且还能像数组一样通过索引随机访问,那么就需要使用双端队列了。

创建一个双端队列

use std::collections::VecDeque;
use std::collections::HashMap;

fn main() {
    // 创建一个双端队列
    let deque = VecDeque::<i32>::new();
    println!("{:?}", deque);  // []

    // 双端队列内部会使用一个环形数组,所以初始化的时候也可以指定容量
    let deque = VecDeque::<i32>::with_capacity(20);
    println!("{:?}", deque);  // []

    // 基于已有的数组进行初始化
    let deque = VecDeque::from([1, 2, 3]);
    println!("{:?}", deque);  // [1, 2, 3]

    // 基于其它动态数据结构转成 deque
    // 比如我们创建一个 HashMap
    let map = HashMap::from([(1, "satori"), (2, "koishi"), (3, "scarlet")]);
    let deque = map.keys().cloned().collect::<VecDeque<i32>>();
    println!("{:?}", deque);  // [3, 1, 2]
    let deque = map.values().cloned().collect::<VecDeque<&str>>();
    println!("{:?}", deque);  // ["scarlet", "satori", "koishi"]
}

获取长度和容量

use std::collections::VecDeque;

fn main() {
    let deque = VecDeque::from([1, 2, 3]);
    println!("{} {}", deque.len(), deque.capacity());  // 3 3
    // 也可以使用 is_empty 判断双端队列是否为空
    println!("{}", deque.is_empty());  // false
}

对双端队列进行扩容

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::from([1, 2, 3]);
    println!("{}", deque.capacity());  // 3
    // 添加 20 个容量
    deque.reserve(20);
    println!("{}", deque.capacity());  // 23
}

对双端队列进行缩容

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::from([1, 2, 3]);
    deque.reserve(20);
    println!("{}", deque.capacity());  // 23
    // 缩容,Rust 会自动选择一个合适的容量
    deque.shrink_to_fit();
    println!("{}", deque.capacity());  // 3
    // 或者也可以指定一个最小容量,表示缩容之后的容量不得低于指定的最小值
    deque.shrink_to(5);
    // 但这里还是 3,是因为容量已经小于最小值了
    println!("{}", deque.capacity());  // 3
}

改变双端队列的长度

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::from([1, 2, 3, 4, 5, 6]);
    // 将长度变成 8,并用 333 填充
    deque.resize(8, 333);
    println!("{:?}", deque);  // [1, 2, 3, 4, 5, 6, 333, 333]
    // 如果指定的长度小于当前长度,那么元素会被删掉
    deque.resize(4, 333);
    println!("{:?}", deque);  // [1, 2, 3, 4]
    deque.push_back(5);
    println!("{:?}", deque);  // [1, 2, 3, 4, 5]
}

向双端队列添加元素

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::from([1, 2, 3]);
    // 从左边添加,或者说从头部添加
    deque.push_front(4);
    deque.push_front(5);
    // 从尾部添加
    deque.push_back(6);
    deque.push_back(7);
    println!("{:?}", deque);  // [5, 4, 1, 2, 3, 6, 7]

    // 还可以从指定位置添加
    deque.insert(2, 8888);
    println!("{:?}", deque);  // [5, 4, 8888, 1, 2, 3, 6, 7]
}

合并两个双端队列

use std::collections::VecDeque;

fn main() {
    let mut deque1 = VecDeque::from([1, 2, 3]);
    let mut deque2 =  VecDeque::from([4, 5]);
    // 注意:如果是搞 Python 的话,会感到有些别扭,这里的 append 的作用类似于 Python 的 extend
    // 此外,这里并不是将 deque2 的元素拷贝一份到 deque1 中,而是移动
    deque1.append(&mut deque2);
    // append 之后 deque2 会变成空,所以要传递可变引用
    println!("{:?}", deque1);  // [1, 2, 3, 4, 5]
    println!("{:?}", deque2);  // []
}

从双端队列中移除元素

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::from([1, 2, 3, 4, 5]);
    // 从左边弹出元素,会返回 Some(T)
    deque.pop_front();
    println!("{:?}", deque);  // [2, 3, 4, 5]
    // 从右边弹出元素
    deque.pop_back();
    println!("{:?}", deque);  // [2, 3, 4]

    // 也可以删除任意位置的元素
    deque.remove(1);
    println!("{:?}", deque);  // [2, 4]

    // 如果所有元素都不想要了,也可以清空
    deque.clear();
    println!("{:?}", deque);  // []
}

保留双端队列中指定个数的元素

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::from([1, 2, 3, 4, 5, 6]);
    // 保留三个元素
    deque.truncate(3);
    println!("{:?}", deque);  // [1, 2, 3]
}

按照指定索引对双端队列进行分隔,保留索引后面的部分(包含索引)

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::from([1, 2, 3, 4, 5, 6]);
    let deque2 = deque.split_off(2);
    println!("{:?}", deque2);  // [3, 4, 5, 6]
}

对双端队列中的元素按照指定条件进行过滤,保留满足条件的元素

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::from([1, 2, 3, 4, 5, 6]);
    deque.retain(|n| n & 1 == 0);
    println!("{:?}", deque);  // [2, 4, 6]

    let mut deque = VecDeque::from([1, 2, 3, 4, 5, 6]);
    // 在遍历的时候也可以改变元素的值
    deque.retain_mut(|n| {
        // 每个元素都乘以 5,结果为偶数的保留下来
        // 注意:参数是元素的引用
        *n *= 5;
        if *n & 1 == 0 {true} else {false}
    });
    println!("{:?}", deque);  // [10, 20, 30]
}

交换双端队列中两个指定索引对应的元素

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::from([1, 2, 3, 4, 5, 6]);
    deque.swap(0, deque.len() - 1);
    println!("{:?}", deque);  // [6, 2, 3, 4, 5, 1]
}

判断双端队列中是否包含指定的元素

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::from([1, 2, 3, 4, 5, 6]);
    // 必须传递引用,这些动态数据结构在获取元素、判断是否包含指定元素的时候,都必须传递引用
    println!("{:?}", deque.contains(&3));  // true
    println!("{:?}", deque.contains(&333));  // false
}

获取双端队列中的元素

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::from([1, 2, 3, 4, 5, 6]);
    // 获取头部元素,返回 Some(&T)
    let front = deque.front();
    println!("{:?}", front);  // Some(1)
    // 获取尾部元素,返回 Some(&T)
    let back = deque.back();
    println!("{:?}", back);  // Some(6)

    // 上面拿到的都是不可变引用,还可以获取可变引用
    let front = deque.front_mut();
    if let Some(p) = front {
        *p += 10;
    }
    let back = deque.back_mut();
    if let Some(p) = back {
        *p += 10;
    }
    println!("{:?}", deque);  // [11, 2, 3, 4, 5, 16]
}

除了获取头部和尾部的元素外,还可以使用 get 获取指定索引的元素。

use std::collections::VecDeque;

fn main() {
    let deque = VecDeque::from([1, 2, 3, 4]);
    println!("{:?}", deque.get(1));  // Some(2)
    println!("{:?}", deque.get(100));  // None
    
    // 或者也可以像数组一样,使用 [],但是要承担越界的风险
    println!("{:?}", deque[1]);  // 2
    // println!("{:?}", deque[100])  // panic
}

基于 get 拿到的是引用,而通过 [] 则是获取具体的值。

use std::collections::VecDeque;

fn main() {
    let deque = VecDeque::from(["S 老师".to_string()]);
    let name: Option<&String> = deque.get(0);
    println!("{:?}", name);  // Some("S 老师")

    // 这里拿到的是 String,在介绍数组时说过,数组如果有效,里面的每个元素都必须是有效的
    // 但这里在将 deque[0] 赋值给 name 之后会转移所有权,会导致双端队列无效
    // 因此 Rust 不允许这么做,告诉我们不能移动所有权,除非元素是可 Copy 的,会拷贝一份
    // let name = deque[0];

    // 当然也可以获取可变引用
    let mut deque = VecDeque::from(["S 老师".to_string()]);
    let name = deque.get_mut(0);
    name.unwrap().push_str("月薪 4W");
    println!("{:?}", deque);  // ["S 老师月薪 4W"] 
}

旋转双端队列

use std::collections::VecDeque;

fn main() {
    let mut deque = (0..10).collect::<VecDeque<i32>>();
    println!("{:?}", deque);  // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    // 将头部的 n 个元素移到尾部
    deque.rotate_left(3);
    println!("{:?}", deque);  // [3, 4, 5, 6, 7, 8, 9, 0, 1, 2]

    // 将尾部的 n 个元素移到头部
    deque.rotate_right(5);
    println!("{:?}", deque);  // [8, 9, 0, 1, 2, 3, 4, 5, 6, 7]
}

以上就是双端队列的相关用法。

BinaryHeap

下面来说一说二叉堆,二叉堆本质上就是一个完美二叉树,并且满足每个父节点小于等于 / 大于等于它的两个孩子节点。

  • 如果父节点大于等于它的孩子节点,那么我们叫大根堆。
  • 如果父节点小于等于它的孩子节点,那么我们叫小根堆。

基于二叉堆,我们可以实现堆排序,这是一种 \(O(nlogn)\) 复杂度的排序,并且也可以方便地解决 \(TopK\) 问题。当然,能做到这两点的还有快排,并且快排的速度实际上是要快于堆排序的,但快排要求数据必须一次性全部给出,而堆排序则不要求。换句话说,使用堆排序,我们可以将数据以流的形式给出。

比如一个 10G 的文本,里面全是数字,要找出前 100 个最大的。那么就可以先读取 100 个,建立一个小根堆,此时堆顶就是最小元素。然后遍历文件,如果发现比数字比堆顶还要小,那么它一定不是前 100 个最大的;如果大于堆顶,那么就将堆顶元素替换掉,并调整堆的顺序。这样便可以使用有限的内存,去处理大量的数据。

基于此特性,二叉堆可以用来实现优先队列,这是堆最重要的用途。关于堆的更多性质这里不再赘述,可以上网查询更多有关堆的内容。

创建一个二叉堆

use std::collections::BinaryHeap;

fn main() {
    let mut heap = BinaryHeap::<i32>::new();
    println!("{:?}", heap);  // []

    // 也可以指定容量
    let mut heap = BinaryHeap::<i32>::with_capacity(20);
    println!("{:?}", heap);  // []

    // 基于数组创建
    let mut heap = BinaryHeap::from([1, 2, 3, 4, 5]);
    println!("{:?}", heap);  // [5, 4, 3, 1, 2]

    let mut heap = (0..10).collect::<BinaryHeap<i32>>();
    println!("{:?}", heap);  // [9, 8, 6, 7, 4, 5, 2, 0, 3, 1]
}

获取二叉堆的大小和容量

use std::collections::BinaryHeap;

fn main() {
    // 基于数组创建
    let mut heap = BinaryHeap::from([1, 2, 3, 4, 5]);
    println!("{} {}", heap.len(), heap.capacity());  // 5 5
}

向二叉堆添加数据

use std::collections::BinaryHeap;

fn main() {
    // 基于数组创建
    let mut heap = BinaryHeap::<i32>::new();
    heap.push(1);
    heap.push(2);
    heap.push(3);
    heap.push(4);
    heap.push(5);
    println!("{:?}", heap);  // [5, 4, 2, 1, 3]
}

此外还可以调用 append,将另一个堆的数据移动到当前堆。

use std::collections::BinaryHeap;

fn main() {
    // 基于数组创建
    let mut heap1 = BinaryHeap::from([1, 2, 3]);
    let mut heap2 = BinaryHeap::from([4, 5, 6]);
    heap1.append(&mut heap2);
    println!("{:?}", heap1);  // [6, 5, 4, 2, 3, 1]
    println!("{:?}", heap2);  // []
}

对二叉堆进行扩容

use std::collections::BinaryHeap;

fn main() {
    // 基于数组创建
    let mut heap = BinaryHeap::from([1, 2, 3]);
    println!("{}", heap.capacity());  // 3
    // 扩容 30 个容量
    heap.reserve(30);
    println!("{}", heap.capacity());  // 33
}

对二叉堆进行缩容

use std::collections::BinaryHeap;

fn main() {
    let mut heap = BinaryHeap::from([1, 2, 3]);
    heap.reserve(30);
    // 缩容,但容量不能低于 10
    heap.shrink_to(10);
    println!("{}", heap.capacity());  // 10
    // 缩容,缩小到 Rust 认为合适的容量
    heap.shrink_to_fit();
    println!("{}", heap.capacity());  // 3
}

将二叉堆转成一个排序好的动态数组

use std::collections::BinaryHeap;

fn main() {
    let mut heap = BinaryHeap::from([1, 3, 7, 5, 4, 2, 6]);
    // 注意:这里会夺走 heap 的所有权
    let vec = heap.into_sorted_vec();
    println!("{:?}", vec);  // [1, 2, 3, 4, 5, 6, 7]
}

获取二叉堆的堆顶元素

use std::collections::BinaryHeap;

fn main() {
    let mut heap = BinaryHeap::from([1, 3, 7, 5, 4, 2, 6]);
    // 获取堆顶元素,拿到的是引用
    let top = heap.peek();
    println!("{:?}", top);  // Some(7)

    // 可以看到,Rust 默认建立的是大根堆
    // 当然我们也可以弹出堆顶元素,弹出之后会自动维护堆的形状
    // 因为是弹出,所以返回的是 Some(T)
    let mut vec: Vec<i32> = Vec::new();
    loop {
        if let Some(n) = heap.pop() {
            vec.push(n)
        } else {
            break
        }
    };
    // 每一次弹出,都会自动堆的形状,让剩余的元素组成一个新的大根堆
    // 所以 vec 是一个降序排序的动态数组
    println!("{:?}", vec);  // [7, 6, 5, 4, 3, 2, 1]
}

比较简单,那么问题来了,默认建立的是大根堆,可不可以建立小根堆呢?

use std::collections::BinaryHeap;
use std::cmp::Reverse;

fn main() {
    let mut heap = BinaryHeap::from(
        [1, 3, 7, 5, 4, 2, 6].map(|c| Reverse(c))
    );
    println!("{:?}", heap);
    // [Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4), Reverse(7), Reverse(6)]

    // 此时建立的就是小根堆
    if let Some(Reverse(min)) = heap.pop() {
        println!("{}", min);  // 1
    }
}

清空二叉堆

use std::collections::BinaryHeap;
use std::cmp::Reverse;

fn main() {
    let mut heap = BinaryHeap::from(
        [1, 3, 7, 5, 4, 2, 6].map(|c| Reverse(c))
    );
    heap.clear();
    println!("{:?}", heap);  // []
}

以上就是二叉堆相关的内容。

小结

以上我们就介绍了 Rust 中的一些动态数据结构,有了它们可以很大程度上节省我们的开发量。

posted @ 2023-10-26 19:02  古明地盆  阅读(468)  评论(0编辑  收藏  举报