针对索引0插入优化的C#集合?

时间:2016-01-28 15:07:55

标签: c# collections

哪些C#集合最适合零索引插入和追加到最终?

我想简单的一个是LinkedList,但考虑到我对大字节缓冲区的兴趣,我怀疑每个节点的内存成本可能对我的需求而言过于昂贵。

我的理解是,普通List有一个缓冲区支持,其容量通常大于实际数据。当数据填充缓冲区时,将创建一个新缓冲区,并将旧内容传输到新缓冲区。这对于追求结束非常有用,但对于一开始的增长却是可怕的。我的测试,将一百万个随机整数附加/插入List

  • 列出附加时间0.014秒。
  • 列出零插入时间173.343sec

5 个答案:

答案 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++;
    }

Full code

测试插入次数:131072

enter image description here

答案 3 :(得分:1)

原因列表(和一般数组)不适合插入,是插入索引之后的所有元素都必须向上移动。

在开头插入的最佳选项是链接列表,因为您只需要更改对第一个项目,前一个项目(在原始第一个项目上)和下一个项目的引用。这是简单的操作。

链接列表插入中途的问题是,您必须遍历整个列表才能将项目放在X位置。这对于大型链接列表并不适用。如果你在0号位置插入很多东西,你可以尝试(如果可能的话)反转列表,你可以插入到最后的位置。

在列表中追加只是将元素设置为特定索引并增加最后使用的索引&#39;领域。这些问题是调整后备阵列的大小,但您可以在列表上设置足够大的容量,以便在不重新分配和移动的情况下设置所有项目。

答案 4 :(得分:0)

正如其他人所说,一个双端队列正在寻找你正在寻找的东西。这里有一个像样的基于列表的实现,它支持排队/出列/偷看等等,正如你期望从一个正确的队列,并在两端,同时支持所有通用列表操作:

IDoubleEndedQueue

DoubleEndedQueue

ConcurrentDoubleEndedQueue(如果需要线程安全)

正如其他人所提到的,当你实现一个带有基础列表的双端队列时,中间的插入/移除需要一个阵列移位(由于能够在比最差的方向更短的方向上移动,所以可以稍微减轻其痛苦 - 使用传统列表在索引0处插入/删除的情况。)