【Rust通用集合类型】Rust向量Vector、String、HashMap原理解析与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨

🎈🎈 养成好习惯,先赞后看哦~🎈🎈

🏆 作者简介:景天科技苑

🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。

🏆《博客》:Rust开发,Python全栈,Golang开发,云原生开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。

所属的专栏: Rust语言通关之路
景天的主页: 景天科技苑

文章目录

  • 通用集合类型
    • 1、vector
      • [1.1 创建 Vector](#1.1 创建 Vector)
      • [1.2 添加/更新元素](#1.2 添加/更新元素)
      • [1.3 丢弃 vector 时也会丢弃其所有元素](#1.3 丢弃 vector 时也会丢弃其所有元素)
      • [1.4 访问vector元素](#1.4 访问vector元素)
      • [1.5 无效引用](#1.5 无效引用)
      • [1.6 遍历 vector 中的元素](#1.6 遍历 vector 中的元素)
      • [1.7 使用枚举来储存多种类型](#1.7 使用枚举来储存多种类型)
      • [1.8 vector容量管理](#1.8 vector容量管理)
      • [1.9 vector 与其他集合转换](#1.9 vector 与其他集合转换)
      • [1.10 vector 常用方法](#1.10 vector 常用方法)
    • 2、Rust字符串
    • [3、 hashmap 储存键值对](#3、 hashmap 储存键值对)
      • [3.1 HashMap 简介](#3.1 HashMap 简介)
      • [3.2 创建 HashMap 的多种方式](#3.2 创建 HashMap 的多种方式)
      • [3.3 插入和更新值](#3.3 插入和更新值)
      • [3.4 访问值](#3.4 访问值)
        • [1)使用 get 方法](#1)使用 get 方法)
        • [2)使用 get_mut 获取可变引用](#2)使用 get_mut 获取可变引用)
        • 3)遍历hashmap
      • [3.5 删除操作](#3.5 删除操作)
        • [1)使用 remove 删除](#1)使用 remove 删除)
        • [2)使用 remove_entry 删除并返回键值对](#2)使用 remove_entry 删除并返回键值对)
      • [3.6 哈希算法选择](#3.6 哈希算法选择)
      • [3.7 HashMap的其他一些常见用法](#3.7 HashMap的其他一些常见用法)

通用集合类型

Rust 标准库中包含一系列被称为 集合(collections)的非常有用的数据结构。

大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。

不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知并且可以随着程序的运行增长或缩小。

每种集合都有着不同能力和代价,而为所处的场景选择合适的集合则是你将要始终成长的技能。

在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合:

vector: 允许我们一个挨着一个地储存一系列数量可变的值

字符串(string):是一个字符的集合。我们之前见过 String 类型,不过在本文我们将深入了解。

哈希 map(hash map):允许我们将值与一个特定的键(key)相关联。这是一个叫做 map 的更通用的数据结构的特定实现。

1、vector

Vector (通常写作 Vec<T>) 是 Rust 标准库中最常用的集合类型之一,它是一个可增长的、堆分配的数组类型。

vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。

vector 只能储存相同类型的值。它们在拥有一系列项的场景下非常实用。

1.1 创建 Vector

使用 Vec::new() 创建空 Vector

rust 复制代码
let mut v: Vec<i32> = Vec::new(); // 需要类型注解,因为还没有插入元素,Rust 并不知道我们想要储存什么类型的元素。

这是一个非常重要的点。vector 是用泛型实现的。你需要知道的就是 Vec 是一个由标准库提供的类型,它可以存放任何类型,而当 Vec 存放某个特定类型时,那个类型位于

尖括号中。这里我们告诉 Rust v 这个 Vec 将存放 i32 类型的元素。

在更实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。

更常见的做法是使用初始值来创建一个 Vec ,而且为了方便 Rust 提供了 vec! 宏。这个宏会根据我们提供的值来创建一个新的 Vec 。

rust 复制代码
let v = vec![1, 2, 3]; // 自动推断为 Vec<i32>
let v = vec![0; 5]; // 创建包含5个0的vector: [0, 0, 0, 0, 0]

1.2 添加/更新元素

添加元素

对于新建一个 vector 并向其增加元素,可以使用 push 方法

rust 复制代码
let mut v = Vec::new();
v.push(1); // 添加元素到末尾
v.push(2);
v.push(3);

更新 Vector

除了 push 方法外,还有:

insert: 在某个位置插入元素

remove: 移出指定索引的元素

pop: 移出并返回最后一个元素

rust 复制代码
let mut v = vec![1, 2, 3];
v.insert(1, 4); // 在索引1处插入4: [1, 4, 2, 3]
v.remove(2); // 移除索引2处的元素: [1, 4, 3]
v.pop(); // 移除并返回最后一个元素: Some(3)
println!("v: {:?}", v); 

1.3 丢弃 vector 时也会丢弃其所有元素

类似于任何其他的 struct ,vector 在其离开作用域时会被释放

rust 复制代码
{
    let v = vec![1, 2, 3, 4];
    // do stuff with v
} // <- v goes out of scope and is freed here

当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。

这可能看起来非常直观,不过一旦开始使用 vector 元素的引用,情况就变得有些复杂了。

1.4 访问vector元素

有两种方法引用 vector 中储存的值。为了更加清楚的说明这个例子,我们标注这些函数返回的值的类型。

1)使用索引语法

rust 复制代码
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2]; // 索引从0开始
println!("第三个元素是 {}", third);

2)使用 get 方法

通过match结合 get(索引)获取

当 get 方法被传递了一个数组外的索引时,它不会 panic 而是返回 None

当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。

rust 复制代码
//使用get方法获取vector元素
let v = vec![1, 2, 3, 4, 5];
let five = v.get(4); // 索引从0开始
println!("第5个元素是 {:?}", five);

得到的是个Option

当下标越界时,不会报错,返回None

结合match,可以获取Option值

rust 复制代码
let v = vec![1, 2, 3, 4, 5];
match v.get(2) {
Some(third) => println!("第三个元素是 {}", third),
None => println!("没有第三个元素"),
}

索引获取与get方法获取的区别

索引语法在越界时会 panic,而 get 方法返回 Option。

使用索引获取,下标越界报错

get方法越界返回None

1.5 无效引用

一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则来确保 vector 内容的这个引用和任何其他引用保持有效。

当我们获取了 vector 的第一个元素的不可变引用,并尝试在 vector 末尾增加一个元素的时候,这是行不通的

rust 复制代码
//vector无效引用
let v = vec![1, 2, 3];
let first = &v[0]; // 创建一个对第一个元素的引用
v.push(4); // 错误:不能在有引用的情况下修改vector
println!("first: {}", first); // 使用引用

为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于vector 的工作方式。

在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。

这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。

1.6 遍历 vector 中的元素

如果想要依次访问 vector 中的每一个元素,我们可以遍历其所有的元素而无需通过索引一次一个的访问。

遍历vector分为不可变引用遍历和可变引用遍历

不可变遍历

rust 复制代码
let v = vec![100, 32, 57];
for i in &v {
    println!("{}", i);
}

可变遍历

rust 复制代码
let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50; // 使用 * 解引用并修改
}
println!("{:?}", v);

为了修改可变引用所指向的值,在使用 += 运算符之前必须使用解引用运算符( * )获取 i 中的值。

1.7 使用枚举来储存多种类型

我们提到 vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。

幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举!

这样最终就能够储存不同类型的值了

rust 复制代码
#[derive(Debug)]
enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。

第二个好处是可以准确的知道这个 vector 中允许什么类型。

如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。

使用枚举外加 match 意味着 Rust 能在编译时就保证总是会处理所有可能的情况。

如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。但是,你可以使用trait 对象。

1.8 vector容量管理

rust 复制代码
//vector容量管理
let mut v = Vec::with_capacity(10); // 预分配容量
v.push(1);
println!("长度: {}, 容量: {}", v.len(), v.capacity());

v.shrink_to_fit(); // 减少容量到刚好容纳元素
println!("长度: {}, 容量: {}", v.len(), v.capacity());

1.9 vector 与其他集合转换

rust 复制代码
//vector与其他集合转换
// 从数组创建
let arr = [1, 2, 3];
// 转换为vector
let v = arr.to_vec();
println!("v: {:?}", v);
// vector转换为数组
// let arr: [i32; 3] = v.try_into().unwrap();
// println!("arr: {:?}", arr);

// vector转换为切片
let slice = &v;
println!("slice: {:?}", slice);
// vector转换为迭代器
let iter = v.into_iter();
for i in iter {
    println!("iter: {}", i);
}

1.10 vector 常用方法

markup 复制代码
len(): 获取长度
is_empty(): 检查是否为空
contains(&value): 检查是否包含某个值
sort(): 排序
reverse(): 反转
split_off(at): 分割 vector
append(&mut other): 合并另一个 vector
dedup(): 移除连续重复元素,一般是先排序,后去重

contains使用示例:

rust 复制代码
let v = vec!["jingtian", "zhangsanfeng", "zhangwuji"];
let a = "jingtian";
if v.contains(&a) {
    println!("v contains {}", a);
} else {
    println!("v does not contain {}", a);
}

2、Rust字符串

在这之前,我们已经多次使用到了字符串了,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,

这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结

构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。

字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方

法提供了实用的功能。在这一部分,我们会讲到 String 中那些任何集合类型都有的操作,比如创建、更新和读取。也

会讨论 String 与其他集合不一样的地方,例如索引 String 是很复杂的,由于人和计算机理解 String 数据方式的不同。

2.1 什么是字符串?

Rust 的核心语言中只有一种字符串类型: str ,字符串 slice,它通常以被借用的形式出现, &str 。

它们是一些储存在别处的UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。

称作 String 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。

当 Rustacean 们谈到 Rust 的 "字符串"时,它们通常指的是 String 和字符串 slice &str 类型,而不仅仅是其中之一。

虽然本部分内容大多是关于 String 的,不过这两个类型在 Rust 标准库中都被广泛使用, String 和字符串 slice 都是 UTF-8 编码的。

Rust 标准库中还包含一系列其他字符串类型,比如 OsString 、 OsStr 、 CString 和 CStr 。相关库 crate 甚至会提供更多储存字符串数据的选择。

与 *String / *Str 的命名类似,它们通常也提供有所有权和可借用的变体,就比如说String / &str 。

这些字符串类型在储存的编码或内存表现形式上可能有所不同。

String - 可增长、可修改、拥有所有权的 UTF-8 编码字符串

&str - 固定大小的字符串切片,通常是对 String 的借用或字符串字面量

为什么 Rust 需要两种字符串类型?

Rust 的这种设计主要是为了:

性能优化:&str 是轻量级的,不需要堆分配

所有权明确:String 拥有数据,&str 只是借用

灵活性:可以在需要时选择使用拥有所有权的或借用的字符串

2.2 新建字符串

很多 Vec 可用的操作在 String 中同样可用,

1)String::new()创建

从以 new 函数创建字符串开始,

let mut s = String::new();

新建一个空的 String

这新建了一个叫做 s 的空的字符串,接着我们可以向其中装载数据。

通常字符串会有初始数据,因为我们希望一开始就有这个字符串。

为此,可以使用 to_string 方法,它能用于任何实现了 Display trait 的类型,字符串字面值就可以。

2)通过字符串字面量 to_string()创建

使用 to_string 方法从字符串字面值创建 String

rust 复制代码
let data = "initial contents";
let s = data.to_string();
// the method also works on a literal directly:
let s = "initial contents".to_string();
3)使用 String::from 创建 String

也可以使用 String::from 函数来从字符串字面值创建 String

rust 复制代码
let s = String::from("initial contents");
println!("s: {}", s);
4)使用 to_owned 方法

通过字符串字面量的to_owned()方法返回字符串String

rust 复制代码
let s3 = "initial contents".to_owned();
println!("s3: {}", s3);
5)r#原字符串

Rust的字符串字面量使用反斜杠\作为转义字符,比如\n表示换行,\t表示制表符等。但是,如果你只是想在字符串中包含一个普通的反斜杠字符,你需要用两个反斜杠\来表示。

如果字符串中包含\,直接这样表示是会报错的

rust 复制代码
//原字符串
let s = String::from("\hello");
println!("原字符串: {}", s);

如果想要在字符串中包含\,可以使用转义符

rust 复制代码
//原字符串
let s = String::from("\\hello");
println!("原字符串: {}", s);

也可以使用r#,原字符串

使用了r#"..."#来定义了一个原始字符串字面量,它允许字符串内部包含任意的字符,包括换行符、tab符号和引号等,而不需要使用转义字符。

rust 复制代码
//原字符串
let s = String::from(r#"\hello"#);
println!("原字符串: {}", s);

2.3 字符串操作

1)追加内容

push_str() 追加字符串

push() 追加单个字符

push_str的参数都是&str类型,当然&String可以自动转换为&str类型

push的参数是char类型

rust 复制代码
let mut s = String::from("hello");
// 追加字符串
s.push_str(" world");
// 追加单个字符
s.push('!');
println!("{}", s); // 输出: hello world!
2)连接字符串

使用 + 运算符或 format! 宏连接字符串
使用+连接

通常我们希望将两个已知的字符串合并在一起。一种办法是像这样使用 + 运算符

使用 + 运算符将两个 String 值合并到一个新的 String 值中

rust 复制代码
// 使用 + 运算符
let s1 = String::from("hello");
let s2 = String::from(" world");
let s3 = s1 + &s2; // 注意 s1 的所有权被移动,s1的所有权已经给了s3了。此后s1不再有效
println!("{}", s3); // 输出: hello world!

执行完这些代码之后字符串 s3 将会包含 Hello, world! 。 s1 在相加后不再有效的原因,和使用 s2 的引用的原因与使用 + 运算符时调用的方法签名有关,这个函数签名看起来像这样:

fn add(self, s: &str) -> String {

字符串运算+,相当于执行了这个add函数,字符串相加第一个必须是String,+后面的都是字符串引用

这并不是标准库中实际的签名;标准库中的 add 使用泛型定义。这里我们看到的 add 的签名使用具体类型代替了泛型,

这也正是当使用 String 值调用这个方法会发生的。后面我们会讨论泛型。这个签名提供了理解 + 运算那微妙部分的线索。

首先, s2 使用了 & ,意味着我们使用第二个字符串的 引用 与第一个字符串相加。

这是因为 add 函数的 s 参数:只能将 &str 和 String 相加,不能将两个 String 值相加。

不过等一下------正如 add 的第二个参数所指定的, &s2 的类型是 &String 而不是 &str 。

那么为示例 还能编译呢?

之所以能够在 add 调用中使用 &s2 是因为 &String 可以被 强转(coerced)成 &str ------当 add 函数被调用时,

Rust 使用了一个被称为 解引用强制多态(deref coercion)的技术,你可以将其理解为它把 &s2 变成了 &s2[...] 。

后面我们会更深入的讨论解引用强制多态。因为 add 没有获取参数的所有权,所以 s2 在这个操作后仍然是有效的 String 。

其次,可以发现签名中 add 获取了 self 的所有权,因为 self 没有 使用 & 。这意味着上面例子中的 s1 的所有权将

被移动到 add 调用中,之后就不再有效。所以虽然 let s3 = s1 + &s2; 看起来就像它会复制两个字符串并创建一个新的

字符串,而实际上这个语句会获取 s1 的所有权,附加上从 s2 中拷贝的内容,并返回结果的所有权。

换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。

使用 format! 宏

rust 复制代码
// 使用 format! 宏
let s1 = String::from("hello");
let s2 = String::from("world");
let s3 = format!("{} {}!", s1, s2);
println!("{}", s3); // 输出: hello world!
println!("{}", s1); // s1 仍然有效,因为 format! 宏不会移动 s1 的所有权
println!("{}", s2); // s2 仍然有效,因为 format! 宏不会移动 s2 的所有权

format! 与 println! 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 String 。

这个版本就好理解的多,并且不会获取任何参数的所有权。

高效连接多个字符串

rust 复制代码
fn concatenate_strings(strings: &[&str]) -> String {
    // 预先计算总长度以避免多次分配
    let total_length = strings.iter().map(|s| s.len()).sum();
    let mut result = String::with_capacity(total_length);
    
    for s in strings {
        result.push_str(s);
    }
    
    result
}

fn main() {
    let parts = ["Rust", " is", " a", " systems", " programming", " language"];
    let combined = concatenate_strings(&parts);
    println!("{}", combined); // 输出: Rust is a systems programming language
}
3)字符串长度
rust 复制代码
let s = "hello";
println!("{}", s.len()); // 输出: 5

let s = "你好";
println!("{}", s.len()); // 输出: 6 (UTF-8编码)

根据utf-8编码中每个字符占的字节来计算长度,每个汉字占3个字节

4)遍历字符串

遍历字符串可以从两个方面遍历,字符方式遍历和字节方式遍历

  1. chars方法遍历,返回每个字符
rust 复制代码
 // 按字符遍历
for c in "नमस्ते".chars() {
    println!("{}", c);
}
  1. 按字节遍历
rust 复制代码
bytes 方法返回每一个原始字节
    // 按字节遍历
    for b in "नमस्ते".bytes() {
        println!("{}", b);
    }

2.4 字符串索引和切片

在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。

然而在 Rust 中,如果我们尝试使用索引语法访问 String 的一部分,会出现一个错误。

Rust 不允许直接使用索引访问字符串中的字符,因为字符串是 UTF-8 编码的,不同语言字符可能占用 字节个数不确定。

rust 复制代码
let s1 = String::from("hello");
let h = s1[0];

错误和提示说明了全部问题:Rust 的字符串不支持索引。

可以使用字符串切片来索引

slice 允许你引用集合中一段连续的元素序列

针对非ASCII码中的字符,一定要注意边界,才能索引,否则也会报错

rust 复制代码
let s = "hello";
// 切片 (字节位置)
let slice = &s[0..2]; // "he"
// 下面的代码会 panic,因为不是字符边界
// let slice = &s[0..3]; // 可能 panic 如果 3 不是字符边界

安全使用字符串切片提取子字符串

由于不同的语言,单个字符所占的字节不同,所以用户输入的索引可能在字符中间,这样切片索引就会失败。

可以根据用户输入的是否字符边界,来判断输入的索引是否合法

is_char_boundary(index) 可以判断索引是否边界

rust 复制代码
fn extract_substring(s: &str, start: usize, end: usize) -> Option<&str> {
    // 首先检查是否是字符边界
    if !s.is_char_boundary(start) || !s.is_char_boundary(end) {
        return None;
    }
    
    // 然后检查范围是否有效
    if start > end || end > s.len() {
        return None;
    }
    
    Some(&s[start..end])
}

fn main() {
    let s = "Hello, 世界!";
    
    match extract_substring(s, 7, 13) {
        Some(sub) => println!("Substring: {}", sub), // 输出: 世界
        None => println!("Invalid substring range"),
    }
}

2.5 字符串常用方法

1)检查方法
rust 复制代码
let s = String::from("hello");

// 检查是否为空
println!("{}", s.is_empty()); // false

// 检查是否包含子串
println!("{}", s.contains("ell")); // true

// 检查是否以某字符串开头/结尾
println!("{}", s.starts_with("he")); // true
println!("{}", s.ends_with("lo")); // true
2)转换方法
rust 复制代码
let s = String::from("Hello World");

// 转换为大写/小写
println!("{}", s.to_lowercase()); // "hello world"
println!("{}", s.to_uppercase()); // "HELLO WORLD"

// 转换为字符串切片
let slice: &str = &s;

// 转换为字节数组
let bytes = s.as_bytes();
3)分割和拼接
rust 复制代码
let s = "hello world";

// 分割字符串,以空格为分隔符
for word in s.split_whitespace() {
    println!("{}", word);
}

// 拼接字符串,将字符串数组连接成一个字符串。join的参数是合并后的单词分隔符
let words = ["hello", "world"];
let joined = words.join(" ");
println!("{}", joined); // "hello world"
4)替换和修剪
rust 复制代码
let s = " hello world ";

// 修剪空白字符,去除两边的空白符
println!("{}", s.trim()); // "hello world"

// 替换
let replaced = s.replace("world", "Rust");
println!("{}", replaced); // " hello Rust "

2.6 字符串与其它类型的转换

1)数字与字符串
rust 复制代码
// 数字转字符串
let num = 42;
let num_str = num.to_string();
println!("num_str1: {}", num_str);
// 或使用 format! 宏
let num_str = format!("{}", num);
println!("num_str2: {}", num_str);

// 字符串转数字
let num_str = "42";
let num: i32 = num_str.parse().unwrap();
println!("num: {}", num);

// 或使用 turbofish 语法
let num = num_str.parse::<i32>().unwrap();
println!("num: {}", num);
2)路径与字符串

