【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.2 添加/更新元素
      • 1.3 丢弃 vector 时也会丢弃其所有元素
      • 1.4 访问vector元素
      • 1.5 无效引用
      • 1.6 遍历 vector 中的元素
      • 1.7 使用枚举来储存多种类型
      • 1.8 vector容量管理
      • 1.9 vector 与其他集合转换
      • 1.10 vector 常用方法
    • 2、Rust字符串
      • 2.1 什么是字符串?
      • 2.2 新建字符串
        • 1)String::new()创建
        • 2)通过字符串字面量 to_string()创建
        • 3)使用 String::from 创建 String
        • 4)使用 to_owned 方法
        • 5)r#原字符串
      • 2.3 字符串操作
        • 1)追加内容
        • 2)连接字符串
        • 3)字符串长度
        • 4)遍历字符串
      • 2.4 字符串索引和切片
      • 2.5 字符串常用方法
        • 1)检查方法
        • 2)转换方法
        • 3)分割和拼接
        • 4)替换和修剪
      • 2.6 字符串与其它类型的转换
        • 1)数字与字符串
        • 2)路径与字符串
      • 2.7 字符串格式化
        • 1)使用 format! 宏
        • 2)使用 println! 宏
      • 2.8 处理 UTF-8 字符串
    • 3、 hashmap 储存键值对
      • 3.1 HashMap 简介
      • 3.2 创建 HashMap 的多种方式
        • 1)使用 new() 创建
        • 2)使用 with_capacity() 预分配空间
        • 3)从元组向量创建
        • 4)从数组创建创建
        • 5)使用迭代器创建
      • 3.3 插入和更新值
        • 1)基本插入
        • 2)更新已有值
        • 3)只在键不存在时插入
        • 4)根据旧值更新一个值
      • 3.4 访问值
        • 1)使用 get 方法
        • 2)使用 get_mut 获取可变引用
        • 3)遍历hashmap
      • 3.5 删除操作
        • 1)使用 remove 删除
        • 2)使用 remove_entry 删除并返回键值对
      • 3.6 哈希算法选择
      • 3.7 HashMap的其他一些常见用法
        • 1)获取长度 - len()
        • 2)检查是否为空 - is_empty()
        • 3)清空 HashMap - clear()
        • 4)获取键或值的集合
        • 5)检查键是否存在

通用集合类型

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

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

这是一个非常重要的点。vector 是用泛型实现的。你需要知道的就是 Vec 是一个由标准库提供的类型,它可以存放任何类型,而当 Vec 存放某个特定类型时,那个类型位于
尖括号中。这里我们告诉 Rust v 这个 Vec 将存放 i32 类型的元素。

在更实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。
更常见的做法是使用初始值来创建一个 Vec ,而且为了方便 Rust 提供了 vec! 宏。这个宏会根据我们提供的值来创建一个新的 Vec 。

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 方法

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

在这里插入图片描述

更新 Vector
除了 push 方法外,还有:
insert: 在某个位置插入元素
remove: 移出指定索引的元素
pop: 移出并返回最后一个元素

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 在其离开作用域时会被释放

{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)使用索引语法

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

在这里插入图片描述

2)使用 get 方法
通过match结合 get(索引)获取
当 get 方法被传递了一个数组外的索引时,它不会 panic 而是返回 None
当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。

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

在这里插入图片描述

得到的是个Option
在这里插入图片描述

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

结合match,可以获取Option值

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 末尾增加一个元素的时候,这是行不通的

//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分为不可变引用遍历和可变引用遍历
不可变遍历

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

在这里插入图片描述

可变遍历

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

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

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

我们提到 vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。
幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举!
这样最终就能够储存不同类型的值了

#[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容量管理

//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 与其他集合转换

//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 常用方法

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

contains使用示例:

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

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

let s = String::from("initial contents");
println!("s: {}", s);

在这里插入图片描述

4)使用 to_owned 方法

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

let s3 = "initial contents".to_owned();
println!("s3: {}", s3);

在这里插入图片描述

5)r#原字符串

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

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

在这里插入图片描述

2.3 字符串操作

1)追加内容

push_str() 追加字符串
push() 追加单个字符
push_str的参数都是&str类型,当然&String可以自动转换为&str类型
在这里插入图片描述

push的参数是char类型
在这里插入图片描述

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

在这里插入图片描述

2)连接字符串

使用 + 运算符或 format! 宏连接字符串
使用+连接
通常我们希望将两个已知的字符串合并在一起。一种办法是像这样使用 + 运算符
使用 + 运算符将两个 String 值合并到一个新的 String 值中

