Rust:Vec

Rust:Vec


Vec是最基础的集合类型,它是可变长的数组,存储任意个数有序的相同类型元素。

结构

Vec定义如下:

rust 复制代码
struct RawVecInner<A: Allocator = Global> {
    ptr: Unique<u8>,
    cap: Cap,
    alloc: A,
}

pub(crate) struct RawVec<T, A: Allocator = Global> {
    inner: RawVecInner<A>,
    _marker: PhantomData<T>,
}

pub struct Vec<T, A: Allocator = Global> {
    buf: RawVec<T, A>,
    len: usize,
}

Vec的整体分三层结构:

  • Vec:管理数组的长度
    • len:当前元素个数
  • RawVec:管理数组的元素类型
    • _marker:标记类型,在逻辑上持有T这个类型
  • RawVecInner:管理数组内存空间
    • ptr:指向堆内存的指针
    • cap:堆内存实际容量
    • alloc:堆内存分配器

Vec本身只管理数组的长度,比如涉及到增删元素等。而RawVec负责管理类型,包括使用PhantomData来获取T的类型特性,型变等。而RawVecInner只负责管理内存,比如说堆内存开在哪里,容量是多少。

而且对于RawVecInner,它内部已经没有关于具体存储的类型T的任何信息了,堆区直接以u8类型管理,也就是按字节进行管理。

如果简化逻辑,只有三个核心的字段:lencapptr

通过len()capacity()方法,可以拿到这两个值:

rust 复制代码
let v: Vec<i32> = vec![1, 2, 3];

println!("{}", v.len());
println!("{}", v.capacity());

增删查改

push

push()用于在数组尾部新插入一个元素。

rust 复制代码
fn push(&mut self, value: T)

示例:

rust 复制代码
let mut vec = vec![1, 2];
vec.push(3);
assert_eq!(vec, [1, 2, 3]);

append

append()把另外一个数组的所有元素移动到当前数组。

rust 复制代码
fn append(&mut self, other: &mut Vec<T, A>)

示例:

rust 复制代码
let mut vec = vec![1, 2, 3];
let mut vec2 = vec![4, 5, 6];
vec.append(&mut vec2);
assert_eq!(vec, [1, 2, 3, 4, 5, 6]);
assert_eq!(vec2, []);

insert

insert()在指定下标位置插入一个新元素。

rust 复制代码
fn insert(&mut self, index: usize, element: T)

如果下标超出了len,会导致panic

示例:

rust 复制代码
let mut vec = vec!['a', 'b', 'c'];
vec.insert(1, 'd');
assert_eq!(vec, ['a', 'd', 'b', 'c']);
vec.insert(4, 'e');
assert_eq!(vec, ['a', 'd', 'b', 'c', 'e']);

pop

pop()用于删除并获得数组尾部的元素。

rust 复制代码
fn pop(&mut self) -> Option<T>

当数组不为空,返回Some。如果数组为空,则返回None

示例:

rust 复制代码
let mut vec = vec![1, 2, 3];
assert_eq!(vec.pop(), Some(3));
assert_eq!(vec, [1, 2]);

索引

数组支持通过[]进行下标访问,本质是实现了Index<I>这个Trait

示例:

rust 复制代码
let v = vec![0, 2, 4, 6];
println!("{}", v[1]);

v[1]表示拿到数组第二个元素。如果下标没有越界,那么直接返回对应位置的值,如果越界,那么会触发panic

要注意的是v[1]这个表达式,既可以是位置表达式,也可以是值表达式。

这意味着你可以对它进行赋值,可以对它进行借用:

rust 复制代码
let mut v = vec![1, 2, 3];
let y = &v[1];
let z = &mut v[2];
v[0] = 10;

以上三种使用场景,都是把下标访问这个整体作为一个位置表达式处理。

如果把它当做一个值表达式进行处理,如果数组内存储的是移动类型,就会导致错误:

rust 复制代码
let mut v = vec![1, 2, 3];
let ret = v[1]; // success

let mut v = vec!["hello".to_string(), "world".to_string()];
let ret = v[1]; // error

以上代码中,对于Vec<i32>访问v[1]是合法的,因为此时会发生拷贝,没有影响数组本身。

