Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

11. 集合

11.1 简介

  在实际开发中,程序往往需要处理大量数据,并对这些数据执行查找、排序、新增、修改、删除等操作。为了高效地存储和管理数据,Rust 提供了多种集合类型(Collections)。这些集合采用不同的数据组织方式和底层实现,以适应不同应用场景的需求:有的集合侧重于提高数据查询效率,有的则更关注新增和删除操作的性能。

11.2 动态数组(Vec)

  动态数组的数据组织方式与普通数组类似,都是采用线性结构组织数据,其元素存储在一块连续的内存空间中。通过数组索引,动态数组可以快速访问每个元素,因此在查询和访问操作上具有显著的性能优势。动态数组内部维护三个字段来管理内存空间:内存区域指针(ptr)、长度(len)和容量(cap)。其中,ptr指向实际存储元素的内存区域,len表示当前存储的元素数量,而cap则表示当前分配的内存容量。

  当现有容量不足以容纳新增元素时,动态数组会自动扩容以容纳更多数据,通常采用容量翻倍的策略,以减少频繁的内存重新分配。动态数组在查询和随机访问方面表现优异,能够通过索引快速定位元素。但在插入或删除元素时,尤其不是在数组末尾进行操作时,需要移动大量数据,从而带来较大的性能开销。因此,动态数组虽然在访问性能上具有明显优势,但在需要频繁插入和删除的场景下,效率往往不如其他数据结构。

  Rust 提供了多种创建动态数组的方式:使用 Vec::new() 创建空数组;使用 Vec::with_capacity() 创建具有指定初始容量的数组,适用于已知容量的场景;使用 Vec::from() 基于已有数组生成动态数组;而 vec![] 宏则提供了一种更简洁的方式,可以直接初始化并使用指定值填充动态数组。

fn main() {
    // 创建一个空的动态数组
    let vec1: Vec<i32> = Vec::new();
    println!("new: 数组容量={}, 元素数量={}", vec1.capacity(), vec1.len());

    // 基于数组创建动态数组,创建后容量和数组长度(元素数量)相等
    let array = [10, 20, 30, 40, 50];
    let vec2 = Vec::from(array);
    println!("from: 数组容量={}, 元素数量={}", vec2.capacity(), vec2.len());

    // 创建并使用指定值填充动态数组,创建后容量和数组长度相等
    let vec3 = vec![10, 20, 30, 40, 50];
    println!("vec!: 数组容量={}, 元素数量={}", vec3.capacity(), vec3.len());

    // 创建一个指定大小的动态数组,容量大小为 100,
    let vec4: Vec<i32> = Vec::with_capacity(100);
    println!("with_capacity: 数组容量={}, 元素数量={}", vec4.capacity(), vec4.len());

    // 在 64 位平台上,动态数组大小为 24 字节(ptr 指针类型、len 和 cap usize 类型)
    println!("动态数组大小: {}", size_of_val(&vec1));
}
shell> cargo run
new: 数组容量=0, 元素数量=0
from: 数组容量=5, 元素数量=5
vec!: 数组容量=5, 元素数量=5
with_capacity: 数组容量=100, 元素数量=0
动态数组大小: 24

  集合类型通常提供一系列用于管理数据的操作接口,包括新增、删除、修改、查询和遍历等,Vec 也同样支持这些常用操作。

