哪些C#集合最适合零索引插入和追加到最终?
我想简单的一个是LinkedList
,但考虑到我对大字节缓冲区的兴趣,我怀疑每个节点的内存成本可能对我的需求而言过于昂贵。
我的理解是,普通List
有一个缓冲区支持,其容量通常大于实际数据。当数据填充缓冲区时,将创建一个新缓冲区,并将旧内容传输到新缓冲区。这对于追求结束非常有用,但对于一开始的增长却是可怕的。我的测试,将一百万个随机整数附加/插入List
:
答案 0 :(得分:6)
具有列表形式但针对两端插入和删除进行优化的数据结构 - 但不是中间 - 称为 deque ,它是“Double End”的缩写队列”。这些天我不知道标准库是否包含deque。
如果您对构建不可变deques 的技术感兴趣,我建议您阅读,以增加复杂性的顺序:
如果您希望创建一个可变的双端队列,使用与列表相同的技术可以非常简单。列表只是一个数组的包装器,其数组在远端是“太大”。要制作一个可变的双端队列,你可以制作一个太大的数组,但数据位于中间,而不是小端。您只需要跟踪数据的顶部和底部索引是什么,当您碰到数组的任何一端时,重新分配数组并将数据复制到中间。
答案 1 :(得分:3)
您可以创建自己的IList<T>
实现,其中包含两个列表:一个用于添加到前面的项目(以相反顺序存储),另一个用于添加到后面的项目(按正确顺序)。这将确保列表开头或结尾的所有插入都是O(1)(除少数需要增加容量的情况外)。
public class TwoEndedList<T> : IList<T>
{
private readonly List<T> front = new List<T>();
private readonly List<T> back = new List<T>();
public void Add(T item)
{
back.Add(item);
}
public void Insert(int index, T item)
{
if (index == 0)
front.Add(item);
else if (index < front.Count)
front.Insert(front.Count - index, item);
else
back.Insert(index - front.Count, item);
}
public IEnumerator<T> GetEnumerator()
{
return front.Reverse<T>().Concat(back).GetEnumerator();
}
// rest of implementation
}
答案 2 :(得分:2)
我的解决方案正如Eric Lippert所说;保持实际数据浮动在后备缓冲区的中间,而不是在开头。但是,这仍然是一个列表,而不是队列;仍然可以在任何地方添加/删除/替换。
public class BList<T> : IList<T>, IReadOnlyList<T>
{
private const int InitialCapacity = 16;
private const int InitialOffset = 8;
private int _size;
private int _offset;
private int _capacity;
private int _version;
private T[] _items;
public BList()
{
_version = 0;
_size = 0;
_offset = InitialOffset;
_capacity = InitialCapacity;
_items = new T[InitialCapacity];
}
public BList(int initialCapacity)
{
_size = 0;
_version = 0;
_offset = initialCapacity/2;
_capacity = initialCapacity;
_items = new T[initialCapacity];
}
public void Insert(int insertIndex, T item)
{
if (insertIndex < 0)
throw new ArgumentOutOfRangeException(nameof(insertIndex));
var padRight = Math.Max(0, (insertIndex == 0) ? 0 : (insertIndex + 1) - _size);
var padLeft = insertIndex == 0 ? 1 : 0;
var requiresResize = _offset - padLeft <= 0 ||
_offset + _size + padRight >= _capacity;
if (requiresResize)
{
var newSize = _size + 1;
var newCapacity = Math.Max(newSize, _capacity * 2);
var newOffset = (newCapacity / 2) - (newSize / 2) - padLeft;
var newItems = new T[newCapacity];
Array.Copy(_items, _offset, newItems, newOffset, insertIndex);
Array.Copy(_items, _offset, newItems, newOffset + 1, _size - insertIndex);
newItems[newOffset + insertIndex] = item;
_items = newItems;
_offset = newOffset;
_size = newSize;
_capacity = newCapacity;
}
else
{
if (insertIndex == 0)
_offset = _offset - 1;
else if (insertIndex < _size)
Array.Copy(_items, _offset + insertIndex, _items, _offset + insertIndex + 1, _size - insertIndex);
_items[_offset + insertIndex] = item;
_size = _size + 1;
}
_version++;
}
测试插入次数:131072
答案 3 :(得分:1)
原因列表(和一般数组)不适合插入,是插入索引之后的所有元素都必须向上移动。
在开头插入的最佳选项是链接列表,因为您只需要更改对第一个项目,前一个项目(在原始第一个项目上)和下一个项目的引用。这是简单的操作。
链接列表插入中途的问题是,您必须遍历整个列表才能将项目放在X位置。这对于大型链接列表并不适用。如果你在0号位置插入很多东西,你可以尝试(如果可能的话)反转列表,你可以插入到最后的位置。
在列表中追加只是将元素设置为特定索引并增加最后使用的索引&#39;领域。这些问题是调整后备阵列的大小,但您可以在列表上设置足够大的容量,以便在不重新分配和移动的情况下设置所有项目。
答案 4 :(得分:0)
正如其他人所说,一个双端队列正在寻找你正在寻找的东西。这里有一个像样的基于列表的实现,它支持排队/出列/偷看等等,正如你期望从一个正确的队列,并在两端,同时支持所有通用列表操作:
ConcurrentDoubleEndedQueue(如果需要线程安全)
正如其他人所提到的,当你实现一个带有基础列表的双端队列时,中间的插入/移除需要一个阵列移位(由于能够在比最差的方向更短的方向上移动,所以可以稍微减轻其痛苦 - 使用传统列表在索引0处插入/删除的情况。)