但是Vec<String>访问v[1]非法了,因为这里把它作为了一个值表达式,那么String作为移动类型就会发生移动,数组的第二个元素就会变成一个失去所有权的非法元素。数组不允许单独移动走某一个元素的所有权,除非你把这个元素从数组删掉,你只能整体移动这个数组。

对于移动类型,要么你把他作为一个位置表达式处理,拿它的借用。要么就显式调用clone()方法,拿到它的拷贝。


get

get()同样用于下标访问,相比于[]更安全。

rust 复制代码
fn get<I>(&self, index: I) -> Option<&<I as SliceIndex<[T]>>::Output>
where
    I: SliceIndex<[T]>,

这个方法签名涉及到SliceIndex<[T]>。它是标准库中用于表示一个索引的Trait,它内部包含一个关联类型Output,表示索引后输出什么类型。

只要实现这个Trait,就可以作为数组的下标使用。标准库中最常见的就是usize以及范围类型,比如可以写arr[1]arr[1..10]等等,就是因为usizeRange都实现了这个Trait

而通过usize访问到的是单个元素,因此Output = T,而通过范围类型访问到的是切片,因此Output = [T]

这里的get同理,你既可以arr.get(1),也可以arr.get(1..10)。前者返回的是Option<&T>,后者则是Option<&[T]>

如果索引合法,那么返回的是Some,反之就是None。而[]在越界时直接导致panic,这就是get的优势。

示例:

rust 复制代码
let v = [10, 40, 30];
assert_eq!(Some(&40), v.get(1));
assert_eq!(Some(&[10, 40][..]), v.get(0..2));
assert_eq!(None, v.get(3));
assert_eq!(None, v.get(0..4));

retain

retain()用于删除不符合条件的元素。

rust 复制代码
fn retain<F>(&mut self, f: F)
where
    F: FnMut(&T) -> bool,

该方法接受一个闭包,对每个元素依次执行,闭包返回true则保留,反之则删除。

示例:

rust 复制代码
let mut vec = vec![1, 2, 3, 4];
vec.retain(|&x| x % 2 == 0);
assert_eq!(vec, [2, 4]);

swap

swap()交换数组内部的两个元素。

rust 复制代码
fn swap(&mut self, a: usize, b: usize)

示例:

rust 复制代码
let mut v = ["a", "b", "c", "d", "e"];
v.swap(2, 4);
assert!(v == ["a", "b", "e", "d", "c"]);

Vec提供这个方法,主要是因为std::mem::swap()无法交换数组的两个元素。

比如以下代码:

rust 复制代码
let mut v = ["a", "b", "c", "d", "e"];
std::mem::swap(&mut v[2], &mut v[4]); // error

这段代码无法运行,是因为std::mem::swap()要求获取两个参数的可变借用。但是这里编译器会认为你对一个数组进行两次可变借用,编译器无法保证这两次借用到了不同的元素,因此编译不通过。


remove

remove()用于删除指定索引的元素,并将其返回。

rust 复制代码
fn remove(&mut self, index: usize) -> T

示例:

rust 复制代码
let mut v = vec!['a', 'b', 'c'];
assert_eq!(v.remove(1), 'b');
assert_eq!(v, ['a', 'c']);

内存分配

new

new()直接创建一个空数组。

rust 复制代码
const fn new() -> Vec<T>

由于new中不需要传入任何参数,所以编译器不知道这个T具体是什么类型,要么通过turbofish显式指定,要么通过返回值类型让编译器自己推。

示例:

rust 复制代码
let mut vec: Vec<i32> = Vec::new();
let mut vec = Vec::<i32>::new();

vec!

vec!是一个宏,它可以直接创建一个已初始化好的数组。

它支持三种写法:

rust 复制代码
let v1 = vec![1, 2, 3];
let v2 = vec![0; 5];
let v3: Vec<i32> = vec![];

第一种是显式指定索引元素,第二种是将所有元素初始化为指定值,第三种则是创建一个空数组。

对于第三种语法,同样需要显式指定返回值类型,让编译器可以推出具体的元素类型。


扩容机制

前文提到,Vec内部的三个核心字段是ptrlencapacity

这三者之间的关系是什么?这就涉及到Vec内部的扩容机制。

示例:

rust 复制代码
let v1 = vec![1, 2, 3];
println!("{}", v1.len());
println!("{}", v1.capacity());

let v2 = vec![0; 5];
println!("{}", v2.len());
println!("{}", v2.capacity());


