【问题标题】:How do I iterate over a HashSet while simultaneously updating it in Rust?如何在 Rust 中同时更新 HashSet 时迭代它?
【发布时间】:2021-12-18 12:20:07
【问题描述】:

在大多数语言中,对容器进行迭代并同时对其进行变异是一种明显的反模式。在 Rust 中,借用检查器使它不可能。

但在某些情况下需要这样做。一个例子是 Earley 的解析算法。我是 Rust 的新手,我不知道在扩展 HashSet (或者实际上是任何容器)时有一种已知的好方法。我想出了以下内容(从 Earley 用例概括而来):

use std::collections::HashSet;

fn extend_from_within<T, F>(original: &mut HashSet<T>, process: F) 
    where T: std::cmp::Eq,
          T: std::hash::Hash,
          F: Fn(&T) -> Set<T>
{
    let mut curr : HashSet<T> = HashSet::new(); // Items currently being processed
    let mut next : HashSet<T> = HashSet::new(); // New items

    // go over the original set once
    let hack : &HashSet<T> = original; // &mut HashSet is not an iterator
    for x in hack {
        for y in process(x) {
            if !original.contains(&y) {
                curr.insert(y);
            }
        }
    }

    // Process and extend until no new items emerge
    while !curr.is_empty() {
        for x in &curr {
            for y in process(x) {
                // make sure that no item is processed twice
                // the check on original is redundant, but might save space
                if !curr.contains(&y) && !original.contains(&y) {
                    next.insert(y);
                }
            }
        }

        original.extend(curr.drain());
        std::mem::swap(&mut curr, &mut next);
    }
}

如您所见,任何对 process 的调用都可以产生多个项目。它们都被添加到集合中,并且它们都必须被处理,但前提是它们还没有被看到。这工作得很好。但是保持最多 4 组,对每个项目进行 3 次成员资格检查(其中一个在原始数组上两次)对于这个问题来说似乎很荒谬。有没有更好的办法?

【问题讨论】:

  • 你能准确地说出你想要什么样的突变吗?值的突变 ?条目删除 ?条目添加?
  • 条目添加。
  • 哈希集,无论是什么语言,在结构上都不适合混合迭代和加法,因为没有不被加法破坏的内在顺序。您似乎从错误的集合(或错误的内容)开始
  • 我明白了。我根本不致力于散列集,但我需要独特的元素。具体来说,I' 使用四个使用整数的结构作为项目(集合的成员),以防万一。还有哪些其他选择?已排序的向量?
  • 我不会深入研究,因为我没有时间研究算法,但由于您的结构可以复制,您可能只需结合一个哈希集来检查唯一性和一个 vecdeque 充当一个队列。拥有 Copy 元素可以让这里的一切变得更轻松,而无需一个独特的集合。

标签: parsing rust containers hashset


【解决方案1】:

我不知道在扩展 HashSet(或者实际上是任何容器)时迭代它的已知好方法

我认为在迭代 HashMaps/HashSets 时处理修改主要有三种模式:

  • 将修改收集到Vec 并在迭代后应用它们
  • draining HashSetcollecting 进入一个新的
  • retain,但仅用于删除

但无论如何,你的情况很特别,你想用process 饱和你的集合,对吧?

有没有更好的办法?

在这种情况下,我可能会选择

let mut todo = original.iter().map(Clone::clone).collect::<VecDeque<T>>();
while let Some(x) = todo.pop_front() {
    for x in process(&x) {
        if original.insert(x.clone()) {
            todo.push_back(x);
        }
    }
}
  • VecDeque 可能不是必需的(普通的Vec 可以),除非您对处理元素的顺序有一些要求。缓存方面,Vec 可能会更好。
  • 可以通过将process(x)Set 结果保留在todo 中来避免clones。在不知道SetT 是什么的情况下,我不能说哪个更好。如果process 的结果通常为空,这也允许在将它们推送到todo 之前过滤掉它们。
  • [编辑:] 另一个变体可能是
    let mut todo = original
        .iter()
        .flat_map(&process)
        .filter(|x| !original.contains(x))
        .collect::<VecDeque<T>>();
    todo.iter().for_each(|x| {
        original.insert(x.clone());
    });
    // while let …
    
    这可能会分配更少,但会导致更多的哈希映射访问/缓存未命中。找出哪个变体更有效确实需要进行基准测试。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-01
    • 1970-01-01
    • 2015-06-30
    • 2015-11-09
    相关资源
    最近更新 更多