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类型管理,也就是按字节进行管理。
如果简化逻辑,只有三个核心的字段:len、cap、ptr。
通过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]等等,就是因为usize和Range都实现了这个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内部的三个核心字段是ptr、len、capacity。
这三者之间的关系是什么?这就涉及到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
也就是说,对于直接进行初始化的数组,有多少个元素,那么它的初始的len和capacity就是多少。
再尝试运行以下代码:
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()拿到的是指向数组堆区头部的原生指针。
我分别在Windows11和Ubuntu 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。
因为零大小类型不占用内存空间,根本不需要在堆区开辟内存,因此ptr和capacity都没有实际意义了。
每次你在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 是基于数学集合论中的二元关系偏序、全序和等价的。
- 对于非空集合中的
a、b、c来说,满足下面条件为偏序关系- 自反性:
a <= a - 反对称性:如果
a <= b且b <= a,则a = b - 传递性:如果
a <= b且b <= c,则a <= c
- 自反性:
- 对于非空集合中的
a、b、c来说,满足下面条件为全序关系- 反对称性:若
a <= b且b <= a,则a = b - 传递性:若
a <= b且b <= c,则a <= c - 完全性:
a < b、b < a或a == b必须满足其一,表示任何元素都可以相互比较
- 反对称性:若
- 对于非空集合中的
a、b、c来说,满足下面条件为等价关系- 自反性:
a == a - 对称性:
a == b,意味着b == a - 传递性:若
a == b且b == 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)符合传递性。因此这就是一个等价关系。
偏序表示集合内部的部分元素可以进行比较,而全序表示所有元素都可以进行比较。
比如说在一个族谱中,假设A是B的祖先,就记作A <= B。满足:
- 自反性:每个人都是自己的祖先,
A <= A - 反对称性:如果
A是B的祖先,B又是A的祖先,那说明A == B - 传递性:如果
A是B的祖先,B是C的祖先,那么A一定是C的祖先
但是在这样一个族谱关系中,并不是任意两个人都可以进行比较。比如说C和D是亲兄弟,C不是D的祖先,D也不是C的祖先。这就是一种对于局部可以进行比较,但不是任意两个元素都能比较的关系,叫做偏序。
而对于全序,就很好理解了,比如说将全人类按照年龄进行排序,你随便挑两个人A和B,一定可以比出结果a < b、b < a 或 a == b。
假设我要对一个数组的所有元素进行排序,你觉得它应该满足哪一种关系?当然是全序关系。
再比如说,哈希表中基于哈希值对元素进行求余分组,又应该满足哪一种关系?那就是等价关系。
所以说编程中需要这些数学逻辑抽象,Rust将这些二元关系以Trait的形式体现。
Ordering
首先为了表示A > B、A < B、A == 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,比如 i32、String、Vec<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
Eq 是 PartialEq 的子 Trait,用于保证严格等价。
rust
pub trait Eq: PartialEq<Self> { }
也就是说,只要实现了 PartialEq 并且能满足自反性、对称性、传递性,就可以安全地写 impl Eq for T {}。
相比于PartialEq,Eq对自反性有要求,所有元素必须等于自己。
例如:
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,它默认将结构体内部的字段逐个进行比较,只要索引字段都相等,那么就认为两个结构体相等。
f32 和 f64 则无法实现 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。
因此,Ord 和 PartialOrd 的区别就在于PartialOrd 可能出现不知道谁大谁小的情况,返回 None,Ord 总能给出确定的结果
例如内置的整数类型:
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
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)。而4和100不存在,分别返回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,
相比于sort,sort_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_exact和chunks功能相同,但是对尾部元素的处理略有不同。
对于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]