使用到路径Path库

rust 复制代码
use std::path::Path;

let path = Path::new("/tmp/foo.txt");
let path_str = path.to_str().unwrap();

2.7 字符串格式化

1)使用 format! 宏
rust 复制代码
let name = "Alice";
let age = 30;
let s = format!("{} is {} years old", name, age);
println!("{}", s);
2)使用 println! 宏
rust 复制代码
println!("{} is {} years old", name, age);

// 格式化数字
let pi = 3.1415926;
println!("{:.2}", pi); // 3.14

2.8 处理 UTF-8 字符串

Rust 字符串严格使用 UTF-8 编码,这带来了一些特殊考虑,字符边界问题

rust 复制代码
s.chars().count()  获取字符个数
s.chars().nth(n).unwrap() 获取第n个字符

let s = "नमस्ते";

// 获取字符数量 (不是字节数)
println!("{}", s.chars().count()); // 6

// 获取第 n 个字符
let third_char = s.chars().nth(2).unwrap();
println!("{}", third_char); // 'म'

3、 hashmap 储存键值对

3.1 HashMap 简介

HashMap 是 Rust 标准库中提供的键值对集合类型,基于哈希表实现。它提供了高效的数据查找、插入和删除操作。

它存储键值对并提供了平均时间复杂度为 O(1) 的查找、插入和删除操作。

