rust常用库函数及方法小摘

Posted by Hao on November 10, 2025

[toc]

统计相关

随机数生成

rand 0.9.x 要设置种子且生成随机数

rand 0.9.x 版本中,API 变得更加语义化。要实现“设置种子”并“生成随机数”,最标准且完整的代码如下。

  1. 配置文件 (Cargo.toml)

确保你使用的是 0.9 版本。如果你需要生成布朗运动常用的正态分布,建议加上 rand_distr

1
2
3
[dependencies]
rand = "0.9"
rand_distr = "0.5" # 用于正态分布等复杂分布
  1. 完整实现代码

这是在 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);
}
  1. rand 0.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 (保持不变)
  1. 为什么在结构体中使用 StdRng

在你的布朗运动结构体中使用 StdRng 是正确的选择,原因有二:

  1. 确定性:给定相同的种子,StdRng 在所有平台上产生的随机序列是一致的,这对金融建模和科学计算的“可复现性”至关重要。
  2. 性能与质量:它比 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)$。

\[\text{biv\_norm}(x, y, \rho) = P(\xi_1 > x, \xi_2 > y)\]

由于 $\xi_1$ 和 $\xi_2$ 是关于原点对称的,并且 $P(\xi_1 < a, \xi_2 < b)$ 是累积分布函数 (CDF),您可以利用以下关系将您所需的 $P(\xi_1 < a, \xi_2 < b)$ 转换为由 biv_norm 计算的概率:

  1. 一元标准正态 CDF: $\Phi(x) = P(\xi < x)$。

    • $P(\xi > x) = 1 - \Phi(x)$
  2. 包含关系: $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_boundupper_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| ...)中的闭包,其核心任务是回答一个问题:相对于 ab的顺序是什么?或者更直白地说:在最终的排序结果中,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))的工作流程是:

  1. 排序算法每次取出集合中的两个元素,分别作为闭包的参数 ab
  2. 闭包执行 b.cmp(a),也就是判断 b相对于 a的顺序
  3. 如果 ba大(即 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
    }

说明:

  1. 闭包参数的模式匹配问题
  • 闭包获得的是引用:当你的闭包被 sort_by这样的高阶函数调用时,该高阶函数传递给闭包的是集合中元素的引用(在你的代码中就是 &Vec<i32>)。所以,无论你如何写闭包的参数列表,闭包实际接收到的是一个引用。使用 |a, b|时,ab直接就是这两个引用 &Vec<i32>。使用 |&a, &b|是一种模式匹配,你是在告诉 Rust:”我期望接收到的参数是引用,并且请自动解构这个引用,将其内部的值绑定到变量 ab上”。
  • 问题的根源:移动非 Copy 类型:解构一个引用(&Vec<i32>)并试图将其内部的值(Vec<i32>)绑定到新变量(a, b)上,意味着你需要将 Vec<i32>从引用背后移动出来。但是,Rust 不允许你通过一个共享引用(&T)来移动其指向的数据,因为你并不拥有该数据的所有权。这就是编译器报错 “cannot move out of a shared reference” 的原因。只有当类型实现了 Copytrait(例如简单的整数、浮点数),Rust 才会在解构时自动进行复制而不是移动,这样操作才是安全的。而 Vec<i32>没有实现 Copytrait 。
  1. 对比简单类型

    如果类型实现了 Copy(如 i32),模式匹配 |&a, &b|是允许的,因为解构时会复制值而非移动。但 Vec<i32>不支持复制,只能移动,从而触发所有权规则检查。

  2. 为什么修改为 a,b 有效
    • sort_by方法会向闭包传递元素的引用(即 &Vec<i32>)。
    • 直接使用 |a, b|后,ab保持为引用类型,通过 a[1]b[1]访问元素时,Rust 会自动解引用(Deref coercion),而不会触发所有权移动。
    • 由于不涉及移动,编译器不再报错
  3. cmp函数里为什么又加了引用.cmp(&b[1])

    cmp方法(属于 i32类型)的定义是 fn cmp(&self, other: &Self) -> Ordering。它需要的是对两个要比较的值的引用。因此,我们需要用 &来获取 b[1]这个 i32值的引用。