let v3: Vec<i32> = vec![];
println!("{}", v3.len());
println!("{}", v3.capacity());

这段代码输出:

rust 复制代码
3
3
5
5
0
0

也就是说,对于直接进行初始化的数组,有多少个元素,那么它的初始的lencapacity就是多少。

再尝试运行以下代码:

rust 复制代码
fn main() {
    let mut v: Vec<i32> = Vec::new();

    for _ in 0..100 {
        if v.len() == v.capacity() {
            println!("ptr = {:p}, len = {}, cap = {}", v.as_ptr(), v.len(), v.capacity());
        }
        v.push(1);
    }
}

其中v.as_ptr()拿到的是指向数组堆区头部的原生指针。

我分别在Windows11Ubuntu 24.04两个系统下运行了这个代码,各自输出结果如下:

  • Ubuntu 24.04
rust 复制代码
ptr = 0x4, len = 0, cap = 0
ptr = 0x5d4782f67d00, len = 4, cap = 4
ptr = 0x5d4782f67d00, len = 8, cap = 8
ptr = 0x5d4782f67d00, len = 16, cap = 16
ptr = 0x5d4782f67d00, len = 32, cap = 32
ptr = 0x5d4782f67d00, len = 64, cap = 64
  • Windows11
rust 复制代码
ptr = 0x4, len = 0, cap = 0
ptr = 0x2871e029bd0, len = 4, cap = 4
ptr = 0x2871e024990, len = 8, cap = 8
ptr = 0x2871e029760, len = 16, cap = 16
ptr = 0x2871e01f570, len = 32, cap = 32
ptr = 0x2871e026b60, len = 64, cap = 64

首先可以看到,一开始数组容量为0。第一次插入元素时,以4为初始长度,后续每次都进行二倍扩容。

而在扩容的过程中,对于Windows底层的MSVC倾向于每次扩容都新开辟一块空间,因此每次ptr的指针值都不一样。而Linux底层的glibc倾向于尽可能原地扩容,因此每次扩容地址都不变,只是在原本的内存尾部新增了内存。

在扩容过程中,只要内存地址变化了,那么就需要把原本数组的所有数据拷贝到新的内存中,这会导致O(N)的时间复杂度,非常低效,在实际开发中会尽可能避免扩容,减少拷贝。


with_capacity

with_capacity创建一个空数组,但是指定初始的capacity

rust 复制代码
fn with_capacity(capacity: usize) -> Vec<T>

示例:

rust 复制代码
let mut vec: Vec<i32> = Vec::with_capacity(10);
println!("len = {}, capacity = {}", vec.len(), vec.capacity());

输出结果:

rust 复制代码
len = 0, capacity = 10

在这里,数组刚创建好时,就已经预分配好10个元素的容量,在范围内就不会触发扩容机制。在预先知道数组的存储上限时,这个方法很有用。

但是以上代码中,如果把i32换成(),就会产生奇怪的现象:

rust 复制代码
let mut vec: Vec<()> = Vec::with_capacity(10);
println!("len = {}, capacity = {}", vec.len(), vec.capacity());

输出结果:

rust 复制代码
len = 0, capacity = 18446744073709551615

我明明指定大小为10,为什么最后大小是这么大的一个数?

这是Vec底层的另一个优化,当数组的元素T是一个零大小类型时,capacity固定为usize::MAX

因为零大小类型不占用内存空间,根本不需要在堆区开辟内存,因此ptrcapacity都没有实际意义了。

每次你在push一个新元素的时候,Vec既不会开新元素,更不会扩容,它实际上只会把len += 1。为了避免触发扩容机制,直接就把capacity指定为最大值,对于零大小类型,你可以存无限个到数组中,对数组而言也就相当于用len做一个计数器而已,不论你访问第几个元素,返回的都是同一个地址。


reserve

reserve()为数组预留出指定个数元素的位置。

rust 复制代码
fn reserve(&mut self, additional: usize)

示例:

rust 复制代码
let mut vec = vec![1];
vec.reserve(10);
assert!(vec.capacity() >= 11);

起初vec只有一个元素,然后调用reserve(10),也就是在已经有1个元素的基础上,至少再预留10个元素的位置,因此这个数组的capacity至少是11


resize

resize()调整数组的长度为指定值。

rust 复制代码
fn resize(&mut self, new_len: usize, value: T)

