[toc]
统计相关
随机数生成
rand 0.9.x 要设置种子且生成随机数
在 rand 0.9.x 版本中,API 变得更加语义化。要实现“设置种子”并“生成随机数”,最标准且完整的代码如下。
- 配置文件 (
Cargo.toml)
确保你使用的是 0.9 版本。如果你需要生成布朗运动常用的正态分布,建议加上 rand_distr。
1
2
3
[dependencies]
rand = "0.9"
rand_distr = "0.5" # 用于正态分布等复杂分布
- 完整实现代码
这是在 0.9.x 中使用 StdRng 设置种子并生成随机数的标准范例:
Rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use rand::{Rng, SeedableRng}; // 必须导入这两个 Trait
use rand::rngs::StdRng;
use rand_distr::{Normal, Distribution};
fn main() {
// --- 1. 设置种子 ---
// 方式 A: 使用简单的 u64 数字作为种子 (最常用)
let mut rng = StdRng::seed_from_u64(42);
// 方式 B: 使用 32 字节数组 (更底层/精确)
// let seed: [u8; 32] = [0u8; 32];
// let mut rng = StdRng::from_seed(seed);
// --- 2. 生成随机数 ---
// 生成基础随机数 (0.9 版本推荐使用 .random() 代替 .gen())
let n1: f64 = rng.random();
println!("0.0 到 1.0 之间的随机数: {}", n1);
// 生成指定范围内的随机数
let n2: i32 = rng.random_range(1..=100);
println!("1 到 100 之间的整数: {}", n2);
// --- 3. 结合布朗运动场景 (正态分布) ---
// 生成均值为 0,标准差为 1 的正态分布随机数
let normal = Normal::new(0.0, 1.0).unwrap();
let v: f64 = normal.sample(&mut rng);
println!("正态分布随机数 (用于布朗运动步长): {}", v);
}
rand0.9.x 的关键变化总结
为了方便你记忆,这里对比了新旧版本的核心差异:
| 功能 | 0.8.x 及旧版本 | 0.9.x 新版本 |
|---|---|---|
| 基础生成方法 | rng.gen::<f64>() |
rng.random::<f64>() |
| 范围生成方法 | rng.gen_range(1..10) |
rng.random_range(1..10) |
| 从系统获取随机种子 | StdRng::from_entropy() |
StdRng::from_os_rng() |
| 获取线程局部生成器 | rand::thread_rng() |
rand::rng() |
| 种子 Trait | SeedableRng |
SeedableRng (保持不变) |
- 为什么在结构体中使用
StdRng?
在你的布朗运动结构体中使用 StdRng 是正确的选择,原因有二:
- 确定性:给定相同的种子,
StdRng在所有平台上产生的随机序列是一致的,这对金融建模和科学计算的“可复现性”至关重要。 - 性能与质量:它比
SmallRng更安全,比直接使用系统调用更快。
二元标准正态累积分布函数
计算相关系数为 $\rho$ 的两个标准正态随机变量 $\xi_1$ 和 $\xi_2$ 同时满足 $\xi_1 < a$ 且 $\xi_2 < b$ 的概率 $P(\xi_1 < a, \xi_2 < b)$
owens-t crate 提供了 Owen’s T 函数的实现,该函数可以用来计算二元标准正态 CDF。
1
2
[dependencies]
owens-t = "0.1" # 检查 crates.io 获取最新版本
owens-t crate 中的 biv_norm 函数可以计算 $P(X > x, Y > y)$。
由于 $\xi_1$ 和 $\xi_2$ 是关于原点对称的,并且 $P(\xi_1 < a, \xi_2 < b)$ 是累积分布函数 (CDF),您可以利用以下关系将您所需的 $P(\xi_1 < a, \xi_2 < b)$ 转换为由 biv_norm 计算的概率:
-
一元标准正态 CDF: $\Phi(x) = P(\xi < x)$。
- $P(\xi > x) = 1 - \Phi(x)$
-
包含关系: $P(\xi_1 < a, \xi_2 < b) = 1 - P(\xi_1 > a) - P(\xi_2 > b) + P(\xi_1 > a, \xi_2 > b)$
-
注意到 $P(\xi_1 > a)$ 和 $P(\xi_2 > b)$ 可以通过一元标准正态 CDF 计算:
\[P(\xi_1 > a) = 1 - \Phi(a)\] \[P(\xi_2 > b) = 1 - \Phi(b)\] -
$P(\xi_1 > a, \xi_2 > b)$ 则通过 biv_norm 计算:
\[P(\xi_1 > a, \xi_2 > b) = \text{biv\_norm}(a, b, \rho)\]
-
因此,您所需的概率为:
\[P(\xi_1 < a, \xi_2 < b) = 1 - (1 - \Phi(a)) - (1 - \Phi(b)) + \text{biv\_norm}(a, b, \rho)\] \[\mathbf{P(\xi_1 < a, \xi_2 < b) = \Phi(a) + \Phi(b) - 1 + \text{biv\_norm}(a, b, \rho)}\]您还需要一个计算一元标准正态 CDF $\Phi(x)$ 的函数,例如可以使用 statrs crate 中的 Normal::standard().cdf(x)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use owens_t;
// 还需要一个用于标准正态 CDF (Phi(x)) 的库,例如 statrs
// Cargo.toml 中需要添加 statrs = "0.18" (或其他版本)
use statrs::distribution::{ContinuousCDF, Normal};
/// 计算 P(xi_1 < a, xi_2 < b)
fn bivariate_standard_normal_cdf_via_owens(a: f64, b: f64, rho: f64) -> f64 {
// 1. 获取标准正态 CDF 函数
let standard_normal = Normal::new(0.0, 1.0).unwrap();
let phi_a = standard_normal.cdf(a);
let phi_b = standard_normal.cdf(b);
// 2. 使用 owens_t::biv_norm 计算 P(xi_1 > a, xi_2 > b)
// biv_norm(a, b, rho) 计算 P(X > a, Y > b)
let p_gt_a_gt_b = owens_t::biv_norm(a, b, rho);
// 3. 应用概率恒等式: P(A, B) = 1 - P(A^c) - P(B^c) + P(A^c, B^c)
// P(xi_1 < a, xi_2 < b) = 1 - P(xi_1 > a) - P(xi_2 > b) + P(xi_1 > a, xi_2 > b)
// P(xi_1 < a, xi_2 < b) = phi_a + phi_b - 1 + p_gt_a_gt_b
// 注意: biv_norm 的参数是 f64,直接传递 a, b 即可
phi_a + phi_b - 1.0 + p_gt_a_gt_b
}
fn main() {
let a = 1.0;
let b = 1.0;
let rho = 0.5;
let probability = bivariate_standard_normal_cdf_via_owens(a, b, rho);
println!("a = {}, b = {}, rho = {}", a, b, rho);
println!("P(xi_1 < a, xi_2 < b) = {}", probability);
}
二分查找
partition_point方法是实现 lower_bound和 upper_bound功能最直接和推荐的方式。它接收一个谓词(返回 bool的闭包),并返回切片中第一个使该谓词返回 false的元素的索引
类似C++的lower_bound
查找第一个大于等于目标值的元素位置。
谓词条件是判断元素是否小于目标值。返回的位置就是第一个“不小于”(即大于等于)目标值的位置
1
2
3
4
5
6
let nums = vec![1, 2, 3, 3, 4];
let target = 3;
// 查找第一个 >= target 的元素的位置 (lower_bound)
let lower = nums.partition_point(|&x| x < target);
println!("Lower bound of {} is at index: {}", target, lower); // 输出 2
类似C++的upper_bound
1
2
3
// 查找第一个 > target 的元素的位置 (upper_bound)
let upper = nums.partition_point(|&x| x <= target);
println!("Upper bound of {} is at index: {}", target, upper); // 输出 4
插入操作
1
2
3
4
5
6
let j = g.partition_point(|&x| x < h);
if j < g.len() {
g[j] = h;
} else {
g.push(h);
}
sort_by按条件排序
sort_by(|a, b| ...)中的闭包,其核心任务是回答一个问题:相对于 a,b的顺序是什么?或者更直白地说:在最终的排序结果中,b应该出现在 a的前面还是后面?
- 当闭包返回
Ordering::Less时,表示第一个参数(a)应该排在第二个参数(b)之前。 - 在
a.cmp(b)中,如果a小于b,则返回Less,所以a会排在b前面,形成递增。 - 在
b.cmp(a)中,如果b小于a,则返回Less,所以a会排在b前面,这相当于把大的数往前放,形成递减。
所以,整个代码 tasks.sort_by(|a, b| b.cmp(a))的工作流程是:
- 排序算法每次取出集合中的两个元素,分别作为闭包的参数
a和b。 - 闭包执行
b.cmp(a),也就是判断b相对于a的顺序。 - 如果
b比a大(即b.cmp(a)返回Ordering::Greater),排序算法就会认为a应该排在b的后面,从而实现了从大到小的降序排列。
1
2
3
4
let mut v = [5, 4, 1, 3, 2];
// 降序排序
v.sort_by(|a, b| b.cmp(a));
assert!(v == [5, 4, 3, 2, 1]); // 结果是从大到小
一种错误使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub fn erase_overlap_intervals(intervals: Vec<Vec<i32>>) -> i32 {
let n=intervals.len();
let mut intervals=intervals;
intervals.sort_by(|&a,&b|a[1].cmp(&b[1])); //这里应该改为|a,b|
let mut rec=1;
let mut preed=intervals[0][1];
for i in 1..n{
if intervals[i][0]>=preed{
rec+=1;
preed=intervals[i][1];
}
}
(n-rec) as i32
}
说明:
- 闭包参数的模式匹配问题:
- 闭包获得的是引用:当你的闭包被
sort_by这样的高阶函数调用时,该高阶函数传递给闭包的是集合中元素的引用(在你的代码中就是&Vec<i32>)。所以,无论你如何写闭包的参数列表,闭包实际接收到的是一个引用。使用|a, b|时,a和b直接就是这两个引用&Vec<i32>。使用|&a, &b|是一种模式匹配,你是在告诉 Rust:”我期望接收到的参数是引用,并且请自动解构这个引用,将其内部的值绑定到变量a和b上”。 - 问题的根源:移动非 Copy 类型:解构一个引用(
&Vec<i32>)并试图将其内部的值(Vec<i32>)绑定到新变量(a,b)上,意味着你需要将Vec<i32>从引用背后移动出来。但是,Rust 不允许你通过一个共享引用(&T)来移动其指向的数据,因为你并不拥有该数据的所有权。这就是编译器报错 “cannot move out of a shared reference” 的原因。只有当类型实现了Copytrait(例如简单的整数、浮点数),Rust 才会在解构时自动进行复制而不是移动,这样操作才是安全的。而Vec<i32>没有实现Copytrait 。
-
对比简单类型:
如果类型实现了
Copy(如i32),模式匹配|&a, &b|是允许的,因为解构时会复制值而非移动。但Vec<i32>不支持复制,只能移动,从而触发所有权规则检查。 -
为什么修改为 a,b 有效 sort_by方法会向闭包传递元素的引用(即&Vec<i32>)。- 直接使用
|a, b|后,a和b保持为引用类型,通过a[1]和b[1]访问元素时,Rust 会自动解引用(Deref coercion),而不会触发所有权移动。 - 由于不涉及移动,编译器不再报错
-
cmp函数里为什么又加了引用.cmp(&b[1])
cmp方法(属于i32类型)的定义是fn cmp(&self, other: &Self) -> Ordering。它需要的是对两个要比较的值的引用。因此,我们需要用&来获取b[1]这个i32值的引用。
容易混淆的地方:
对于一个引用类型b(比如b是&Vec <i32>),b和 b[1]确实是两种不同的东西。
简单来说:b是一个引用(指向整个 Vec),而 b[1]是通过索引操作从 b所指向的向量中取出的一个具体的值(i32)。
下面这个表格清晰地展示了两者的核心区别:
| 特性 | b(闭包参数) |
b[1](索引操作结果) |
|---|---|---|
| 实际类型 | &Vec<i32>(引用) |
i32(值) |
| 本质 | 一个“指针”,指向向量数据的内存地址 | 向量中存储的具体的整数值 |
| 所有权 | 不拥有数据的所有权(借用) | 对于 i32这种实现了 Copy的类型,你会得到该值的一个副本 |
如何用于 cmp |
通过 b[1]访问其元素 |
需要取其引用 &b[1]以匹配 cmp签名 |
| 场景 | 向量类型 (intervals) |
闭包参数 (a, b) 类型 |
索引操作 (a[1]) 结果类型 |
推荐的比较写法 |
|---|---|---|---|---|
元素为 i32 |
Vec<Vec<i32>> |
&Vec<i32> |
i32(值) |
a[1].cmp(&b[1]) |
元素为 Vec<i32> |
Vec<Vec<Vec<i32>>> |
&Vec<Vec<i32>> |
&Vec<i32>(引用) |
a[1].cmp(b[1]) |
结构体排序
1
2
//envelopes: mut Vec<Vec<i32>>
envelopes.sort_unstable_by_key(|e| (e[0], -e[1]));
最小堆的实现
rust的BinaryHeap默认实现最大堆,如果要建立最小堆,可以使用 Reverse 包装器,std::cmp::Reverse 是一个简单的包装器结构体,它会反转其内部值的排序顺序。
原理
当你将一个元素 T 包装在 Reverse <T> 中时,Reverse <T> 的 Ord 实现会调用 T 的 Ord 实现,但会返回相反的结果。
a.cmp(b) 返回 Ordering::Less,那么 Reverse(a).cmp(&Reverse(b)) 就会返回 Ordering::Greater。
这样一来,BinaryHeap 在比较 Reverse 元素时,就会把最小的原始元素当成 “最大” 的 Reverse 元素,从而将其放在堆顶。
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::collections::BinaryHeap;
use std::cmp::Reverse;
fn main() {
// 创建一个存储 Reverse<i32> 的 BinaryHeap
let mut min_heap = BinaryHeap::new();
// 插入元素时,用 Reverse 包装起来
min_heap.push(Reverse(3));
min_heap.push(Reverse(1));
min_heap.push(Reverse(2));
println!("最小堆弹出顺序:");
while let Some(Reverse(num)) = min_heap.pop() {
println!("{}", num); // 输出顺序:1, 2, 3
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//用最小堆实现霍夫曼树
use std::collections::BinaryHeap;
use std::cmp::Reverse;
impl Solution {
pub fn min_build_time(blocks: Vec<i32>, split: i32) -> i32 {
let n=blocks.len();
let mut pqu=BinaryHeap::new();
for i in 0..n{
pqu.push(Reverse(blocks[i]));
}
while pqu.len()>1{
let a=pqu.pop().unwrap().0;
let b=pqu.pop().unwrap().0;
pqu.push(Reverse(split+std::cmp::max(a,b)));
}
pqu.pop().unwrap().0
}
}
闭包
- Fn闭包 通过==引用==捕获变量,不可变借用。
- FnMut闭包 通过==可变引用==捕获变量,可变借用。
- FnOnce闭包 通过==值==捕获变量,所有权转移。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main(){
let x=5;
let y=10;
//Fn闭包:通过引用捕获变量
let add = |a| a+x;
//FnMut闭包:通过可变引用捕获变量
let mut multiply = |a|{
a/y
};
//FnOnce闭包:通过值捕获变量
let divide = move |a|{
a/y
};
}
多线程
spawn创建线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::{thread,time::Duration,Builder};
fn main(){
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
//Thread也支持通过std::thread::Builder结构体进行创建,Builder提供了一些线程的配置项
let handle=Builder::new()
.name("my_thread".to_string()) //线程名
.stack_size(1024*4) //设置线程的堆栈大小是1024*4
.spawn({
"hello world" //相当于子线程的执行结果就是字符串"hello world"
});
let res=handle.join().unwrap(); //阻止当前线程(主线程)执行,等待子线程执行完毕,得到子线程的执行结果,即"hello world"
}
线程中使用了其他线程的变量是不合法的,必须使用move表明线程拥有data的所有权,我们可以使用move关键字把data的所有权转移到子线程内。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::thread;
fn main(){
let data=String::from("hello world");
let thread=std::thread::spawn(move ||{ //使用move把data的所有权转到线程内
println!("{}",data);
});
thread.join();
let v=vec![1,3,5,7,9];
let mut childs=vec![];
for n in v{
let c=thread::spawn(move ||{
println!("{}",n*n);
});
childs.push(c);
}
for c in childs{
c.join().unwrap(); //等待所有子进程结束,或者使用expect方法来获取返回值
}
}
move闭包通常和thread::spawn函数一起使用,它允许用户使用其他线程的数据,这样在创建新线程时,可以把其他线程中的变量的所有权从一个线程转移到另一个线程,然后就可以使用改变量了。
得到当前系统的默认并行度
1
2
3
4
5
6
7
use std::{io,thread};
fn main() ->io::Result<()>{
let count=thread::available_parallelism()?.get();
assert!(count>=1_usize);
println!("{},{}",count,1_usize);
Ok(())
}
线程间通信
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::sync::{Arc,Mutex};
use std::thread;
fn main() {
let counter=Arc::new(Mutex::new(0));
let mut handles=vec![];
for _ in 0..10{
let counter=Arc::clone(&counter);
let handle=thread::spawn(move||{
let mut num=counter.lock().unwrap();
*num+=1;
});
handles.push(handle);
}
for handle in handles{
handle.join().unwrap();
}
println!("result: {}",*counter.lock().unwrap());
}
标准输入输出和命令行参数
std::io模块
该模块最核心的部分是Read和Write两个trait。
trait Read用于读;Write用于向输出流中写入数据,包含字节数据和UTF-8数据两种格式。
从标准输入流中读取数据
一般不直接使用trait Read,而是通过实现各个子trait提供给用户使用。
rust语言的Stdin实质上是”BufReader <StdinRaw>“的线程安全版。
函数std::io::stdin()返回一个std::Stdin的实例,这是一个结构体,代表标准输入流。注意:函数stdin()中的s是小写的,而结构体Stdin中的S 是大写的。
std::io是标准库中关于输入输出的包,标准库提供的std::io::stdin()会返回当前进程的标准输入流stdin的句柄。而read_line()则是标准输入流stdin的句柄上的一个方法,用于从标准输入流读取一行数据。read_line()方法的返回值是一个Result枚举,而unwrap()则是用于简化可恢复错误的处理,它会返回Result中存储的实际值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::io;
fn myf(){
//一行有多个数
let mut buf = String::new();
io::stdin().read_line(&mut buf).unwrap();
let mut nums=buf.split_whitespace();
let num1:f64=nums.next().unwrap().parse().unwrap();
let num2:f64=nums.next().unwrap().parse().unwrap();
let num3:f64=nums.next().unwrap().parse().unwrap();
//读入数组
let mut buf=String::new();
io::stdin().read_line(&mut buf).unwrap();
let ns:Vec<i32>=buff.split_whitespace().map(|x| x.parse().unwrap()).collect();
for v in ns{
print!("{} ",v);
}
}
fn main(){
let mut buf=String::new();
io::stdin().read_line(&mut buf).unwrap();
let num1:i32=buf.trim().parse().unwrap();
println!("{}",num1);
myf();
}
如果有多个标准输入流实例,则通过互斥锁进行同步。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::io::{self,BufRead}
fn main()->io::Result<()>{
let mut buffer=String::new();
let mystdin=io::stdin();
let mut handle=stdin.lock(); //显式互斥
handle.read_line(&mut buffer);
Ok(())
}
use std::io;
fn main()->io::Result<()>{
let mut buffer=String::new();
io::stdion().read_line(&mut buffer); //隐式同步读取内容
Ok(())
}
目前Rust标准库还没有提供直接从命令行读取数字或格式化数据的方法。
标准输出流
std::io::stdout() 会返回 std::io::Stdout结构体,表示标准输出流。结构体Sdtout实现了Write trait。函数stdout是模块std::io的一个成员函数,为当前进程的标准输出创建一个新的实例,返回的是句柄Stdout,这个句柄就是当前进程的标准输出流的句柄。stdout函数返回的每个句柄都是对共享缓冲区的引用,因此对该缓冲区的访问通过互斥锁进行同步。
隐式同步:
1
2
3
4
5
6
use std::io::{self,Write};
fn main()->io::Result<()>{
io::stdout().write_all(b"hello world")?;
Ok(())
}
显式同步:
1
2
3
4
5
6
7
8
use std::io::{self,Write};
fn main()->io::Result<()>{
let stdout=io::stdout();
let mut handle=stdout.lock();
handle.write_all(b"hello world")?;
Ok(())
}
“标准输出”通常就是指终端屏幕,而“标准输入”一般就是指键盘。
write()是标准输出流stdout的句柄上的一个方法,用于向标准输出流写入==字节流内容==。
1
2
3
4
use std::io::Write;
fn main(){
std::io::stdout().write("http".as_bytes()).unwrap();
}
命令行参数
Rust语言在标准库中内置了std::env::args()函数返回所有的命令行参数,其第一项是程序名。
1
2
3
4
5
6
fn main(){
let cmd_line=std::env::args();
for arg in cmd_line{
println!("{} ",arg);
}
}
«««< Updated upstream
模块化
文件结构:
1
2
3
4
5
6
7
8
9
10
AC/
├── Calc/ # 库 crate(被调用方)
│ ├── src/
│ │ └── lib.rs # 含 pub fn European_Call
│ └── Cargo.toml
└── Chapter5/
└── C5/ # 二进制 crate(调用方)
├── src/
│ └── main.rs # 要调用 European_Call
└── Cargo.toml
在调用方(C5)的 Cargo.toml 中添加依赖
因为 Calc 是本地 crate,需用 path 指定相对路径
1
2
3
4
5
6
7
8
[package]
name = "C5"
version = "0.1.0"
edition = "2021"
[dependencies]
# 关键:添加对 Calc crate 的依赖
Calc = { path = "../../Calc" } # 路径从 C5/Cargo.toml 指向 Calc/Cargo.toml
- 路径写法:
../../Calc表示 “向上两层到 AC 目录,再进入 Calc 目录”
在 C5 的 main.rs 中调用函数
通过 use 导入 Calc 中的公开函数,或直接通过 crate名::函数名 调用:
1
2
3
4
5
6
use Calc::European_Call;
fn main(){
let call=European_Call(S,K,r,sigma,q,T);
println!("{call:?}");
}
双端队列VecDeque的使用
Rust标准库中的 VecDeque是一个基于可增长环形缓冲区实现的双端队列,它允许在队列的头部和尾部进行高效的插入和删除操作。以下是其基本用法的详细介绍。
- 🚀 创建 VecDeque
你可以通过以下几种方式创建一个 VecDeque:
| 方法 | 说明 | 示例代码 |
|---|---|---|
VecDeque::new() |
创建一个空的 VecDeque。 |
let mut deque: VecDeque<i32> = VecDeque::new(); |
VecDeque::with_capacity(n) |
创建一个初始容量为 n的 VecDeque。 |
let mut deque = VecDeque::with_capacity(10); |
VecDeque::from([x, y, z]) |
从数组或向量直接转换创建。 | let deq = VecDeque::from([1, 2, 3]); |
- ➕ 添加元素
VecDeque的核心特性是支持在两端高效添加元素。
- 在尾部添加:使用
push_back方法。 - 在头部添加:使用
push_front方法。这与普通队列(仅支持尾部添加)不同,是其“双端”能力的体现。
1
2
3
4
5
6
use std::collections::VecDeque;
let mut deque = VecDeque::new();
deque.push_back('a'); // 队列: [a]
deque.push_front('b'); // 队列: [b, a]
deque.push_back('c'); // 队列: [b, a, c]
- ➖ 删除元素
相应地,你也可以从两端移除元素。
- 从尾部移除:使用
pop_back方法,返回一个Option<T>,如果队列为空则返回None。 - 从头部移除:使用
pop_front方法,同样返回Option<T>。
1
2
3
4
5
let mut deque = VecDeque::from(['b', 'a', 'c']);
assert_eq!(deque.pop_front(), Some('b')); // 移除并返回头部元素 'b'
assert_eq!(deque.pop_back(), Some('c')); // 移除并返回尾部元素 'c'
assert_eq!(deque.pop_back(), Some('a')); // 移除并返回尾部元素 'a'
assert_eq!(deque.pop_back(), None); // 队列已空,返回 None
- 👀 访问元素
你可以安全地查看或获取队列两端的元素,而不会移除它们。
- 查看头部元素:使用
front()方法,返回Option<&T>。 - 查看尾部元素:使用
back()方法,返回Option<&T>。 - 通过索引访问:使用
get(index)方法获取指定索引处元素的不可变引用,或使用get_mut(index)获取可变引用。使用索引语法deque[index]在越界时会直接触发 panic。
1
2
3
4
5
6
7
8
9
let mut deque = VecDeque::from([10, 20, 30]);
// 查看元素
assert_eq!(deque.front(), Some(&10));
assert_eq!(deque.back(), Some(&30));
// 通过索引获取和修改
if let Some(elem) = deque.get_mut(1) {
*elem = 25; // 将索引1处的元素从20修改为25
}
assert_eq!(deque[1], 25);
- 🔁 其他实用操作
VecDeque还提供了一些其他常用方法:
len(): 返回队列中的元素数量。is_empty(): 检查队列是否为空。swap(i, j): 交换索引i和j处的两个元素。clear(): 清空队列中的所有元素。iter(): 返回一个用于遍历队列元素的迭代器。
- 💡 核心要点与使用场景
- 高性能:在队列两端进行插入和删除操作的平均时间复杂度为 O(1),非常高效。但当容量不足需要扩容时,会发生 O(n) 的时间复杂度操作。
- 预分配容量:如果你能预估队列的大致大小,使用
VecDeque::with_capacity或reserve方法预先分配空间可以避免在运行时多次扩容,从而提升性能。 - 适用场景:
VecDeque非常适合需要同时作为队列(FIFO)和栈(LIFO)使用的场景,或者需要频繁在序列两端进行操作的算法。通常,它比链表(LinkedList)有更好的内存局部性和缓存友好性,是更推荐使用的双端数据结构。
希望这些介绍能帮助你快速上手 Rust 标准库中的 VecDeque。如果你对特定方法的细节或更高级的用法(如范围操作 range_mut或队列合并 append等)有进一步兴趣,我可以为你提供更详细的说明。
绘图
1
cargo add plotters
以画散点连接图为例
整体示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
use plotters::prelude::*;
fn main(){
let r:f64=0.05;
let rf=0.1;
let X:f64=2.0;
let t_values: Vec<f64> = (1..=20).map(|x| x as f64 * 0.1).collect(); // T = 0.1, 0.2, ..., 2.0
let f_values:Vec<f64>=t_values.iter().map(|&t| X*((r-rf)*t).exp()).collect();
// 使用 fold 手动计算最大值和最小值,以避免 Ord trait 的限制
let min_forward = f_values
.iter()
.fold(f64::INFINITY, |a, &b| a.min(b)); // 初始值设为无穷大,然后不断取较小值
let max_forward = f_values
.iter()
.fold(f64::NEG_INFINITY, |a, &b| a.max(b)); // 初始值设为负无穷大,然后不断取较大值
println!("\nMaturity (T)\tForward Rate (F)");
for (t,f) in t_values.iter().zip(f_values.iter()){
println!("{:.1}\t\t{:.6}",t,f);
}
let root=BitMapBackend::new("forward_plot.png", (800, 600)).into_drawing_area();
root.fill(&WHITE).unwrap();
let mut chart = ChartBuilder::on(&root)
.caption("Forward Exchange Rate vs. Maturity",("sans-serif",30))
.margin(10)
.x_label_area_size(40)
.y_label_area_size(40)
.build_cartesian_2d(0.0..2.1,min_forward*0.95..max_forward*1.05)
.unwrap();
chart.configure_mesh().draw().unwrap();
chart.draw_series(
t_values
.iter()
.zip(f_values.iter())
.map(|(&x, &y)| Circle::new((x, y), 3, BLUE.filled()))
).unwrap();
chart.draw_series(LineSeries::new(
t_values.iter().zip(f_values.iter()).map(|(&t,&f)| (t,f)),
&RED,
)).unwrap();
println!("\nPlot saved as 'forward_plot.png'");
}
准备数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let r:f64=0.05;
let rf=0.1;
let X:f64=2.0;
let t_values: Vec<f64> = (1..=20).map(|x| x as f64 * 0.1).collect(); // T = 0.1, 0.2, ..., 2.0
let f_values:Vec<f64>=t_values.iter().map(|&t| X*((r-rf)*t).exp()).collect();
// 使用 fold 手动计算最大值和最小值,以避免 Ord trait 的限制
let min_forward = f_values
.iter()
.fold(f64::INFINITY, |a, &b| a.min(b)); // 初始值设为无穷大,然后不断取较小值
let max_forward = f_values
.iter()
.fold(f64::NEG_INFINITY, |a, &b| a.max(b)); // 初始值设为负无穷大,然后不断取较大值
println!("\nMaturity (T)\tForward Rate (F)");
for (t,f) in t_values.iter().zip(f_values.iter()){
println!("{:.1}\t\t{:.6}",t,f);
}
绘制
1
let root=BitMapBackend::new("forward_plot.png", (800, 600)).into_drawing_area();
BitMapBackend 是 plotters 库中的一个绘图后端(Backend) ,专门用于生成 位图图像(如 PNG、JPG) 。
- 后端的作用:定义图表的 输出格式 和 存储方式 (这里是 “保存为 PNG 文件”)。
BitMapBackend继承自plotters的通用后端 trait,提供了将绘图指令(如 “画点”“画线”)转换为像素数据的能力。
**静态方法:BitMapBackend::new(...)**
1
`::new` 是 `BitMapBackend` 的 **静态构造方法** ,用于创建一个新的后端实例。它接收两个参数:
-
参数 1:
"forward_plot.png"指定绘图完成后,图像文件的保存路径 和 文件名 。
这里是相对路径(与程序运行目录同级),运行后会在当前目录下生成一个名为
forward_plot.png的文件。若指定路径(如
"./images/forward_plot.png"),需确保目录已存在(否则会报错)。 -
参数 2:
(800, 600)指定图像的分辨率 :宽 800 像素,高 600 像素。
尺寸越大,图像越清晰,但文件体积也会越大。常见尺寸如
(640, 480)(标准)、(1024, 768)(高清)。
**方法调用:.into_drawing_area()**
这是关键的一步,作用是 将 “后端实例” 转换为 “绘图区域(DrawingArea)” 。
我们需要先理解 plotters 的核心设计: “后端(Backend)” 负责输出,“绘图区域(DrawingArea)” 负责绘制 。
为什么需要转换?
BitMapBackend只关心 “如何将像素写入文件”,不提供绘图逻辑(如填充颜色、画图形)。DrawingArea是一个 抽象的绘图接口 ,它封装了 “在指定区域内绘图” 的核心能力(如fill填充颜色、draw_series绘制数据系列),且与具体后端无关(换SVGBackend也能复用同一套绘图逻辑)。
into_drawing_area() 的作用:
- 从
BitMapBackend中提取出一个 “根绘图区域” (整个图像的矩形区域)。 - 这个区域的尺寸就是
BitMapBackend::new指定的(800, 600),后续所有绘图操作都在这个区域内进行。 - 返回的
DrawingArea对象会被赋值给变量root,作为后续绘图的 “基础画布”。
整体流程总结
这行代码的执行逻辑是:
- 告诉
plotters:“我要生成一个名为forward_plot.png、尺寸 800×600 的 PNG 图像”(通过BitMapBackend::new)。 - 从这个 PNG 后端中,获取一个 “可以直接绘图的 800×600 像素画布”(通过
into_drawing_area())。 - 将这个画布命名为
root,后续所有绘图操作都基于这个画布展开。
1
root.fill(&WHITE).unwrap();
.fill(&WHITE):填充背景色
1
2
3
4
5
6
7
let mut chart = ChartBuilder::on(&root)
.caption("Forward Exchange Rate vs. Maturity",("sans-serif",30))
.margin(10)
.x_label_area_size(40)
.y_label_area_size(40)
.build_cartesian_2d(0.0..2.1,min_forward*0.95..max_forward*1.05)
.unwrap();
关键参数说明
ChartBuilder::on(&root)
- 基于已创建的
root绘图区域(如BitMapBackend)初始化图表构建器。
.caption(...)
- 标题内容 :
"Forward Exchange Rate vs. Maturity" - 字体样式 :
("sans-serif", 30)表示使用无衬线字体,字号 30。
.margin(10)
- 设置图表四周的边距为 10 像素,防止标签被裁剪。
.x_label_area_size(40)
- X轴标签区域宽度为 40 像素,确保期限标签(如
0.1,0.2年)有足够显示空间。
.y_label_area_size(40)
- Y轴标签区域高度为 40 像素,用于显示远期汇率数值。
.build_cartesian_2d(...)
- X轴范围 :
0.0..2.1表示期限从 0 到 2.1 年(覆盖t_values的0.1到2.0步长)。 -
Y轴范围 :动态计算:
*
min_forward是f_values的最小值,max_forward是最大值。0.95和1.05为扩展因子,使图表上下留出 5% 空白,避免数据点紧贴边缘。
1
2
3
4
5
6
7
8
9
chart.configure_mesh().draw().unwrap();
chart.configure_mesh()
.x_labels(10) // X轴显示10个刻度标签
.y_labels(5) // Y轴显示5个刻度标签
.light_line_style(&TRANSPARENT) // 副网格线设为透明
.bold_line_style(&RED.mix(0.5)) // 主网格线设为半透明红色
.disable_x_mesh() // 禁用X轴网格线
.draw()?; // 应用配置(需错误处理)
这行代码的作用是 配置并绘制图表的网格系统
1
2
3
4
5
6
chart.draw_series(
t_values
.iter()
.zip(f_values.iter()) // 1. 压缩配对两个数组的迭代器
.map(|(&x, &y)| Circle::new((x, y), 3, BLUE.filled())) // 2. 转换为 Circle 元素
).unwrap(); // 3. 错误处理
闭包参数 :|&x, &y|解构元组中的 t和 f值(通过 &解引用)。
Circle 构造 :
- 圆心坐标 :
(x, y),对应数据点的位置。 - 半径 :
3像素,控制点的大小。 - 样式 :
BLUE.filled()表示蓝色实心填充(对比BLUE.stroke()仅绘制边框)
1
2
3
4
chart.draw_series(LineSeries::new(
t_values.iter().zip(f_values.iter()).map(|(&t,&f)| (t,f)),
&RED,
)).unwrap();
这行代码的完整执行逻辑是:
- 通过
t_values.iter().zip(f_values.iter()).map(...)生成一系列(x, y)坐标点。 - 使用
LineSeries::new(...)和这些坐标点以及红色样式,创建一个折线图数据系列。 - 调用
chart.draw_series()方法,将这个红色折线系列绘制到chart图表对象上。 - 使用
.unwrap()处理绘制过程中可能出现的错误。
rayon的基本用法
rayon 是 Rust 中最流行的并行计算库,它的核心哲学是 “数据并行化”(Data Parallelism)。它的设计目标是让你能以极小的改动,将现有的迭代器代码转变为多线程并行执行。
以下是 rayon 的基本用法和核心概念:
1. 核心方法:into_par_iter()
这是 rayon 最常用的接口。它能将一个普通的迭代器转换为 “并行迭代器”(Parallel Iterator)。
串行 vs 并行
Rust
1
2
3
4
5
6
7
8
9
use rayon::prelude::*; // 必须引入这个 Trait 才能使用并行方法
let mut nums = vec![1, 2, 3, 4, 5, 6, 7, 8];
// 串行版本
nums.iter().map(|x| x * 2).collect::<Vec<_>>();
// 并行版本:只需改一个词
nums.par_iter().map(|x| x * 2).collect::<Vec<_>>();
2. 三种主要的并行迭代器
类似于标准库的迭代器,rayon 针对所有权提供了三种形式:
| 方法 | 转换后的类型 | 说明 |
|---|---|---|
par_iter() |
&T |
产生不可变引用的并行迭代。 |
par_iter_mut() |
&mut T |
产生可变引用的并行迭代(常用于就地修改数组)。 |
into_par_iter() |
T |
消耗集合,产生拥有所有权的元素。 |
3. 常用的并行操作
大部分标准迭代器的方法,rayon 都有对应的并行实现:
filter: 并行过滤元素。sum / product: 并行求和或求积。reduce: 将元素并行合并为一个值(要求合并操作满足结合律)。find_any: 只要任何一个线程找到了符合条件的元素,就立即返回(比串行find快得多)。
Rust
1
2
3
4
5
// 示例:大规模并行求平方和
let sum: u64 = (0..1000_000u64)
.into_par_iter()
.map(|x| x * x)
.sum();
4. 任务切分:join
如果你不想操作集合,而是有两个独立的任务想同时跑,可以使用 join。它会尝试在后台线程池中并行执行这两个闭包。
Rust
1
2
3
4
let (a, b) = rayon::join(
|| do_heavy_task_1(),
|| do_heavy_task_2()
);
5. 并行化的底层原理:工作窃取(Work Stealing)
rayon 不会为每个迭代项开一个线程(那样开销太大),它维护了一个固定大小的线程池(通常等于你的 CPU 核心数)。
- 它将任务切分成许多小块放入队列。
- 当一个线程干完活后,它会从其他忙碌线程的队列末尾“窃取”任务。
- 这种机制保证了 CPU 负载的高度均衡,避免了“一核有难,多核围观”的情况。
6. 使用建议与注意事项
-
计算密集型 vs IO 密集型:
rayon 专门为 CPU 计算密集型(如你的蒙特卡洛模拟、矩阵运算)优化。如果是网络请求或磁盘读写等 IO 任务,建议使用 tokio 等异步运行时。
-
避免小任务过度并行:
如果迭代器里的逻辑极其简单(比如只是 x + 1),且数据量很小,并行化带来的线程调度开销可能会比串行还慢。
-
线程安全约束:
在 par_iter 闭包中捕获的变量必须实现 Sync,而迭代出的元素如果要跨线程传递,必须实现 Send。