HashMap<K, V> 类型储存了一个键类型 K 对应一个值类型 V 的映射。

它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。

HashMap<K, V> 存储的是 K 类型的键和 V 类型的值的映射关系。

它要求键类型 K 实现了 Eq 和 Hash trait,而值类型 V 可以是任意类型。

例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。

给出一个队名,就能得到它们的得分。

HashMap特性

  1. 快速查找:平均 O(1) 的时间复杂度,适用于需要频繁插入、删除和查找键值对的场景。
  2. 无序:键值对的顺序不保证,哈希表的存储顺序是由哈希值决定的。
  3. 可以动态调整大小,以应对负载因子变化。

HAashMap的桶与槽

在哈希表(例如 Rust 的 HashMap)的实现中,桶(bucket)是用来存储键值对的容器或位置。

每个桶存储着哈希表中一个特定的"槽"中的元素,多个元素可能会被存放到同一个桶中。

这种设计的目的是为了优化哈希表的性能,使得查找、插入和删除操作能够在常数时间复杂度(O(1))内完成。

如何理解桶?
哈希函数 :当你插入一个键值对时,哈希表首先会通过一个哈希函数计算出该键的哈希值。哈希值是一个整数,表示该键在哈希表中的位置。
桶的数量 :哈希表通常将桶的数量设置为一个固定的大小,这个大小可能是基于哈希表的当前大小动态扩展的。哈希表会根据哈希值将键映射到相应的桶。
桶的作用 :每个桶是一个容器,桶中可以存放一个或多个键值对。哈希表通过哈希值来确定要查找哪个桶,进而查找该桶中的键值对。
桶冲突(碰撞) :当多个键的哈希值相同,或者它们的哈希值被映射到相同的桶时,就会发生哈希碰撞。