new_len是新的长度,如果new_len > len,多出来的位置就会用value进行填充。如果new_len < len,那么多出来的元素就会被截断。

示例:

rust 复制代码
let mut vec = vec!["hello"];
vec.resize(3, "world");
assert_eq!(vec, ["hello", "world", "world"]);

let mut vec = vec!['a', 'b', 'c', 'd'];
vec.resize(2, '_');
assert_eq!(vec, ['a', 'b']);

shrink_to_fit

shrink_to_fit会尽可能缩小数组的容量,让它刚好存储当前已有的元素。

rust 复制代码
fn shrink_to_fit(&mut self)

示例:

rust 复制代码
let mut vec = Vec::with_capacity(10);
vec.extend([1, 2, 3]);
assert!(vec.capacity() >= 10);
vec.shrink_to_fit();
assert!(vec.capacity() >= 3);

这里,一开始数组的长度是10,插入三个元素后,调用shrink_to_fit(),那么此时数组只保证剩下的capacity至少为3,也就是缩小了数组的容量。

但是缩小后,可能仍然存在多余的空间。它即有可能原地缩小容量,也有可能重新分配。


truncate

truncate将数组缩短,只保留前 len 个元素并丢弃其余元素。

rust 复制代码
fn truncate(&mut self, len: usize)

如果 len 大于或等于当前数组的长度,则该方法不会产生任何效果。

示例:

rust 复制代码
let mut vec = vec![1, 2, 3, 4, 5];
vec.truncate(2);
assert_eq!(vec, [1, 2]);
rust 复制代码
let mut vec = vec![1, 2, 3];
vec.truncate(8);
assert_eq!(vec, [1, 2, 3]);

clear

clear用于清空整个数组。

rust 复制代码
fn clear(&mut self)

clear只会将len变成0,不会影响已经开辟的内存。

示例:

rust 复制代码
let mut v = vec![1, 2, 3];
v.clear();
assert!(v.is_empty());

二元关系

在讲解查找与排序之前,需要先了解二元关系,因为它涉及到数组元素之间的比较。

Rust 中把比较操作抽象为一些 Trait,定义在 std::cmp 模块中。该模块中定义的 Trait 是基于数学集合论中的二元关系偏序、全序和等价的。

  • 对于非空集合中的 abc 来说,满足下面条件为偏序关系
    • 自反性:a <= a
    • 反对称性:如果 a <= bb <= a,则 a = b
    • 传递性:如果 a <= bb <= c,则 a <= c
  • 对于非空集合中的 abc 来说,满足下面条件为全序关系
    • 反对称性:若 a <= bb <= a,则 a = b
    • 传递性:若 a <= bb <= c,则 a <= c
    • 完全性:a < bb < aa == b 必须满足其一,表示任何元素都可以相互比较
  • 对于非空集合中的 abc 来说,满足下面条件为等价关系
    • 自反性:a == a
    • 对称性:a == b,意味着 b == a
    • 传递性:若 a == bb == c,则 a == c

等价关系,则一般用于对元素分组,比如说对于{1, 2, 3, 4, 5}这个集合,使用f(x) = x % 2对它进行分组,分为{1, 3, 5}{2, 4}。其中f(1) == f(3) == f(5),而f(2) == f(4)f(1) == f(1)满足自反性,f(1) == f(3)f(3) == f(1)满足对称性,而f(1) == f(3)f(3) == f(5)f(1) == f(5)符合传递性。因此这就是一个等价关系。

偏序表示集合内部的部分元素可以进行比较,而全序表示所有元素都可以进行比较。

比如说在一个族谱中,假设AB的祖先,就记作A <= B。满足:

  1. 自反性:每个人都是自己的祖先,A <= A
  2. 反对称性:如果 AB 的祖先,B 又是 A 的祖先,那说明 A == B
  3. 传递性:如果 AB 的祖先,BC 的祖先,那么 A 一定是 C 的祖先

但是在这样一个族谱关系中,并不是任意两个人都可以进行比较。比如说CD是亲兄弟,C不是D的祖先,D也不是C的祖先。这就是一种对于局部可以进行比较,但不是任意两个元素都能比较的关系,叫做偏序。

而对于全序,就很好理解了,比如说将全人类按照年龄进行排序,你随便挑两个人AB,一定可以比出结果a < bb < aa == b

