在迭代期间可以变异的可迭代集合
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在迭代期间可以变异的可迭代集合相关的知识,希望对你有一定的参考价值。
Java中是否存在可以迭代的Java集合数据结构(以及C#),具有以下属性:
- 可以在不影响当前迭代器(已经启动的迭代器的迭代的其余部分)的情况下移除当前元素。
- 可以添加新元素,但也不会影响当前迭代器 - 当前迭代器的迭代仍然存在时,不会将其作为迭代值包含在内。在我的情况下,每次迭代只会添加一个新元素,但在从迭代器中获取新的迭代器之前不应该看到任何元素。
- 元素的顺序无关紧要。
实际上,有一个传入列表和一个传出的项目列表。传入列表被迭代,一些被复制到新列表。在迭代期间,可以将一些新元素添加到新列表中。迭代结束后,旧的传入列表将替换为新的传出列表。整个过程本身就在一个循环中。
因此,与具有这些添加/删除属性的元素相比,每次将元素复制到新构造的集合对象似乎效率低下。
我有点想到某种队列,让我预览当前项目然后要么出列或不出列,然后转到下一项。而且我可以在队列的头部添加更多项目,但不会看到它们,因为我正在走向终点。双重链表可能具有这些属性,对吧?
如果你真的想知道它的用途,那就是在an answer of mine中添加第二个大代码块。
在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
,并且在当前索引(不需要读取您的需求)之前不要更改任何内容。
但是,如果您确实需要在当前索引之前添加或删除元素,则此方法不起作用(或者至少,它变得更复杂)。
对于C#,您可以使用LinkedList<T>
,就像在'C'中一样:
public DoStuff<T>(LinkedList<T> list)
{
var node = list.First;
while(node != null)
{
// do stuff with node
node = node.Next;
}
}
node
是LinkedListNode<T>
类型。您可以使用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<E>
。但是,标准ListIterator<E>
上的List<E>
可能更清洁。
在java中有CopyOnWriteArrayList可以执行您想要的操作:每次更改任何内容时,它都会生成后备数组的副本。但这确实意味着一旦开始迭代,任何迭代都会“一成不变”,因此您可以随意删除/添加到底层集合,而不会影响任何正在运行的迭代器。
您还可以构建具有此行为的自己的集合类型。这是一个3班轮:
public class ConstantIterationArrayList<T> extends ArrayList<T> {
public Iterator<T> iterator() {
return new ArrayList<T>(this).iterator();
}
}
(上面创建了列表的副本,然后为您提供了副本的迭代器,从而方便地确保对此列表的任何修改对该迭代器完全没有影响)。
这是您的问题的真正问题:
以上将不时制作基础数据存储的副本(上面的代码片段每次都是迭代器时都这样做.CopyOnWriteArrayList
每次调用remove()
或add()
时都这样做)。操作'复制基础数据存储'需要O(n)时间,因为对于大于两倍的列表,它需要两倍的时间。
ArrayList
通常具有remove()
操作的属性,除非您要删除列表末尾或非常接近列表末尾的元素,否则是O(n)操作:如果列表是,则从列表中删除元素需要两倍的时间两倍大。
幸运的是,现代CPU具有相当大的缓存,并且可以在缓存页面内以极快的速度运行。这转化为:尽管复制数据感觉效率低下,但实际上,只要支持数组适合页面左右,它就比基于LinkedList
语义的数据存储快得多。我们谈论的是多达~1000个元素给予或接受。 (注意,一般来说,你对LinkedList
做的几乎所有事情都是O(n),并且ArrayList
在现代CPU架构中表现得很好,LinkedList
往往做得很差。重点是:LinkedList
很少是正确答案! )
所以,如果你在这个列表中的项目不超过1000个,我会继续使用CopyOnWriteArrayList
或我上面为你写的自定义类。
但是,如果你有更多,ArrayList
不是在这里使用的正确的数据存储。即使你现在忘记了你不断的迭代需求;在大型数组列表上调用remove()
是个坏主意(除非非常靠近列表的末尾)。在这种情况下,我将精确地描述您需要对此数据类型执行哪些操作以及确切需要快速执行哪些操作,并且一旦您有完整列表,请尝试找到完全符合您需求的集合类型,并且在(可能的)情况下,没有任何特定的存在是一个完美的匹配,自己做一个。如上所述,当您必须滚动自己的数据类型时,通常最好让大部分工作由现有数据类型完成,因此要么扩展现有数据类型,要么封装一个。
以上是关于在迭代期间可以变异的可迭代集合的主要内容,如果未能解决你的问题,请参考以下文章