为了处理这种情况,哈希表通常会使用一种方法来将多个元素存储在同一个桶中,常见的解决方案有:
链式法(Chaining) :每个桶实际上是一个链表(或者其他数据结构),多个键值对就可以在同一个桶中按链表的形式存储。
开放寻址法(Open Addressing) :当发生碰撞时,哈希表会寻找另一个空桶来存储这个元素。
多次哈希 :如果下标位置已被占用,就用另外一个hash函数计算新的下标位置。当然理论上来讲,第二个hash函数算出的下标位置仍然可能已经被占用。

工程实践上,前2种方式比较容易实现。比如Java中的HashMap是用链地址法处理哈希冲突;Rust中的HashMap是用开放寻址法中的二次寻址方式处理哈希冲突。

为什么桶重要?

桶的设计使得哈希表能够有效地组织数据并在平均常数时间内执行操作。如果没有桶,哈希表可能会退化成一个线性查找的结构,失去哈希表的效率优势。

在 Rust 的 HashMap 中,桶的数量和哈希函数一起决定了哈希表的性能。当碰撞较多时,哈希表可能会进行扩展,增加桶的数量,以保持较好的性能。

HashMap是无序的,如果要使用有序的map,可以使用BTreeMap

use std::collections::BTreeMap;

-实现