假设我要对一个数组的所有元素进行排序,你觉得它应该满足哪一种关系?当然是全序关系。

再比如说,哈希表中基于哈希值对元素进行求余分组,又应该满足哪一种关系?那就是等价关系。

所以说编程中需要这些数学逻辑抽象,Rust将这些二元关系以Trait的形式体现。

Ordering

首先为了表示A > BA < BA == B这三种关系,标准库提供了一个枚举Ordering,定义如下:

rust 复制代码
#[repr(i8)]
pub enum Ordering {
    Less = -1,
    Equal = 0,
    Greater = 1,
}

其中Less表示小于,Equal表示等于,Greater表示大于。


PartialEq

PartialEq 用于定义部分相等的关系,也就是等价关系中的一种实现。

rust 复制代码
pub trait PartialEq<Rhs = Self>: Reflect {
    fn eq(&self, other: &Rhs) -> bool;
    fn ne(&self, other: &Rhs) -> bool { !self.eq(other) }
}

其中:

  • eq 用于定义两个值相等的逻辑
  • ne 定义不相等,通常默认实现就是取反

它要求实现对称性、传递性,但不要求自反性。

几乎所有内置类型都实现了 PartialEq,比如 i32StringVec<T> 等,使用的操作符是 ==!=

rust 复制代码
let a = 3;
let b = 4;
assert!(a != b);

let s1 = "hello".to_string();
let s2 = "hello".to_string();
assert!(s1 == s2);

PartialEq 允许自定义比较逻辑。例如:

rust 复制代码
#[derive(Debug)]
struct Point { x: i32, y: i32 }

impl PartialEq for Point {
    fn eq(&self, other: &Self) -> bool {
        self.x == other.x && self.y == other.y
    }
}

let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 1, y: 2 };
assert!(p1 == p2);

Eq

EqPartialEq 的子 Trait,用于保证严格等价。

rust 复制代码
pub trait Eq: PartialEq<Self> { }

也就是说,只要实现了 PartialEq 并且能满足自反性、对称性、传递性,就可以安全地写 impl Eq for T {}

相比于PartialEqEq对自反性有要求,所有元素必须等于自己。

例如:

rust 复制代码
#[derive(PartialEq, Eq)]
struct Point { x: i32, y: i32 }

let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 1, y: 2 };
assert!(p1 == p2);

可以通过#[derive(PartialEq, Eq)]让结构体自动实现对应Trait,它默认将结构体内部的字段逐个进行比较,只要索引字段都相等,那么就认为两个结构体相等。

f32f64 则无法实现 Eq,因为 NaN != NaN,破坏了自反性。


PartialOrd

PartialOrd 用于定义偏序关系。

rust 复制代码
pub trait PartialOrd<Rhs = Self>: PartialEq<Rhs> {
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;

    fn lt(&self, other: &Rhs) -> bool { matches!(self.partial_cmp(other), Some(Ordering::Less)) }
    fn le(&self, other: &Rhs) -> bool { matches!(self.partial_cmp(other), Some(Ordering::Less | Ordering::Equal)) }
    fn gt(&self, other: &Rhs) -> bool { matches!(self.partial_cmp(other), Some(Ordering::Greater)) }
    fn ge(&self, other: &Rhs) -> bool { matches!(self.partial_cmp(other), Some(Ordering::Greater | Ordering::Equal)) }
}

partial_cmp需要自己实现,其他的都可以依赖默认实现。而这个方法返回了一个Option<Ordering>,因为在偏序关系中,不是任意两个元素都可以进行比较,只要这两个元素不能比较,那么返回None

rust 复制代码
let x = 1.0_f32;
let y = 2.0_f32;
assert_eq!(x.partial_cmp(&y), Some(Ordering::Less));

let nan = f32::NAN;
assert_eq!(x.partial_cmp(&nan), None); // 不可比较

要实现 PartialOrd,必须先实现 PartialEq。这是因为不等式比较离不开相等性的基础。

对于partial_cmp以外的方法,lt表示<le表示<=gt表示>ge表示>=。实现这个Trait,就可以用对应的操作符去对元素做比较。


Ord

如果一个类型的所有元素都可以比较出谁大谁小,那就满足全序关系,可以实现 Ord

rust 复制代码
pub trait Ord: Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;
}

它返回一个确定的 Ordering,不再是 Option