fn main() {
    // 创建并使用指定值填充动态数组
    let mut vec = vec![10, 20, 30, 40];

    // 插入数据
    vec.push(50);         // 插入到末尾
    vec.insert(0, 100);    // 插入到指定索引位置
    println!("插入数据: {:?}", vec);

    // 删除数据
    vec.remove(0);          // 删除指定索引的值
    vec.drain(1..=3);       // 批量删除,索引 1~3
    println!("删除数据: {:?}", vec);

    // 修改数据,修改指定索引的值
    vec[0] = 10;
    println!("修改数据: {:?}", vec);

    // 读取数据,获取指定索引的值
    let value1 = vec[0];
    let value2 = vec.get(0);    // 返回值是 Option,如果索引超出范围动态数组,返回 None
    println!("获取指定索引的值: {}, {}", value1, value2.unwrap());

    // 通过索引遍历数据
    for index in 0..vec.len() {
        println!("通过索引遍历数据: index={}, {:?}", index, vec.get(index));
    }

    // 通过不可变引用迭代器遍历数据
    for element in vec.iter() {
        println!("通过引用遍历数据: {}", element);
    }

    // 通过可变引用迭代器遍历数据
    for element in vec.iter_mut() {
        *element += 100;     // 修改数据,将元素加 1.0
        println!("通过可变引用遍历数据: {}", element);
    }

    // 通过获取所有权迭代器遍历数据
    for element in vec.into_iter() {
        println!("通过获取所有权遍历数据: {}", element);
    }

    // 所有权被转移,原始数组无法再访问
    // println!("{:?}", vec);
}
shell> cargo run
插入数据: [100, 10, 20, 30, 40, 50]
删除数据: [10, 50]
修改数据: [10, 50]
获取指定索引的值: 10, 10
通过索引遍历数据: index=0, Some(10)
通过索引遍历数据: index=1, Some(50)
通过引用遍历数据: 10
通过引用遍历数据: 50
通过可变引用遍历数据: 110
通过可变引用遍历数据: 150
通过获取所有权遍历数据: 110
通过获取所有权遍历数据: 150

  动态数组在扩容时可能会带来较大的性能开销:当现有容量不足以容纳新增元素时,Vec 会按倍数增长容量(Rust v1.92.0)。如果当前内存空间后方没有足够的空闲连续区域,Vec 需要在其他位置重新分配内存,并将已有元素复制过去。当元素数量较多时,复制过程会带来较大的性能开销。因此,在已知所需容量的情况下,建议使用 Vec::with_capacity() 预先分配空间,以避免扩容导致的性能损失。

use std::time;

fn main() {

    // 创建一个空的动态数组,初始容量为 0
    let mut vec = Vec::new();
    println!("初始容量={}, 元素数量={}", vec.capacity(), vec.len());

    // 第一次添加元素,容量扩充为 4
    vec.push(1);
    println!("第一次扩容容量={}, 元素数量={}", vec.capacity(), vec.len());

    // 连续添加元素,超过容量时,容量翻倍扩充为 8
    vec.push(1);
    vec.push(1);
    vec.push(1);
    vec.push(1);
    println!("再次扩容容量={}, 元素数量={}", vec.capacity(), vec.len());


    // 使用 Vec::with_capacity() 预先分配空间,可以避免扩容导致的性能开销
    let count = 10_000_000;
    {
        // 未预分配容量,需要多次扩容
        let start = time::Instant::now();
        let mut vec1 = Vec::new();
        for i in 0..count {
            vec1.push(i);
        }
        println!("Vec::new() 耗时: {:?}", start.elapsed());
    }

    {
        // 预先分配容量(避免频繁扩容)
        let start = time::Instant::now();
        let mut vec = Vec::with_capacity(count);
        for i in 0..count {
            vec.push(i);
        }
        println!("Vec::with_capacity() 耗时: {:?}", start.elapsed());
    }
}
shell> cargo run
初始容量=0, 元素数量=0
第一次扩容容量=4, 元素数量=1
再次扩容容量=8, 元素数量=5
Vec::new() 耗时: 209.692176ms
Vec::with_capacity() 耗时: 186.665145ms

  另外,在动态数组中间新增或删除数据时,也会导致较大的性能开销。这是因为插入或删除元素后,Vec 需要移动其他元素以保持顺序,尤其是在数组较大时,性能损耗更加明显。

use std::time;

fn main() {
    // 创建一个大小为 1 亿的动态数组
    let capacity = 100000000;
    let mut vec = Vec::with_capacity(capacity);
    vec.extend(0..capacity);   // 批量插入

    // 删除第一个元素
    let start = time::Instant::now();
    vec.remove(0);
    let end = time::Instant::now();
    println!("删除第一个元素耗时 {:?}", end - start);

    // 删除最后一个元素
    let start = time::Instant::now();
    vec.remove(vec.len() - 1);
    let end = time::Instant::now();
    println!("删除最后一个元素耗时 {:?}", end - start);

    // 删除中间元素
    let start = time::Instant::now();
    vec.remove(vec.len() / 2);
    let end = time::Instant::now();
    println!("删除中间元素耗时 {:?}", end - start);

    // 在开头插入一个元素
    let start = time::Instant::now();
    vec.insert(0, 100);
    let end = time::Instant::now();
    println!("在第一个位置插入元素耗时 {:?}", end - start);

    // 在末尾插入一个元素
    let start = time::Instant::now();
    vec.push(100);
    let end = time::Instant::now();
    println!("在最后位置插入元素耗时 {:?}", end - start);

    // 在中间插入一个元素
    let start = time::Instant::now();
    vec.insert(vec.len() / 2, 100);
    let end = time::Instant::now();
    println!("在中间位置插入元素耗时 {:?}", end - start);
}
shell> cargo run
删除第一个元素耗时 80.90129ms
删除最后一个元素耗时 821ns
删除中间元素耗时 46.291873ms
在第一个位置插入元素耗时 98.852552ms
在最后位置插入元素耗时 104ns
在中间位置插入元素耗时 56.636787ms

  动态数组 Vec 会根据需要自动扩容,但不会自动缩容。如果删除大量元素后需要释放空闲的内存空间,可以手动调用 shrink_to_fit() 函数,将容量收缩至当前元素数量,或使用 shrink_to() 函数将容量收缩至指定大小,但不会小于当前元素数量,即 容量 = max(n, 元素数量)。