基于平衡二叉树(通常是红黑树)实现,确保键按顺序排列。

特性

  1. 有序:键值对按键的排序顺序存储(默认是升序),可以进行范围查询等操作。
  2. 查找性能较差:查找、插入、删除的时间复杂度是 O(log n),比哈希表稍慢。
  3. 适用于需要键有顺序的场景,如按键排序或按范围查询。

3.2 创建 HashMap 的多种方式

创建hashmap的时候,首先需要将hashmap的库导入进来

rust 复制代码
use std::collections::HashMap;
1)使用 new() 创建

指定hashmap的键值类型:HashMap<键的类型, 值的类型>

当然,也可以根据插入的键值进行自动推导

rust 复制代码
let mut map: HashMap<String, i32> = HashMap::new();
2)使用 with_capacity() 预分配空间
rust 复制代码
//预分配空间
let mut map = HashMap::with_capacity(100); // 预先分配空间,减少扩容次数
for i in 0..100 {
    map.insert(i, i);
}
println!("{:?}", map);
3)从元组向量创建
rust 复制代码
let tuples = vec![("a", 1), ("b", 2), ("c", 3)];
let map: HashMap<_, _> = tuples.into_iter().collect();
4)从数组创建创建
rust 复制代码
//从数组创建HashMap
let arr = [("a", 1), ("b", 2), ("c", 3)];
let map: HashMap<_, _> = arr.iter().cloned().collect();
println!("{:?}", map);
5)使用迭代器创建
rust 复制代码
let keys = vec!["a", "b", "c"];
let values = vec![1, 2, 3];
let map: HashMap<_, _> = keys.iter().zip(values.iter()).collect();

3.3 插入和更新值

1)基本插入
rust 复制代码
let mut scores = HashMap::new();
scores.insert("Blue", 10);
scores.insert("Yellow", 50);
println!("{:?}", scores);
2)更新已有值

对于已经存在的键,执行插入操作就是更新已有值

rust 复制代码
scores.insert("Blue", 25); // 覆盖原来的值10
3)只在键不存在时插入
rust 复制代码
scores.entry("Blue").or_insert(30); // 不会改变,因为Blue已存在
scores.entry("Red").or_insert(50);  // 会插入Red:50
4)根据旧值更新一个值

另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它

例如: 计数一些文本中每一个单词分别出现了多少次。我们使用哈希 map 以单词作为键并递增其值来记录我们遇到过几次这个单词。

如果是第一次看到某个单词,就插入值 0 。

rust 复制代码
let text = "hello world wonderful world";

let mut map = HashMap::new();

for word in text.split_whitespace() {
    //这里使用的是entry().or_insert() 。只有单词第一次出现的时候,才将该键插入到map,后续就不再继续插进去修改,而是通过修改count来增加次数
    let count = map.entry(word).or_insert(0);
    //只要单词出现过一次,就将count加上1,从而可以统计单词出现的次数
    *count += 1;
}

println!("{:?}", map); // 输出: {"world": 2, "hello": 1, "wonderful": 1}

