【问题标题】:C# collections optimised for index 0 insert?为索引 0 插入优化的 C# 集合?
【发布时间】:2016-05-06 00:55:48
【问题描述】:

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

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

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

  • 列表追加时间 0.014 秒。
  • 列出零插入时间 173.343 秒

【问题讨论】:

  • 一个队列在一端插入并在另一端移除。 Deque 在两端都有插入和删除。标准库中没有 Deque,但实现一个并不难。 (Java 有等价的 ArrayDeque)
  • 啊,好吧,不知道那个。然后我假设在双端队列中数据位于容量缓冲区的中间。
  • 双端队列的工作方式与队列相同,就像两端一样。数据只是“环绕”。它可以放在任何地方。

标签: c# .net collections


【解决方案1】:

具有列表形式但针对两端插入和删除进行优化的数据结构 - 但不是中间 - 称为 deque,它是“Double Ended”的缩写队列”。我不知道这些天标准库是否包含双端队列。

如果您对构造不可变双端队列的技术感兴趣,我建议您阅读,按照复杂度递增的顺序排列:

如果您希望创建一个可变双端队列,使用与列表相同的技术非常简单。列表只是数组的包装器,其中数组在远端“太大”。要创建一个可变双端队列,您可以创建一个太大但数据位于中间而不是小端的数组。你只需要跟踪数据的顶部和底部索引是什么,当你碰到数组的任一端时,重新分配数组并将数据复制到中间。

【讨论】:

  • 很好的答案,很高兴看到有人提到使用手指树的不可变解决方案。
  • 这种方法的一个常见变体是有一个额外的私有first 变量。要在列表的开头插入,请增加 first。然后索引运算符将返回(first+index)%capacity。这种方法比 Eric 的建议更复杂,但在列表仍有未填充的数据槽时避免了不必要的分配。
  • 不如你先减量。
  • @Brian:是不是相当于一个循环缓冲区,我们只是在需要时分配一个更大的缓冲区来防止覆盖现有数据?这就是我认为的解决方案。
  • @LucaCremonesi:是的。
【解决方案2】:

您可以创建自己的 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
}

【讨论】:

  • 我认为这对我的用例很有用(在任一侧增长,内部只有索引替换)。干杯。 :)
【解决方案3】:

我的解决方案正如 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

【讨论】:

  • 接受我自己的答案纯粹是因为它解决了我的明确需求(列表针对 add-first 和 add-last 进行了优化,但仍然完全可编辑和访问)
【解决方案4】:

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

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

中途插入链表的问题是,您必须遍历整个列表才能在位置 X 获得项目。这对于大型链表并不适用。如果您在位置 0 有很多插入要做,您可以尝试(如果可能)反转列表,您可以插入“到末尾”。

添加到列表中只是将元素设置在特定索引处并增加“使用的最后一个索引”字段。问题在于调整后备数组的大小,但您可以在列表上设置足够大的容量,以允许在不重新分配和移动的情况下设置所有项目。

【讨论】:

  • this answer 解释了为什么最好避免使用链表。
  • 好的。这是一个不错的帖子。谢谢@luca
【解决方案5】:

正如其他人所指出的,双端队列是您要查找的内容。这里有一个不错的基于列表的实现,它支持入队/出队/窥视等,正如您对适当队列的期望一样,并且在两端,同时还支持所有通用列表操作:

IDoubleEndedQueue

DoubleEndedQueue

ConcurrentDoubleEndedQueue(如果需要线程安全)

正如其他人所提到的,当您使用底层列表实现双端队列时,中间的插入/删除需要数组移位(可以通过比最坏的方向更短的方向移位来稍微减轻这种痛苦 -使用传统列表在索引 0 处插入/删除的情况)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-06
    • 1970-01-01
    • 2019-01-13
    • 2013-11-04
    • 1970-01-01
    相关资源
    最近更新 更多