// 使用 + 运算符
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! 宏

// 使用 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 。
这个版本就好理解的多,并且不会获取任何参数的所有权。
在这里插入图片描述

高效连接多个字符串

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)字符串长度
let s = "hello";
println!("{}", s.len()); // 输出: 5let s = "你好";
println!("{}", s.len()); // 输出: 6 (UTF-8编码)

根据utf-8编码中每个字符占的字节来计算长度,每个汉字占3个字节
在这里插入图片描述

4)遍历字符串

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

  1. chars方法遍历,返回每个字符
 // 按字符遍历
for c in "नमस्ते".chars() {println!("{}", c);
}

在这里插入图片描述

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

在这里插入图片描述

2.4 字符串索引和切片

在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。
然而在 Rust 中,如果我们尝试使用索引语法访问 String 的一部分,会出现一个错误。
Rust 不允许直接使用索引访问字符串中的字符,因为字符串是 UTF-8 编码的,不同语言字符可能占用 字节个数不确定。

let s1 = String::from("hello");
let h = s1[0];

错误和提示说明了全部问题:Rust 的字符串不支持索引。
在这里插入图片描述

可以使用字符串切片来索引
slice 允许你引用集合中一段连续的元素序列
针对非ASCII码中的字符,一定要注意边界,才能索引,否则也会报错

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

在这里插入图片描述

安全使用字符串切片提取子字符串
由于不同的语言,单个字符所占的字节不同,所以用户输入的索引可能在字符中间,这样切片索引就会失败。
可以根据用户输入的是否字符边界,来判断输入的索引是否合法
is_char_boundary(index) 可以判断索引是否边界
在这里插入图片描述

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)检查方法
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)转换方法
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)分割和拼接
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)替换和修剪
let s = " hello world ";// 修剪空白字符,去除两边的空白符
println!("{}", s.trim()); // "hello world"// 替换
let replaced = s.replace("world", "Rust");
println!("{}", replaced); // " hello Rust "

在这里插入图片描述

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

1)数字与字符串
// 数字转字符串
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库

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

在这里插入图片描述

2.7 字符串格式化

1)使用 format! 宏
let name = "Alice";
let age = 30;
let s = format!("{} is {} years old", name, age);
println!("{}", s);
2)使用 println! 宏
println!("{} is {} years old", name, age);// 格式化数字
let pi = 3.1415926;
println!("{:.2}", pi); // 3.14

在这里插入图片描述

2.8 处理 UTF-8 字符串

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

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的库导入进来

use std::collections::HashMap;
1)使用 new() 创建

指定hashmap的键值类型:HashMap<键的类型, 值的类型>
当然,也可以根据插入的键值进行自动推导

let mut map: HashMap<String, i32> = HashMap::new();
2)使用 with_capacity() 预分配空间
//预分配空间
let mut map = HashMap::with_capacity(100); // 预先分配空间,减少扩容次数
for i in 0..100 {map.insert(i, i);
}
println!("{:?}", map);

在这里插入图片描述

3)从元组向量创建
let tuples = vec![("a", 1), ("b", 2), ("c", 3)];
let map: HashMap<_, _> = tuples.into_iter().collect();

在这里插入图片描述

4)从数组创建创建
//从数组创建HashMap
let arr = [("a", 1), ("b", 2), ("c", 3)];
let map: HashMap<_, _> = arr.iter().cloned().collect();
println!("{:?}", map);

在这里插入图片描述

5)使用迭代器创建
let keys = vec!["a", "b", "c"];
let values = vec![1, 2, 3];
let map: HashMap<_, _> = keys.iter().zip(values.iter()).collect();

在这里插入图片描述

3.3 插入和更新值

1)基本插入
let mut scores = HashMap::new();
scores.insert("Blue", 10);
scores.insert("Yellow", 50);
println!("{:?}", scores);

在这里插入图片描述

2)更新已有值

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

scores.insert("Blue", 25); // 覆盖原来的值10

在这里插入图片描述

3)只在键不存在时插入
scores.entry("Blue").or_insert(30); // 不会改变,因为Blue已存在
scores.entry("Red").or_insert(50);  // 会插入Red:50

在这里插入图片描述

4)根据旧值更新一个值

另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它
例如: 计数一些文本中每一个单词分别出现了多少次。我们使用哈希 map 以单词作为键并递增其值来记录我们遇到过几次这个单词。
如果是第一次看到某个单词,就插入值 0 。

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>

//访问值
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简化

//访问值
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 获取可变引用
//使用get_mut方法获取可变引用,可用来修改值
if let Some(score) = scores.get_mut("Blue") {*score += 10;
}

可以看到Blue的值加了10
在这里插入图片描述

3)遍历hashmap

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

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

在这里插入图片描述

3.5 删除操作

1)使用 remove 删除

返回被删除的值Option<V>

//删除键值对
let a = scores.remove("Blue"); // 返回被删除的值Option<V>
println!("Removed: {:?}", a);
println!("scores: {:?}", scores);

在这里插入图片描述

2)使用 remove_entry 删除并返回键值对
if let Some((team, score)) = scores.remove_entry("Blue") {println!("Removed {} with score {}", team, score);
}

在这里插入图片描述

3.6 哈希算法选择

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

cargo add twox_hash
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()
println!("Number of teams: {}", scores.len());
2)检查是否为空 - is_empty()
if scores.is_empty() {println!("No teams registered!");
}
3)清空 HashMap - clear()
scores.clear(); // 移除所有键值对

在这里插入图片描述

4)获取键或值的集合
let teams: Vec<_> = scores.keys().collect();
let points: Vec<_> = scores.values().collect();
println!("teams: {:?}", teams);
println!("points: {:?}", points);

在这里插入图片描述

5)检查键是否存在
//检查键是否存在
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");
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/80805.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SoapUi测试1——REST(WebAPi、Json协议/HTTP、Post通讯方式)接口测试

文章目录 1背景1.1接口测试工具SoapUi产生背景1.2常见接口类型1.3接口包含内容1.4请求格式 2软件使用3http、webservice、webapi如何测试3.1REST&#xff08;WebAPi、JSON/HTTP、POST&#xff09;3.2SOAP&#xff08;Webserver、XML/HTTP、POST&#xff09; 1背景 1.1接口测试…

Linux按键驱动测试

文章目录 一、设备节点添加 二、创建驱动文件代码 2.1 核心数据结构 2.2 按键值定义 2.3 关键函数实现 三、创建测试文件 四、测试 一、设备节点添加 首先在设备树文件中添加pinctrl以及在根目录下添加设备节点。如下&#xff1a; //创建按键输入的pinctrlpinctrl_key: keygrp…

5000元可以运行32B大模型的笔记本

5000元可以运行32B 大模型的笔记本 荣耀笔记本 X14 Plus 锐龙版 R7-8845HS -32G -1T 模型名称 模型大小 tokens/s qwq-32b-q4 19GB 2.4 Qwen2.5-Coder-14B- Q8 16GB 4 DeepSeek-R1-Distill-Qwen-7B-Q8 8GB 8.1 DeepSeek-R1-Distill-Llama-8B-Q4 5GB 11.7

arm设备树基础知识

文章目录 前言dts片段通用属性介绍地址大小中断phandlecompatible mmc节点介绍 前言 arm开发&#xff0c;早晚要了解设备树 dts片段 interrupt-parent <0x8005>; model "linux,dummy-virt"; #size-cells <0x02>; #address-cells <0x02>; co…

【C++ 核心知识点面试攻略:从基础到实战(上位机开发视角)】

一、命名空间&#xff08;Namespace&#xff09;相关问题 问题1&#xff1a;C引入命名空间的核心目的是什么&#xff1f;如何通过命名空间解决命名冲突&#xff1f; 答案&#xff1a; C引入命名空间的核心目的是 避免全局作用域中的命名冲突&#xff0c;通过将变量、函数、类…

线性代数与数据学习

The Functions of Deep Learning (essay from SIAM News, December 2018) Deep Learning and Neural Nets

phpstorm用php连接数据库报错

项目场景&#xff1a; phpstorm用php连接数据库 问题描述 用php使用mysql_connect 的时候报错了&#xff0c;没有这个函数 原因分析&#xff1a; php解释器问题&#xff0c;后来查资料得知mysql_connct只适用于php5.5以下解释器。一开始用的7&#xff0c;改成5.3以后还是报…

51c大模型~合集122

我自己的原文哦~ https://blog.51cto.com/whaosoft/13877107 #PHYBench 北大物院200人合作&#xff0c;金牌得主超50人&#xff01;PHYBench&#xff1a;大模型究竟能不能真的懂物理&#xff1f; 本项目由北京大学物理学院朱华星老师、曹庆宏副院长统筹指导。基准设计、…

单片机 + 图像处理芯片 + TFT彩屏 触摸滑动条控件