通过哈希 map 储存单词和计数来统计出现次数

or_insert 方法事实上会返回这个键的值的一个可变引用( &mut V )。

这里我们将这个可变引用储存在 count 变量中,所以为了赋值必须首先使用星号( * )解引用 count 。

这个可变引用在 for 循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。

3.4 访问值

1)使用 get 方法

get的参数是个引用

返回的是Option<&V>

rust 复制代码
//访问值
let team_name = "Blue";
let score = scores.get(team_name); // 返回Option<&i32>
println!("{}: {:?}", team_name, score);
//使用match语句处理Option
match score {
    Some(s) => println!("Score: {}", s),
    None => println!("Team not found"),
}

可以使用功能if let简化

rust 复制代码
//访问值
let team_name = "Blue";
let score = scores.get(team_name); // 返回Option<&i32>
println!("{}: {:?}", team_name, score);
//使用match语句处理Option
if let Some(s) = score {
    println!("Score: {}", s);
} else {
    println!("Team not found");
}
2)使用 get_mut 获取可变引用
rust 复制代码
//使用get_mut方法获取可变引用,可用来修改值
if let Some(score) = scores.get_mut("Blue") {
    *score += 10;
}

可以看到Blue的值加了10

3)遍历hashmap

注意:遍历使用的是hashmap的引用,如果不使用引用,此时的hashmap就被借用,后续就不可以再用这个hashmap了