因此,OrdPartialOrd 的区别就在于PartialOrd 可能出现不知道谁大谁小的情况,返回 NoneOrd 总能给出确定的结果

例如内置的整数类型:

rust 复制代码
let a = 1;
let b = 2;
assert_eq!(a.cmp(&b), Ordering::Less);

查找

contains

contains()用于查找是否存在指定值的元素,要求T实现了PartialEq

rust 复制代码
fn contains(&self, x: &T) -> bool
where
    T: PartialEq,

注意的是,它接收的参数类型是&T,你需要传入一个借用。

示例:

rust 复制代码
let v = [10, 40, 30];
assert!(v.contains(&30));
assert!(!v.contains(&50));

starts_with

starts_with()检查数组是否以某个数组为前缀,要求T实现PartialEq

rust 复制代码
fn starts_with(&self, needle: &[T]) -> bool
where
    T: PartialEq,

这个方法接收的是一个数组的切片,只要数组的前缀和这个切片匹配上,就返回true

示例:

rust 复制代码
let v = [10, 40, 30];
assert!(v.starts_with(&[10]));
assert!(v.starts_with(&[10, 40]));
assert!(v.starts_with(&v));
assert!(!v.starts_with(&[50]));
assert!(!v.starts_with(&[10, 50]));

如果传入一个空的切片&[],那么固定返回true


ends_with

ends_with()检查数组是否以某个数组为后缀,要求T实现PartialEq

rust 复制代码
fn ends_with(&self, needle: &[T]) -> bool
where
    T: PartialEq,

示例:

rust 复制代码
let v = [10, 40, 30];
assert!(v.ends_with(&[30]));
assert!(v.ends_with(&[40, 30]));
assert!(v.ends_with(&v));
assert!(!v.ends_with(&[50]));
assert!(!v.ends_with(&[50, 30]));

binary_search()对切片进行二分查找,寻找给定元素。

rust 复制代码
fn binary_search(&self, x: &T) -> Result<usize, usize>
where
    T: Ord,

这个方法要求数组本身就是有序的,否则二分算法就失效了。

如果找到了对于的值,返回Result::Ok,其中包含匹配到的元素的索引,但如果有多个匹配的项,有可能返回任意一个。

如果没找到对于的值,返回Result::Err,其中包含一个索引。如果要插入这个元素,那么插入到这个索引下,可以保持数组依然有序。

示例:

rust 复制代码
let s = [0, 1, 1, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55];

assert_eq!(s.binary_search(&13),  Ok(9));
assert_eq!(s.binary_search(&4),   Err(7));
assert_eq!(s.binary_search(&100), Err(13));

let r = s.binary_search(&1);
assert!(match r { Ok(1..=4) => true, _ => false, });

这个代码中,查找13可以正常找到,返回Ok(9)。而4100不存在,分别返回Err(7)Err(13)

而查找1时,这个元素出现了四次,因此在[1, 4]的区间内都有可能。


排序

sort

sort()用于对数组升序排序,要求T实现Ord

rust 复制代码
fn sort(&mut self)
where
    T: Ord,

它是一个稳定的排序,也就是相同的元素经过排序后,前后位置不会交换。

它内部不是传统的排序算法,而是插入排序、快速排序、归并排序的融合排序算法driftsort。因为快速排序的效率会退化为 O(N2),但是这个sort改进后,最低效率为 O(N * lgN),最高效率为 O(N)。

如果简单来说,这个算法首先会检测整个数组中已经部分有序的子数组,称为一个run,并把这些run标记。对于无序的子数组,也会被标记,随后对这些无序的子数组进行排序。如果这个子数组比较小,就用插入排序避免递归,如果子数组比较大,就用快速排序提速。最后将所有已经有序的子数组,利用优化后的归并排序合并起来,形成整体有序的数组。

其实这个算法内部还存在非常多优化,未来也许会出一期博客专门讨论这个算法,用起来其实不用考虑里面是什么算法,直接调用就好了。

示例:

rust 复制代码
let mut v = [4, -5, 1, -3, 2];

v.sort();
assert_eq!(v, [-5, -3, 1, 2, 4]);

sort_by

sort_by()使用指定的比较函数对数据进行排序。

rust 复制代码
fn sort_by<F>(&mut self, compare: F)
where
    F: FnMut(&T, &T) -> Ordering,

传入一个闭包F,要求这个闭包必须返回Ordering

