我在kata期间遇到了这个问题。我更具可读性的实现如下:
use std::vec::Vec;
fn repeat_even(v: Vec<i32>) -> Vec<i32> {
v.into_iter().flat_map(|x| match x % 2 { 0 => vec![x, x], _ => vec![x] }).collect()
}
fn main() {
let v = vec![1, 2, 3, 4, 6];
assert_eq!(repeat_even(v), vec![1, 2, 2, 3, 4, 4, 6, 6]);
}
我有两个问题:
是否有必要创建另一个Vec
?可以使用相同的Vec
,即在迭代时修改它吗?
我认为,我的解决方案效率低下:我分配了很多向量,但我无法保证这将得到优化。是否有更好的解决方案:可读且分配较少?
答案 0 :(得分:5)
你可以在同一个向量中完成它,但每次遇到偶数时都需要移动向量的其余部分(在加倍数之后),这是低效的。使用新的向量和简单的循环来做它会更好:
fn main() {
let v = vec![1, 2, 3, 4, 6];
let mut v2 = Vec::with_capacity(v.len() + v.iter().filter(|&n| n % 2 == 0).count());
for n in v {
v2.push(n);
if n % 2 == 0 { v2.push(n) }
}
assert_eq!(v2, vec![1, 2, 2, 3, 4, 4, 6, 6]);
}
此解决方案仅分配内存一次,其中包含保存所有数字所需的确切空间,包括双倍均衡。
答案 1 :(得分:3)
是否有必要创建另一个
Vec
?可以使用相同的Vec
,即在迭代时修改它吗?
可能但效率不高。 Vec
在堆上分配一块内存,其中每个元素与下一个元素相邻。如果你只是想对每个元素进行一些数值运算,那么是的,你可以在适当的位置进行操作。但是你需要在其他元素之间插入新元素,这意味着将所有以下元素向右移动一个位置,并且(可能)分配更多内存。
您正在考虑的Haskell代码可能正在使用Haskell Data.List
,它是一个链接列表而不是矢量。如果您使用了更加节省内存的结构,例如Data.Vector.Unboxed
或repa,那么在迭代时您也无法插入元素。
我的解决方案正如我想象的那样效率低下:我分配了很多向量,我无法保证这将得到优化。这是一个更好的解决方案:可读性和分配更少?
这样的事可能有用。它仍然具有功能感,但通过分配一个Vec
然后改变它来工作:
fn double_even(v: Vec<i32>) -> Vec<i32> {
// allocate for the worst case (i.e. all elements of v are even)
let result = Vec::with_capacity(v.len() * 2);
v.into_iter().fold(result, |mut acc, n| {
acc.push(n);
if n % 2 == 0 {
acc.push(n);
}
acc
})
}
最后你也可以shrink_to_fit()
,但它看起来有点丑陋,因为你无法将解决方案作为表达式返回。
答案 2 :(得分:3)
flat_map
期望迭代器,因此您可以返回值的迭代器:
use std::iter;
fn double_even(v: &[i32]) -> Vec<i32> {
v.iter().flat_map(|&x| {
let count = if x % 2 == 0 { 2 } else { 1 };
iter::repeat(x).take(count)
}).collect()
}
fn main() {
let v = vec![1, 2, 3, 4, 6];
assert_eq!(double_even(&v), vec![1, 2, 2, 3, 4, 4, 6, 6]);
}
注意事项:
use std::vec::Vec
。它已经通过prelude导入。vec!()
;请改用vec![]
。这对编译器来说并不重要,但它对人类很重要。如果您真的设置尝试重用内存,我会沿着迭代器向后向向前走以帮助避免索引失效:
fn double_even(mut v: Vec<i32>) -> Vec<i32> {
for i in (0..v.len()).rev() {
let val = v[i];
if val % 2 == 0 {
v.insert(i, val);
}
}
v
}
这可能在算法上更糟;每个insert
移动后面的所有数据。我相信当每个元素都是偶数时,最坏情况是O(n^2)
。
我也不会在这里采取按值计算。我反而采取了一个可变的参考。如果你真的需要它,你总是可以将它包回一个值:
fn double_even_ref(v: &mut Vec<i32>) {
for i in (0..v.len()).rev() {
let val = v[i];
if val % 2 == 0 {
v.insert(i, val);
}
}
}
fn double_even(mut v: Vec<i32>) -> Vec<i32> {
double_even_ref(&mut v);
v
}
答案 3 :(得分:2)
是否有必要创建另一个Vec?可以使用相同的Vec,即在迭代时修改它吗?
我认为,我的解决方案效率低下:我分配了很多向量,但我无法保证这将得到优化。是否有更好的解决方案:可读且分配较少?
您可以做的一件非常惯用的事情是将您的函数实现为&#34;迭代器适配器&#34; - 也就是说,而不是特别处理Vec
,而是查看Iterator
个i32
元素。然后一切都将成为堆栈中的变量,并且根本不会进行任何分配。它可能看起来像这样:
struct DoubleEven<I> {
iter: I,
next: Option<i32>,
}
impl<I> Iterator for DoubleEven<I>
where I: Iterator<Item=i32>
{
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
self.next.take().or_else(||
self.iter.next().map(|value| {
if value % 2 == 0 { self.next = Some(value) }
value
})
)
}
}
然后你可以写
fn main() {
let vec = vec![1, 2, 3, 4, 5, 6];
let double_even = DoubleEven {
iter: vec.into_iter(),
next: None,
};
for x in double_even {
print!("{}, ", x) // prints 1, 2, 2, 3, 4, 4, 5, 6, 6,
}
}
更好的是,您可以将函数double_even
添加到可以转换为i32
的迭代器的任何内容中,从而允许您编写以下内容:
trait DoubleEvenExt : IntoIterator + Sized {
fn double_even(self) -> DoubleEven<Self::IntoIter> {
DoubleEven {
iter: self.into_iter(),
next: None,
}
}
}
impl<I> DoubleEvenExt for I where I: IntoIterator<Item=i32> {}
fn main() {
let vec = vec![1, 2, 3, 4, 5, 6];
for x in vec.double_even() {
print!("{}, ", x) // prints 1, 2, 2, 3, 4, 4, 5, 6, 6,
}
}
现在我承认在这种情况下样板文件正在累加,但你可以看到在调用现场,代码非常简洁。对于更复杂的适配器,此模式非常有用。此外,除了最初的Vec
分配之外,还有 no 内存分配!只是堆栈分配的变量,允许在发布版本中使用高效的代码。