rust 复制代码
//遍历HashMap
for (key, value) in &scores {
    println!("{}: {}", key, value);
}

3.5 删除操作

1)使用 remove 删除

返回被删除的值Option<V>

rust 复制代码
//删除键值对
let a = scores.remove("Blue"); // 返回被删除的值Option<V>
println!("Removed: {:?}", a);
println!("scores: {:?}", scores);
2)使用 remove_entry 删除并返回键值对
rust 复制代码
if let Some((team, score)) = scores.remove_entry("Blue") {
    println!("Removed {} with score {}", team, score);
}

3.6 哈希算法选择

Rust 默认使用加密安全的哈希算法,但有时我们需要更快的哈希算法。

需要先安装包

bash 复制代码
cargo add twox_hash
rust 复制代码
use std::collections::HashMap;
use std::hash::BuildHasherDefault;
use twox_hash::XxHash64;

type FastHashMap<K, V> = HashMap<K, V, BuildHasherDefault<XxHash64>>;

fn main() {
    let mut map: FastHashMap<&str, i32> = FastHashMap::default();
    map.insert("foo", 42);
}

使用更换过hash算法的类型创建hashmap

3.7 HashMap的其他一些常见用法

1)获取长度 - len()
rust 复制代码
println!("Number of teams: {}", scores.len());
2)检查是否为空 - is_empty()
rust 复制代码
if scores.is_empty() {
    println!("No teams registered!");
}
3)清空 HashMap - clear()
rust 复制代码
scores.clear(); // 移除所有键值对
4)获取键或值的集合
rust 复制代码
let teams: Vec<_> = scores.keys().collect();
let points: Vec<_> = scores.values().collect();
println!("teams: {:?}", teams);
println!("points: {:?}", points);
5)检查键是否存在
rust 复制代码
//检查键是否存在
let score = scores.get("Yellow"); // 返回Option<&V>
if score.is_some() {
    println!("Yellow team exists");
} else {
    println!("Yellow team does not exist");
}

//使用contains_key方法检查键是否存在
if scores.contains_key("Yellow") {
    println!("Yellow team exists");
} else {
    println!("Yellow team does not exist");
}
相关推荐
方圆想当图灵5 分钟前
关于 Nacos 在 war 包部署应用关闭部分资源未释放的原因分析
后端
love530love11 分钟前
命令行创建 UV 环境及本地化实战演示—— 基于《Python 多版本与开发环境治理架构设计》的最佳实践
开发语言·人工智能·windows·python·conda·uv
Lemon程序馆16 分钟前
今天聊聊 Mysql 的那些“锁”事!
后端·mysql
龙卷风040518 分钟前
使用本地IDEA连接服务器远程构建部署Docker服务
后端·docker
vv安的浅唱22 分钟前
Golang基础笔记七之指针,值类型和引用类型
后端·go
陪我一起学编程33 分钟前
MySQL创建普通用户并为其分配相关权限的操作步骤
开发语言·数据库·后端·mysql·oracle
麦子邪36 分钟前
C语言中奇技淫巧04-仅对指定函数启用编译优化
linux·c语言·开发语言
破刺不会编程1 小时前
linux线程概念和控制
linux·运维·服务器·开发语言·c++
henreash1 小时前
NLua和C#交互
开发语言·c#·交互