容易混淆的地方:

对于一个引用类型b(比如b是&Vec <i32>),bb[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是一个基于可增长环形缓冲区实现的双端队列,它允许在队列的头部和尾部进行高效的插入和删除操作。以下是其基本用法的详细介绍。

  1. 🚀 创建 VecDeque

你可以通过以下几种方式创建一个 VecDeque

方法 说明 示例代码
VecDeque::new() 创建一个空的 VecDeque let mut deque: VecDeque<i32> = VecDeque::new();
VecDeque::with_capacity(n) 创建一个初始容量为 nVecDeque let mut deque = VecDeque::with_capacity(10);
VecDeque::from([x, y, z]) 从数组或向量直接转换创建。 let deq = VecDeque::from([1, 2, 3]);
  1. ➕ 添加元素

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]
  1. ➖ 删除元素

相应地,你也可以从两端移除元素。

  • 从尾部移除:使用 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
  1. 👀 访问元素

你可以安全地查看或获取队列两端的元素,而不会移除它们。

  • 查看头部元素:使用 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);
  1. 🔁 其他实用操作

VecDeque还提供了一些其他常用方法:

  • len(): 返回队列中的元素数量。
  • is_empty(): 检查队列是否为空。
  • swap(i, j): 交换索引 ij处的两个元素。
  • clear(): 清空队列中的所有元素。
  • iter(): 返回一个用于遍历队列元素的迭代器。
  1. 💡 核心要点与使用场景
  • 高性能:在队列两端进行插入和删除操作的平均时间复杂度为 O(1),非常高效。但当容量不足需要扩容时,会发生 O(n) 的时间复杂度操作。
  • 预分配容量:如果你能预估队列的大致大小,使用 VecDeque::with_capacityreserve方法预先分配空间可以避免在运行时多次扩容,从而提升性能。
  • 适用场景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();

BitMapBackendplotters 库中的一个绘图后端(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,作为后续绘图的 “基础画布”。

整体流程总结

这行代码的执行逻辑是:

  1. 告诉 plotters:“我要生成一个名为 forward_plot.png、尺寸 800×600 的 PNG 图像”(通过 BitMapBackend::new)。
  2. 从这个 PNG 后端中,获取一个 “可以直接绘图的 800×600 像素画布”(通过 into_drawing_area())。
  3. 将这个画布命名为 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();

关键参数说明

  1. ChartBuilder::on(&root)
  • 基于已创建的 root绘图区域(如 BitMapBackend)初始化图表构建器。
  1. .caption(...)
  • 标题内容"Forward Exchange Rate vs. Maturity"
  • 字体样式("sans-serif", 30)表示使用无衬线字体,字号 30。
  1. .margin(10)
  • 设置图表四周的边距为 10 像素,防止标签被裁剪。
  1. .x_label_area_size(40)
  • X轴标签区域宽度为 40 像素,确保期限标签(如 0.1, 0.2年)有足够显示空间。
  1. .y_label_area_size(40)
  • Y轴标签区域高度为 40 像素,用于显示远期汇率数值。
  1. .build_cartesian_2d(...)
  • X轴范围0.0..2.1表示期限从 0 到 2.1 年(覆盖 t_values0.12.0步长)。
  • Y轴范围 :动态计算:

    *min_forwardf_values的最小值,max_forward是最大值。

    • 0.951.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|解构元组中的 tf值(通过 &解引用)。

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();

这行代码的完整执行逻辑是:

  1. 通过 t_values.iter().zip(f_values.iter()).map(...) 生成一系列 (x, y) 坐标点。
  2. 使用 LineSeries::new(...) 和这些坐标点以及红色样式,创建一个折线图数据系列。
  3. 调用 chart.draw_series() 方法,将这个红色折线系列绘制到 chart 图表对象上。
  4. 使用 .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。