【问题标题】:Iterable collection that can be mutated during iteration可以在迭代期间发生变异的可迭代集合
【发布时间】:2019-02-14 15:01:43
【问题描述】:

在 Java(如果你知道的话,还有 C#)中是否有一个可以迭代的集合数据结构,具有以下属性:

  • 可以删除当前元素而不影响当前迭代器(已启动的迭代器的其余迭代)。
  • 可以添加新元素,但也不会影响当前迭代器 - 在当前迭代器的迭代仍在进行时,不会将其作为迭代值包含在内。在我的例子中,每次迭代只会添加一个新元素,但在从可迭代对象中获取新迭代器之前,不会看到任何元素。
  • 元素的顺序无关紧要。

实际上,有一个传入列表和一个传出项目列表。传入的列表被迭代,一些被复制到一个新的列表中。一些新元素可以在迭代期间添加到新列表中。迭代结束后,旧的传入列表被新的传出列表替换。这整个过程本身就是一个循环。

因此,与具有这些添加/删除属性的集合对象相比,每次将元素复制到新构建的集合对象似乎效率低下。

我在想某种队列,它可以让我预览当前项目,然后将其出列或不退出,然后移至下一个项目。而且我可以将更多项目添加到队列的头部,但不会看到它们,因为我正在向最后移动。双向链表可以具有这些属性,对吗?

如果您真的想知道它的用途,那就是在an answer of mine 中添加第二个大代码块。

【问题讨论】:

  • 对于 C#:无法从枚举器上下文中修改通常的集合,但您可以查看 TPL Dataflow Library
  • 在java中,见List#listIteratorListIterator
  • 你为什么要 C# 和 java?如果您在 java 中需要它,但 C# 中有一个类,它对您有什么帮助?
  • @TimSchmelter 这对我有帮助,因为我在这两种情况下都进行了编程,并且我想了解自己的学习情况。这并不奇怪,是吗?
  • 我只能第二个@Misha,在Java中,只需使用一个可变列表并获得一个ListIterator。但就地修改并不总是比填充新列表更有效。

标签: java c# collections iteration


【解决方案1】:

在 C# 中,使用 List<T>for (...) 而不是 foreach (...) 很容易:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Demo
{
    static class Program
    {
        static void Main()
        {
            List<int> list = Enumerable.Range(1, 10).ToList();

            for (int i = 0; i < list.Count; ++i)
            {
                if ((list[i] % 3) == 0) // Remove multiples of 3.
                    list.RemoveAt(i--); // NOTE: Post-decrement i
                else if ((list[i] % 4) == 0) // At each multiple of 4, add (2*value+1)
                    list.Add(list[i] * 2 + 1);
                else
                    ; // Do nothing.
            }

            Console.WriteLine(string.Join(", ", list)); // Outputs 1, 2, 4, 5, 7, 8, 10, 17
        }
    }
}

这里的关键是使用索引而不是foreach,并且在当前索引之前不要更改任何内容(从阅读您的要求来看,不需要)。

但是,如果您确实需要在当前索引之前添加或删除元素,那么这种方法不起作用(或者至少,它变得更加复杂)。

【讨论】:

  • 从列表中间删除一个项目的性能是什么?
  • @ErikE: stackoverflow.com/questions/6052003/… (MSDN: "这个方法是一个 O(n) 操作,其中 n 是 (Count - index)")
  • 我希望最终列表中有 9,因为添加的元素不应该被迭代。
  • @MatthewWatson 这就是我害怕的……洗牌所有剩余的项目。
  • @ErikE 您应该将其添加到您的要求中。他们说New elements can be added, but will not affect the current iteration.,而你的意思是New elements can be added, but will not affect the current iteration **or any future iteration**.
【解决方案2】:

对于 C#,您可以使用 LinkedList&lt;T&gt;,就像在好的 ol' C 中一样:

public DoStuff<T>(LinkedList<T> list)
{
    var node = list.First;

    while(node != null)
    {
        // do stuff with node

        node = node.Next;
    }
}

node 的类型为 LinkedListNode&lt;T&gt;。您可以使用node.Value 访问该值,使用list.Remove(node) 删除该值。对于T elem,您还有list.AddAfter(node, elem)list.AddBefore(node, elem)list.AddFirst(elem)list.AddLast(elem)。所有这些操作都是 O(1)。你可以用它做各种迭代,如果你只想迭代原始元素,然后在做任何事情之前缓存下一个节点并记住最后一个节点:

var lastNode = list.Last;
var node = list.First;

while(node != lastNode.Next)
{
    var nextNode = node.Next;

    // do stuff with node

    node = nextNode;
}

Java 中等价的数据结构也称为LinkedList&lt;E&gt;。但是,标准List&lt;E&gt; 上的ListIterator&lt;E&gt; 使用起来可能更简洁。

【讨论】:

  • 如果使用 ListIterator,如何避免在迭代过程中看到新元素?
  • 您可以跳过添加的元素。 listIterator.add(e); 然后立即listIterator.next();
【解决方案3】:

在 java 中有 CopyOnWriteArrayList 可以做你想做的事:每次你改变任何东西时,它都会复制一个后备数组。但这确实意味着一旦您开始迭代,任何迭代都是“一成不变的”,因此您可以随意删除/添加到底层集合,而不会影响任何正在运行的迭代器。

您还可以构建自己的具有此行为的集合类型。这将是一个 3 班轮:

public class ConstantIterationArrayList<T> extends ArrayList<T> {
    public Iterator<T> iterator() {
        return new ArrayList<T>(this).iterator();
    }
}

(上面创建了列表的副本,然后为您提供了副本的迭代器,从而方便地确保对该列表的任何修改绝对不会影响该迭代器)。

这是您问题的真正问题:

上面会不时复制底层数据存储(我上面的 sn-p 每次创建迭代器时都会这样做。CopyOnWriteArrayList 每次调用 remove()add() 时都会这样做)。 “复制底层数据存储”操作需要 O(n) 时间,例如,两倍大的列表需要两倍的时间。

ArrayList 通常具有以下属性:remove() 操作,除非您要删除位于列表末尾或非常接近列表末尾的元素,否则是 O(n) 操作:如果列表是两倍大,则从列表中删除一个元素需要两倍的时间。

幸运的是,现代 CPU 具有相当大的缓存,并且可以在缓存页面内以极快的速度运行。这意味着:尽管复制数据感觉效率很低,但在实践中,只要支持数组适合一个页面左右,它比基于LinkedList 语义的数据存储快得多。我们正在谈论最多约 1000 个元素的给予或接受。 (注意,一般来说,你对LinkedList 所做的几乎所有事情都是O(n),而ArrayList 往往与现代 CPU 架构配合得很好,LinkedList 往往做得很差。重点是:LinkedList 也很少是正确答案!)

因此,如果您在此列表中的项目不超过 1000 项,我会继续使用 CopyOnWriteArrayList 或我在上面为您编写的自定义类。

但是,如果您有更多,则ArrayList 不是在这里使用的正确数据存储。即使您暂时忘记了不断迭代的需求;在大型数组列表上调用 remove() 是一个坏主意(除非删除非常接近列表末尾)。在这种情况下,我会准确地勾勒出您需要对该数据类型执行哪些操作以及哪些操作真正需要快速,一旦您有了完整的列表,请尝试找到一个完全符合您需求的集合类型,并且在(可能的)情况下,没有任何特定的完美匹配,自己做一个。像上面一样,当您必须滚动自己的数据类型时,让现有数据类型完成大部分工作通常是个好主意,因此要么扩展现有数据类型,要么封装一个。

【讨论】:

  • 听起来我最初在链接答案中写的实现(在我的问题的底部)可能接近我能得到的最好的,嗯?
猜你喜欢
  • 2021-12-20
  • 2012-04-02
  • 2011-02-17
  • 2018-03-04
  • 2019-03-04
  • 2015-07-23
  • 1970-01-01
  • 2014-01-31
  • 2021-10-22
相关资源
最近更新 更多