fn main() {
    // 创建一个容量大小为 100 的动态数组,添加元素后,使用 shrink_to_fit() 将容量收缩到当前元素数量
    let mut vec = Vec::with_capacity(100);
    vec.push(1);
    vec.shrink_to_fit();
    println!("调用 shrink_to_fit() 后容量: {}", vec.capacity());

    // 创建一个容量大小为 100 的动态数组添加元素后,使用 shrink_to() 将容量收缩至指定大小
    let mut vec = Vec::with_capacity(100);
    vec.extend(1..=10);             // 插入 10 个元素
    vec.shrink_to(vec.len() + 2);   // 如果未来还需要插入,可以预留一些余量
    println!("调用 shrink_to() 后容量: {}", vec.capacity());

    // 指定大小小于当前元素数量,最终容量等于元素数量
    vec.shrink_to(6);
    println!("调用 shrink_to(6) 后容量: {}", vec.capacity());
}
shell> cargo run
调用 shrink_to_fit() 后容量: 1
调用 shrink_to() 后容量: 12
调用 shrink_to(6) 后容量: 10

11.3 哈希表(HashMap)

  哈希表(HashMap)同样采用线性结构来组织数据,将元素存储在一块连续的内存空间中,但访问方式并不依赖数组索引。其存储的元素由键值对(key-value)组成,HashMap 会通过哈希函数将键(key)转换为哈希值,并根据该哈希值计算存储位置,实现数据的快速查询与访问。

  由于元素的存储位置由哈希值决定,其排列顺序取决于哈希值的分布,而非插入顺序,因此哈希表并不像数组那样具备有序性。但也正因如此,哈希表在任意位置进行插入或删除操作时,无需像数组那样大规模移动后续元素,随机插入和删除效率较高。

  使用 HashMap::new() 函数可以创建一个空哈希表,或使用 HashMap::with_capacity()函数创建一个具有指定初始容量的哈希表。

use std::collections::HashMap;