触摸滑动条控件使用说明 一、项目概述 本项目基于单片机和RA8889/RA6809图形处理芯片的TFT触摸屏滑动条控件。该控件支持水平和垂直滑动条&#xff0c;可自定义外观和行为&#xff0c;并支持回调函数进行值变化通知。 硬件平台&#xff1a;51/ARM均可(测试时使用STC8H8K64U单…

linux离线安装zsh

下载zsh 下载仓库后解压 下载地址&#xff1a;https://github.com/zsh-users/zsh 离线安装 安装方法见INSTALL文件 ./configure --prefix[/usr/local] make make install

机器学习中的数据转换:关键步骤与最佳实践

机器学习中的数据转换&#xff1a;关键步骤与最佳实践 摘要 &#xff1a;在机器学习领域&#xff0c;数据是模型的核心&#xff0c;而数据的转换是构建高效、准确模型的关键步骤之一。本文深入探讨了机器学习中数据转换的重要性、常见的数据类型及其转换方法&#xff0c;以及在…

TDR阻抗会爬坡? 别担心,不是你的错,你只是不够了解TDR!

在背板系统或任何长走线设计里&#xff0c;你大概都碰过这画面&#xff1a; TDR 曲线一开始乖乖在 92 Ω&#xff0c;但越往末端、阻抗越爬越高&#xff0c;来到最高 97 Ω&#xff0c;心里瞬间凉半截 &#x1f612; &#xff0c;「难不成... 板厂又翻车了吗&#xff1f;」 然…

在另外一台可以科学下载的电脑用ollama下载模型后,怎么导入到另外一台服务器的ollama使用

环境&#xff1a; Win10专业版 Ubuntu20.04 问题描述&#xff1a; 在另外一台可以科学下载的电脑用ollama下载模型后&#xff0c;怎么导入到另外一台服务器的ollama使用&#xff0c;原电脑win10上的ollama下载的模型,复制到ubuntu20.04的ollama上推理 解决方案&#xff1a;…

Ethan独立开发产品日报 | 2025-04-27

1. CreateWise AI 旨在提升你工作效率的AI播客编辑器 人工智能播客编辑器&#xff0c;让你的播客制作速度提升10倍&#xff01;它可以自动去除口头语和沉默&#xff0c;生成节目笔记和精彩片段&#xff0c;还能一键制作适合社交媒体分享的短视频——所有这些功能都只需一次点…

解决 shadui组件库Popover 点击后会消失

react用了shadui组件库 <Popover><PopoverTrigger><div className"text-operation-item" onClick{props.callback}><img src{props.imgSrc} width{20} height{20} /></div></PopoverTrigger><PopoverContent className"…

SVC电气设备作用

SVC&#xff08;Static Var Compensator&#xff0c;静止无功补偿器&#xff09;是一种基于电力电子技术的动态无功补偿装置&#xff0c;属于灵活交流输电系统&#xff08;FACTS&#xff09;的核心设备之一。它通过快速调节电网中的无功功率&#xff0c;改善电能质量、稳定系统…

黑马点评商户查询缓存--缓存更新策略

ShopTypeServiceImpl类 代码 package com.hmdp.service.impl;import cn.hutool.json.JSONUtil; import com.hmdp.dto.Result; import com.hmdp.entity.ShopType; import com.hmdp.mapper.ShopTypeMapper; import com.hmdp.service.IShopTypeService; import com.baomidou.myba…

C 语言函数指针与指针函数详解

一、引言 在 C 语言的编程世界中&#xff0c;函数指针和指针函数是两个既强大又容易混淆的概念。它们为 C 语言带来了更高的灵活性和可扩展性&#xff0c;广泛应用于回调函数、动态链接库、状态机等多种场景。深入理解和掌握函数指针与指针函数&#xff0c;对于提升 C 语言编程…

HTML5 新特性详解:语义化标签、表单与音视频嵌入

前言 HTML5作为当前Web开发的核心技术&#xff0c;为开发者提供了更强大、更语义化的工具集。本文将深入探讨HTML5的三大核心特性&#xff1a;语义化标签、增强的表单功能以及原生的音视频支持&#xff0c;帮助开发者构建更现代化、更易维护的网页应用。 一、HTML5语义化标签…

利用HandlerMethodArgumentResolver和注解解析封装用户信息和Http参数

获取用户身份信息详情注解 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/*** 获取用户身份信息详情注解*/ Retention(RetentionPolicy.RUNTIME) Tar…