【问题标题】:How do I turn a circular buffer into a vector in O(n) without an allocation?如何在没有分配的情况下将循环缓冲区转换为 O(n) 中的向量?
【发布时间】:2015-11-02 10:42:20
【问题描述】:

我有一个Vec,它是一个循环缓冲区的分配。假设缓冲区已满,因此分配中没有元素不在循环缓冲区中。我现在想将该循环缓冲区转换为Vec,其中循环缓冲区的第一个元素也是Vec 的第一个元素。作为一个例子,我有这个(分配)功能:

fn normalize(tail: usize, buf: Vec<usize>) -> Vec<usize> {
    let n = buf.len();
    buf[tail..n]
        .iter()
        .chain(buf[0..tail].iter())
        .cloned()
        .collect()
}

Playground

显然,这也可以在不分配任何内容的情况下完成,因为我们已经有一个足够大的分配,并且我们有一个 swap 操作来交换分配的任意元素。

fn normalize(tail: usize, mut buf: Vec<usize>) -> Vec<usize> {
    for _ in 0..tail {
        for i in 0..(buf.len() - 1) {
            buf.swap(i, i + 1);
        }
    }
    buf
}

Playground

遗憾的是,这需要buf.len() * tail 交换操作。我相当肯定它可以在buf.len() + tail 交换操作中完成。对于tailbuf.len() 的具体值,我已经能够找出解决方案,但我不确定在一般情况下如何去做。

我的递归部分解决方案can be seen in action

【问题讨论】:

  • 这通常被称为“旋转”向量,例如azillionmonkeys.com/qed/case8.html
  • 啊太棒了。我的 google-fu 让我失望了。我在寻找错误的条款。
  • 如果你想用一些代码添加答案,我将它移植到 Rust here

标签: algorithm rust circular-buffer


【解决方案1】:

最简单的解决方案是使用 3 次反转,这确实是 Algorithm to rotate an array in linear time 中推荐的。

//  rotate to the left by "k".
fn rotate<T>(array: &mut [T], k: usize) {
    if array.is_empty() { return; }

    let k = k % array.len();

    array[..k].reverse();
    array[k..].reverse();
    array.reverse();
}

虽然这是线性的,但这需要最多读取和写入每个元素两次(反转具有奇数个元素的范围不需要接触中间元素)。另一方面,非常可预测的反转访问模式与预取 YMMV 配合得很好。

【讨论】:

  • 注意。你不需要自己实现reversearray[..k].reverse(); array[k..].reverse(); array.reverse(); 应该可以工作。
  • @huon:哼哼……我什至没有想过要去找它。它不仅有效,而且还使它变得更加简单。
【解决方案2】:

此操作通常称为向量的“旋转”,例如C++ 标准库有std::rotate 来执行此操作。有known algorithms 用于执行操作,尽管如果您尝试通用/使用非Copy 类型进行移植,则在移植时可能需要非常小心,其中swaps 成为关键,因为通常不能直接从向量中读取内容。

也就是说,可能可以将unsafe 代码与std::ptr::read/std::ptr::write 一起使用,因为数据只是在四处移动,因此无需执行调用者定义的代码或非常对异常安全的复杂担忧。

上面链接中的 C 代码端口(@ker 提供):

fn rotate(k: usize, a: &mut [i32]) {
    if k == 0 { return }

    let mut c = 0;
    let n = a.len();
    let mut v = 0;
    while c < n {
        let mut t = v;
        let mut tp = v + k;
        let tmp = a[v];
        c += 1;
        while tp != v {
            a[t] = a[tp];
            t = tp;
            tp += k;
            if tp >= n { tp -= n; }
            c += 1;
        }
        a[t] = tmp;
        v += 1;
    }
}

【讨论】:

  • 事实上,当环形缓冲区未满时,它会变得更加复杂,因为那时我需要防止从未初始化的内存中读取。但这应该是可以克服的。
  • @ker:如果您愿意实施两步流程,我认为您可以相对轻松地处理不完整的情况:(1)将所有元素移到前面(Vec 将需要它们)和(2)适当洗牌。当然,一次处理所有事情在移动方面会更有效......但是(1)可以使用批量memcpy(以及因此矢量化操作)来实现,因此这两个步骤过程可能最终对缓存更加友好。
  • 这其实很聪明。但是我仍然不确定未初始化的内存。我可以memcpy未初始化的内存吗?
  • 您的端口可以通过删除tmp变量并使用a.swap(t, tp)而不是a[t] = a[tp]来实现移动语义
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-09-22
  • 1970-01-01
  • 2013-09-26
  • 1970-01-01
  • 1970-01-01
  • 2014-10-30
  • 2011-03-15
相关资源
最近更新 更多