fn main() {
    // 创建一个空的哈希表
    let map: HashMap<String, i32> = HashMap::new();
    println!("new: 初始容量={}, 元素数量={}", map.capacity(), map.len());

    // 创建具有预设容量的哈希表,可以减少后续插入时的扩容开销
    // 注意:HashMap 的实际分配容量通常是 2 的幂次方,且受负载因子限制
    let map: HashMap<String, String> = HashMap::with_capacity(10);
    println!("with_capacity: 初始容量={}, 元素数量={}", map.capacity(), map.len());
}
shell> cargo run
new: 初始容量=0, 元素数量=0
with_capacity: 初始容量=14, 元素数量=0

  HashMap 提供一系列用于管理数据的操作接口,包括新增、删除、查询和遍历等。需要注意的是,当向哈希表中插入已存在的键时,旧的值会被新值覆盖。

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<u32, &str> = HashMap::new();

    // 插入键值对,不保证元素的按插入顺序存放
    map.insert(3, "张三");
    map.insert(4, "李四");
    map.insert(5, "王五");
    println!("insert: {:?}", map);

    // 如果键已存在,insert 会更新并返回旧值
    let old_value = map.insert(3, "赵六");   // 旧值: Some("张三")
    println!("insert: {:?}, 旧值={:?}", map, old_value);

    // 条件插入,键不存在才插入
    map.entry(7).or_insert("钱七");
    println!("or_insert: {:?}", map);

    // 删除数据
    let removed_value = map.remove(&3);
    println!("remove: {:?}, 删除的数据={:?}", map, removed_value);

    // 清空数据
    // map.clear();
    // println!("clear: {:?}", map);

    // 判断数据是否存在
    let contains_key = map.contains_key(&4);
    println!("contains_key: {}", contains_key);

    // 获取数据
    let value = map.get(&4);
    println!("get: {:?}", value);

    // 获取所有 key
    let keys = map.keys();
    println!("keys: {:?}", keys);

    // 获取所有 value
    let values = map.values();
    println!("values: {:?}", values);

    // 遍历数据
    for (key, value) in map.iter() {
        println!("iter: key={}, value={}", key, value);
    }

    for (key, value) in map.iter_mut() {
        println!("iter_mut: key={}, value={}", key, value);
    }

    for (key, value) in map.into_iter() {
        println!("into_iter: key={}, value={}", key, value);
    }
}
shell> cargo run
insert: {4: "李四", 5: "王五", 3: "张三"}
insert: {4: "李四", 5: "王五", 3: "赵六"}, 旧值=Some("张三")
or_insert: {4: "李四", 7: "钱七", 5: "王五", 3: "赵六"}
remove: {4: "李四", 7: "钱七", 5: "王五"}, 删除的数据=Some("赵六")
contains_key: true
get: Some("李四")
keys: [4, 7, 5]
values: ["李四", "钱七", "王五"]
iter: key=4, value=李四
iter: key=7, value=钱七
iter: key=5, value=王五
iter_mut: key=4, value=李四
iter_mut: key=7, value=钱七
iter_mut: key=5, value=王五
into_iter: key=4, value=李四
into_iter: key=7, value=钱七
into_iter: key=5, value=王五

  HashMap 的键(Key)必须实现 Hash 和 Eq trait。其中,Hash trait 用于计算键的哈希值,Eq trait 用于判断键之间的相等性。当插入新数据时,如果两个键的哈希值相同且经过相等性比较后判定为同一个键,新值会覆盖旧值,避免重复键的出现,即键不会重复。

use std::collections::HashMap;
use std::hash::{Hash, Hasher};

// 自定义结构体
#[derive(Debug)]
struct User {
    id: u32,
    name: String,
}

// 为 User 实现 Hash trait
impl Hash for User {
    fn hash<H: Hasher>(&self, state: &mut H) {
        // 计算哈希值时,使用 id 和 name 字段
        self.id.hash(state);
        self.name.hash(state);
    }
}

// 为 User 实现 PartialEq 和 Eq trait
impl PartialEq for User {
    fn eq(&self, other: &Self) -> bool {
        // 判断字段必须与 hash() 函数统一
        self.id == other.id && self.name == other.name
    }
}

impl Eq for User {}

fn main() {
    // 创建一个 HashMap,键是 User,值是 u32
    let mut map: HashMap<User, u32> = HashMap::new();

    // 创建两个 User 实例
    let user1 = User { id: 1, name: String::from("张三") };
    let user2 = User { id: 2, name: String::from("李四") };
    let user3 = User { id: 1, name: String::from("张三") }; // 与 user1 相同

    // 插入元素
    map.insert(user1, 10);
    map.insert(user2, 20);
    map.insert(user3, 30); // 由于 user3 与 user1 相同,不会插入新值

    for (key, value) in &map {
        println!("User ID: {}, Name: {}, Value: {}", key.id, key.name, value);
    }
}
shell> cargo run
User ID: 1, Name: 张三, Value: 30
User ID: 2, Name: 李四, Value: 20

  HashMap 在扩容时会产生一定的性能开销。当容量不足以容纳新增元素时,它会自动扩容:重新计算哈希值、复制已有元素并插入新位置。为了避免频繁扩容带来的性能损失,在已知所需容量的情况下,可以通过 with_capacity() 预先分配空间。需要注意的是,HashMap 不会自动缩容,若希望释放多余的内存空间,则需手动调用 shrink_to() 或 shrink_to_fit()。

11.4 哈希集合(HashSet)

  HashSet 基于哈希表实现,其底层结构是 HashMap<K, ()>,但仅使用键(key)来存储数据,值固定为没有实际值的单元类型 ()。其特性与 HashMap 基本一致:查询效率高、不具备有序性、插入与删除操作也较为高效。同时,HashSet 会自动保证元素的唯一性,即不允许存在重复元素。此外,集合中的元素类型必须实现 Hash 和 Eq trait,以支持哈希计算与相等性比较。HashSet 也提供了一系列用于管理数据的操作接口,且接口与 HashMap 类似。