示例:

rust 复制代码
let mut v = [4, -5, 1, -3, 2];
v.sort_by(|a, b| a.cmp(b));
assert_eq!(v, [-5, -3, 1, 2, 4]);

// 逆序排序
v.sort_by(|a, b| b.cmp(a));
assert_eq!(v, [4, 2, 1, -3, -5]);

sort_unstable

sort_unstable()用于对数组升序排序,要求T实现Ord

rust 复制代码
fn sort_unstable(&mut self)
where
    T: Ord,

相比于sortsort_unstable整体效率更高,但是不稳定,可能导致相同元素的前后顺序变化。

它内部使用的是ipnsort,内部以插入排序、快速排序、堆排序为主导。相比于之前的driftsort,可以发现其实就是把核心的归并排序换成了堆排序。这样不需要额外的空间复杂度,效率会高一些。

它在整个数组比较短时采用插入排序,比较长时以快速排序为主导。当快速排序递归到达一定深度,该用堆排序停止递归。

示例:

rust 复制代码
let mut v = [4, -5, 1, -3, 2];

v.sort_unstable();
assert_eq!(v, [-5, -3, 1, 2, 4]);

迭代器

基础迭代器

关于Vec基础的迭代器创建方法,其实在前一章迭代器已经有非常详尽的描述了,这里简单回顾一下接口。

  • iter:返回不可变借用的迭代器
rust 复制代码
fn iter(&self) -> Iter<'_, T>
  • iter_mut:返回可变借用迭代器
rust 复制代码
fn iter_mut(&mut self) -> IterMut<'_, T>
  • into_iter:返回所有权迭代器(该方法在IntoIterator中)
rust 复制代码
into_iter(self) -> <&'a Vec<T, A> as IntoIterator>::IntoIter

chunks

  • chunks

返回一个迭代器,每次迭代会产生一个长度为 chunk_size 的切片,从原切片的开头开始。

这些切片之间 不重叠。如果 chunk_size 不能整除切片的长度,那么最后一个切片的长度会小于 chunk_size

rust 复制代码
fn chunks(&self, chunk_size: usize) -> Chunks<'_, T>

示例:

rust 复制代码
let slice = ['l', 'o', 'r', 'e', 'm'];
let mut iter = slice.chunks(2);

assert_eq!(iter.next().unwrap(), &['l', 'o']);
assert_eq!(iter.next().unwrap(), &['r', 'e']);
assert_eq!(iter.next().unwrap(), &['m']);
assert!(iter.next().is_none());

传入的参数chunk_size = 2,因此每次迭代拿到长度为2的切片,由于无法整除,最后一次切片只有一个元素。

  • chunks_mut
rust 复制代码
fn chunks_mut(&mut self, chunk_size: usize) -> ChunksMut<'_, T>

相比于chunks,返回的切片是可变切片,可以修改数组内部的元素。

示例:

rust 复制代码
let v = &mut [0, 0, 0, 0, 0];
let mut count = 1;

for chunk in v.chunks_mut(2) {
    for elem in chunk.iter_mut() {
        *elem += count;
    }
    count += 1;
}
assert_eq!(v, &[1, 1, 2, 2, 3]);
  • chunks_exact
rust 复制代码
fn chunks_exact(&self, chunk_size: usize) -> ChunksExact<'_, T>

chunks_exactchunks功能相同,但是对尾部元素的处理略有不同。

对于chunks,如果无法整除,尾部的元素就会形成一个更短的切片。但是chunks_exact则会直接舍弃尾部的元素。

示例:

rust 复制代码
let slice = ['l', 'o', 'r', 'e', 'm'];
let mut iter = slice.chunks_exact(2);
assert_eq!(iter.next().unwrap(), &['l', 'o']);
assert_eq!(iter.next().unwrap(), &['r', 'e']);
assert!(iter.next().is_none());
assert_eq!(iter.remainder(), &['m']);

对于最后被省略的元素,可以通过remainder()方式获取。

  • chunks_exact_mut
rust 复制代码
fn chunks_exact_mut(&mut self, chunk_size: usize) -> ChunksExactMut<'_, T>

依据函数签名就可以看出来,这个方法其实就是chunks_exact的可变切片版本,不多赘述了。


windows

返回一个迭代器,遍历所有长度为 size 的连续窗口。这些窗口是重叠的。如果切片的长度小于 size,迭代器不会返回任何值。

