可迭代的集合,可以在迭代期间进行变异

Java中是否存在可以迭代的Java集合数据结构(以及C#),具有以下属性:

  • 可以在不影响当前迭代器(已经启动的迭代器的迭代的其余部分)的情况下移除当前元素。
  • 可以添加新元素,但也不会影响当前迭代器 – 当前迭代器的迭代仍在进行时,不会将其作为迭代值包含在内。 在我的情况下,每次迭代只会添加一个新元素,但是在从迭代中获取新的迭代器之前不应该看到任何元素。
  • 元素的顺序无关紧要。

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

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

我有点想到某种队列,让我预览当前项目然后要么出列或不出列,然后转到下一项。 我可以在队列的头部添加更多项目,但是不会看到它们,因为我正在走向终点。 双重链表可能有这些属性,对吧?

如果你真的想知道它的用途,那就是在我的答案中加入第二个大代码块。

在C#中,这很容易与Listfor (...)而不是foreach (...)

 using System; using System.Collections.Generic; using System.Linq; namespace Demo { static class Program { static void Main() { List 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 ,并且在当前索引(不需要读取您的需求) 之前不要更改任何内容。

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

对于C#,您可以像在好的C中一样使用LinkedList

 public DoStuff(LinkedList list) { var node = list.First; while(node != null) { // do stuff with node node = node.Next; } } 

node的类型为LinkedListNode 。 您可以使用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 。 但是,标准List ListIterator上的ListIterator List可能更清晰。

在java中有CopyOnWriteArrayList可以执行您想要的操作:每次更改任何内容时,它都会生成后备数组的副本。 但这确实意味着一旦开始迭代,任何迭代都会“一成不变”,因此您可以随意删除/添加到底层集合,而不会影响任何正在运行的迭代器。

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

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

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

这是您的问题的真正问题:

上面将不时制作底层数据存储的副本(上面的代码片段每次都是迭代器时都CopyOnWriteArrayList 。每次调用remove()add()时, CopyOnWriteArrayListCopyOnWriteArrayList )。 “复制基础数据存储”操作需要O(n)时间,因为对于大于两倍的列表,它需要两倍的时间。

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

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

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

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