use std::collections::HashSet;

fn main() {
    let mut set: HashSet<&str> = HashSet::new();

    // 插入元素,不保证元素的按插入顺序存放
    set.insert("张三");             // 单条插入
    set.extend(["李四", "王五"]);    // 批量插入
    println!("insert: {:?}", set);

    // 如何元素已存在,无法插入
    let is_inserted = set.insert("张三");   // 返回值: false
    println!("insert: {:?}, 是否插入成功={}", set, is_inserted);

    // 替换指定值,不存在则插入,存在则返回被替换的值
    let replaced = set.replace("张三");
    println!("replace: {:?}, 被替换的值={:?}", set, replaced);

    // 删除元素
    let removed = set.remove("李四");
    println!("remove: {:?}, 是否删除成功={}", set, removed);

    // 清空数据
    // set.clear();
    // println!("clear: {:?}", set);

    // 判断元素是否存在
    let contains = set.contains("王五");
    println!("contains: {}", contains);

    // 获取数据
    let value = set.get("张三");
    println!("get: {:?}", value);

    // 遍历集合
    for value in &set {
        println!("iter: {}", value);
    }

    for value in set.into_iter() {
        println!("into_iter: {}", value);
    }
}
shell> cargo run
insert: {"王五", "张三", "李四"}
insert: {"张三", "王五", "李四"}, 是否插入成功=false
replace: {"张三", "王五", "李四"}, 被替换的值=Some("张三")
remove: {"张三", "王五"}, 是否删除成功=true
contains: true
get: Some("张三")
iter: 张三
iter: 王五
into_iter: 张三
into_iter: 王五

11.5 双向链表(LinkedList)

  双向链表的数据组织方式与动态数组和哈希表截然不同,链表的元素并不存储在一块连续的内存空间中,而是由多个分散的节点(Node)组成。每个节点包含三个部分:元素值(elem)、前一个节点指针(prev)和后一个节点指针(next)。通过这两个指针,链表可以高效地在任意位置插入或删除元素,而无需像动态数组那样移动大量数据,也不存在扩容性能开销问题。

  虽然双向链表的数据组织方式在插入和删除操作上具有显著优势,但在查询(如查找某个特定元素)时的性能却不如动态数组或哈希表。这是因为链表不支持通过索引或哈希直接访问元素,查询操作需要从头或尾开始遍历,直到找到目标元素。

  LinkedList 同样提供一系列用于管理数据的操作接口,包括新增、删除、查询和遍历等。由于不依赖连续内存且不存在扩容问题,因此未提供 with_capacity 等预分配相关接口。

use std::collections::LinkedList;

fn main() {
    let mut list: LinkedList<i32> = LinkedList::new();
    println!("链表元素数量:{}", list.len());

    // 从头部/尾部插入
    list.push_front(1);
    list.push_back(2);
    list.push_back(3);
    list.push_front(0);
    println!("push: {:?}", list);

    // 弹出头部/尾部元素
    let front = list.pop_front();
    let back = list.pop_back();
    println!("pop: {:?}, pop_front={:?}, pop_back={:?}", list, front, back);

    // 获取头尾/尾部元素
    let front = list.front();
    let back = list.back();
    println!("front: {:?}, back: {:?}", front, back);

    let front_mut = list.front_mut();
    println!("front_mut: {:?}", front_mut);

    let back_mut = list.back_mut();
    println!("back_mut: {:?}", back_mut);

    // 从头开始遍历
    for v in list.iter() {
        println!("iter: {}", v);
    }
    // 从尾开始遍历(反向迭代)
    for v in list.iter().rev() {
        println!("rev iter: {}", v);
    }

    for v in list.iter_mut().rev() {
        println!("rev iter_mut: {}", v);
    }

    for v in list.into_iter().rev() {
        println!("rev into_iter: {}", v);
    }
}
#![allow(unused)]
fn main() {
shell> cargo run
链表元素数量:0
push: [0, 1, 2, 3]
pop: [1, 2], pop_front=Some(0), pop_back=Some(3)
front: Some(1), back: Some(2)
front_mut: Some(1)
back_mut: Some(2)
iter: 1
iter: 2
rev iter: 2
rev iter: 1
rev iter_mut: 2
rev iter_mut: 1
rev into_iter: 2
rev into_iter: 1
}