如何根据条件重复向量中的某些元素?

时间:2017-05-20 06:46:30

标签: performance vector rust mutability

我在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,即在迭代时修改它吗?

  • 我认为,我的解决方案效率低下:我分配了很多向量,但我无法保证这将得到优化。是否有更好的解决方案:可读且分配较少?

4 个答案:

答案 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.Unboxedrepa,那么在迭代时您也无法插入元素。

  

我的解决方案正如我想象的那样效率低下:我分配了很多向量,我无法保证这将得到优化。这是一个更好的解决方案:可读性和分配更少?

这样的事可能有用。它仍然具有功能感,但通过分配一个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]);
}

注意事项:

如果您真的设置尝试重用内存,我会沿着迭代器向后向前走以帮助避免索引失效:

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,而是查看Iteratori32元素。然后一切都将成为堆栈中的变量,并且根本不会进行任何分配。它可能看起来像这样:

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 内存分配!只是堆栈分配的变量,允许在发布版本中使用高效的代码。