rust 复制代码
fn windows(&self, size: usize) -> Windows<'_, T>

它和chunks功能很类似,但是chunks返回的多个窗口是互不重叠的,window返回的窗口是互相重叠的。

示例:

rust 复制代码
let slice = ['l', 'o', 'r', 'e', 'm'];
let mut iter = slice.windows(3);

assert_eq!(iter.next().unwrap(), &['l', 'o', 'r']);
assert_eq!(iter.next().unwrap(), &['o', 'r', 'e']);
assert_eq!(iter.next().unwrap(), &['r', 'e', 'm']);
assert!(iter.next().is_none());

比如以上代码中,第一个切片和第二个切片,'o''r'元素就重叠了。

因此windows没有提供对应的windows_mut方法,可变借用独占所有权,但是一个元素会在多个窗口出现,这违背借用规则。

如果你需要一个可变的windows窗口,可以结合Cell内部可变性来完成:

rust 复制代码
use std::cell::Cell;

let mut array = ['R', 'u', 's', 't', ' ', '2', '0', '1', '5'];
let slice = &mut array[..];
let slice_of_cells: &[Cell<char>] = Cell::from_mut(slice).as_slice_of_cells();

for w in slice_of_cells.windows(3) {
    Cell::swap(&w[0], &w[2]);
}

assert_eq!(array, ['s', 't', ' ', '2', '0', '1', '5', 'u', 'R']);

这里把数组的每个元素通过可变借用放到Cell里面包装起来,然后再把Cell放到一个数组里面形成一个新数组,再对新数组使用windows方法,就可以实现可变的窗口了。


From

Vec实现了非常多From,因此有非常多种构造方式,这里介绍三种最常见的。

数组

  • 从可变切片 &mut [T] 创建一个新的 Vec<T>,通过克隆元素填充。
rust 复制代码
impl<T> From<&mut [T]> for Vec<T>
  • 从可变数组 &mut [T; N] 创建一个新的 Vec<T>,通过克隆元素填充。
rust 复制代码
impl<T, const N: usize> From<&mut [T; N]> for Vec<T>
  • 从数组 [T; N] 转换为 Vec<T>,直接移动元素。
rust 复制代码
impl<T, const N: usize> From<[T; N]> for Vec<T>

示例:

rust 复制代码
let arr = [1, 2, 3];
let v = Vec::from(arr);
println!("{:?}", v); // 输出 [1, 2, 3]

字符串

  • &str 转换为 Vec<u8>,存储 UTF-8 字节。
rust 复制代码
impl From<&str> for Vec<u8>
  • String 转换为 Vec<u8>,存储 UTF-8 字节。
rust 复制代码
impl From<String> for Vec<u8>

示例:

rust 复制代码
let s = "hello";
let v = Vec::from(s);
println!("{:?}", v); // 输出 [104, 101, 108, 108, 111]

迭代器

  • 从迭代器收集元素生成 Vec<T>,常用 Iterator::collect()
rust 复制代码
impl<T> FromIterator<T> for Vec<T>

示例:

rust 复制代码
let v: Vec<i32> = (0..5).collect();
println!("{:?}", v); // 输出 [0, 1, 2, 3, 4]

相关推荐
doiito1 小时前
【Agent Harness实战】认清现实吧,LLM就是个“超级赌场”,而我们需要的是一套“紧箍咒”
架构·rust
devilnumber1 小时前
Java 迭代器(Iterator)完全指南:从入门到实战
java·开发语言·迭代器
罗超驿1 小时前
13.Java多线程进阶:手动实现线程池与定时器机制详解
开发语言·面试·javaee
弹简特1 小时前
【Java项目-轻聊】10-实现会话管理模块
java·开发语言·数据库
人道领域1 小时前
Java后端开发者转型AIAgent开发路线指南
java·开发语言
盒马盒马1 小时前
Rust:String
java·前端·rust
许彰午1 小时前
35_Java设计模式之工厂模式
java·开发语言·设计模式
凡人叶枫1 小时前
Effective C++ 条款32:确定你的 public 继承塑模出 is-a(是一种)关系
java·linux·开发语言·c++·嵌入式开发
码云骑士1 小时前
18-生成器不只是省内存(上)-yield的状态机模型与帧暂停
c语言·开发语言·python