Arrays 支持O(1)随机存取和最坏情况O(1)追加的数据结构?

Arrays 支持O(1)随机存取和最坏情况O(1)追加的数据结构?,arrays,performance,data-structures,language-agnostic,big-o,Arrays,Performance,Data Structures,Language Agnostic,Big O,我实现了一个可调整大小的索引集合,该集合使用数组存储其元素(如.NET中的List或Java中的ArrayList)。但在关键时刻总是会有一次令人讨厌的插入,此时集合刚刚达到其容量,而下一次插入需要将内部数组中所有元素的完整副本复制到一个新的数组中(大概两倍大) 在我看来,一个常见的错误是使用链表来“修复”这个问题;但我相信为每个元素分配一个节点的开销可能是相当浪费的,而且事实上会使保证O(1)插入的好处相形见绌。在这种罕见的情况下,阵列插入是昂贵的,而实际上,其他每一次阵列插入都是非常便宜(和

我实现了一个可调整大小的索引集合,该集合使用数组存储其元素(如.NET中的
List
或Java中的
ArrayList
)。但在关键时刻总是会有一次令人讨厌的插入,此时集合刚刚达到其容量,而下一次插入需要将内部数组中所有元素的完整副本复制到一个新的数组中(大概两倍大)

在我看来,一个常见的错误是使用链表来“修复”这个问题;但我相信为每个元素分配一个节点的开销可能是相当浪费的,而且事实上会使保证O(1)插入的好处相形见绌。在这种罕见的情况下,阵列插入是昂贵的,而实际上,其他每一次阵列插入都是非常便宜(和更快)的

我认为可能有意义的是一种由数组链表组成的混合方法,即每次当前“头”数组达到其容量时,都会向链表中添加一个两倍大的新数组。那么就不需要复制,因为链表仍然具有原始数组。本质上,这与
List
ArrayList
方法类似(在我看来),除了以前需要复制内部数组所有元素的地方,这里只需要分配一个新数组并插入一个节点

当然,如果需要,这会使其他功能复杂化(例如,在集合中间插入/删除);但正如我在标题中所表达的,我实际上只是在寻找一个只添加(和迭代)的集合


有没有适合这个目的的数据结构?或者,你自己能想到一个吗?

好的。您所描述的几乎完全是C++标准库中的内容。区别在于数组(通常)用于保存指向子数组的指针,而不是使用链表。

一个想法是创建一个包含少量元素的列表,如:

struct item
{
    int data[NUM_ITEMS];
    item *next;
}

在这种情况下,insert将占用
O(1)
,如果达到限制,只需创建一个新块并将其附加到列表的末尾

有一个称为可扩展数组的漂亮结构,它具有最坏情况下的O(1)插入和O(n)内存开销(也就是说,它与动态数组具有渐近可比性,但具有O(1)最坏情况插入)。诀窍是采用向量使用的方法-加倍和复制-但要使复制变慢。例如,假设您有一个由四个元素组成的数组,如下所示:

[1] [2] [3] [4]
如果要添加一个新数字,例如5,请首先分配一个两倍大的数组:

[1] [2] [3] [4]
[ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ]
接下来,在新数组中插入5:

[1] [2] [3] [4]
[ ] [ ] [ ] [ ] [5] [ ] [ ] [ ]
最后,将4从旧阵列中拉入新阵列:

[1] [2] [3] [ ]
[ ] [ ] [ ] [4] [5] [ ] [ ] [ ]
从现在起,无论何时进行插入,都要将元素添加到新数组中,并从旧数组中再下拉一个元素。例如,在添加6之后,我们将得到

[1] [2] [ ] [ ]
[ ] [ ] [3] [4] [5] [6] [ ] [ ]
再插入两个值后,我们将在这里结束:

[ ] [ ] [ ] [ ]
[1] [2] [3] [4] [5] [6] [7] [8]
如果现在需要再添加一个元素,则丢弃现在为空的旧数组,并分配一个比当前数组大两倍的数组(能够容纳16个元素):

重复这个过程。不考虑内存分配的成本(通常是数组大小的次线性),每次插入最多只能做O(1)个工作

查找仍然是O(1),因为您只需决定这两个数组中的哪一个,中间的插入是O(n),因为改组。 如果你好奇的话,我知道我不知道你会发现它有多有用,但是非常欢迎你尝试一下


如果您想花一点时间阅读一篇研究论文并尝试实现一个相当复杂的数据结构,您可以在O中得到相同的结果(最坏情况下的O(1)append)(√n) 空间开销(顺便说一句,这是可以证明是最优的)使用我从来没有真正实现过这一点,但如果内存是一种超级稀缺的资源,那么它肯定是值得一读的。有趣的是,它使用上述构造作为子例程

当我需要这样一个容器时,我会使用

中描述的结构实现,谢谢;我以前听说过deque,但出于某种原因,我只是假设它通常是作为循环队列实现的(比如.NET中的
队列
,对不起,我对C++不是很有经验),它为推/弹出操作暴露了头部和尾部。很高兴知道@Dan Tao:让你知道,C++的
std::dequeue
像数组一样支持O(1)随机访问。哈哈,这真是太棒了,这正是我所希望的那种新颖想法!谢谢@丹涛-没问题!实际上,我在阅读一篇研究论文时遇到了这个问题,该论文需要将它作为快速优先级队列中的一个子程序。他们在脚注中提到了这一点,我认为讽刺的是,这是论文中最重要的结果!你知道,在接受了这个答案之后,我突然想到了一件伤心的事。。。在.NET中,因为数组分配本身是O(n)——所有元素都被初始化。你知道它在Java中是否也是这样工作的吗?@dantao-你是对的,它需要O(n)来清空内存。我现在想知道的是,您是否可以在实践中看到这一点,或者GC是否足够聪明,可以将它回收的内存归零(因为当您分配类对象时,所有内容都默认为零)。在这种情况下,执行分配实际上可能不需要O(n),因为它已经在后台归零了。但你是对的。。。我完全没有想到这一点。@templatetypedef:分配的摊余成本很可能至少是O(N),如果没有其他原因的话,因为在垃圾回收器运行之前,可以分配的内存量有一个有限的限制L,因此,分配N个字节的摊销成本必须至少是GC循环最低成本的N/L倍。如果成本
[1] [2] [3] [4] [5] [6] [7] [8]
[ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ]