【问题标题】:Big O Notation Arrays vs. Linked List insertions大 O 表示法数组与链表插入
【发布时间】:2011-10-14 16:20:54
【问题描述】:

大 O 表示法数组与链表插入:

根据学术文献,数组是常数 O(1),而链表是线性 O(n)。

一个数组只需要一次乘法和加法。

没有布置在连续内存中的链表需要遍历。

这个问题是,O(1) 和 O(n) 是否分别准确地描述了数组和链表的索引/搜索成本?

【问题讨论】:

标签: data-structures big-o


【解决方案1】:

O(1) 准确描述了在数组末尾插入。但是,如果要插入到数组的中间,则必须移动该元素之后的所有元素,因此在这种情况下插入的复杂性是 O(n) 对于数组。结束追加还可以减少如果数组已满则必须调整数组大小的情况。

对于链表,你必须遍历链表进行中间插入,即O(n)。不过,您不必向下移动元素。

*上有一个很好的图表:http://en.wikipedia.org/wiki/Linked_list#Linked_lists_vs._dynamic_arrays

                          Linked list   Array   Dynamic array   Balanced tree

Indexing                          Θ(n)   Θ(1)       Θ(1)             Θ(log n)
Insert/delete at beginning        Θ(1)   N/A        Θ(n)             Θ(log n)
Insert/delete at end              Θ(1)   N/A        Θ(1) amortized   Θ(log n)
Insert/delete in middle     search time 
                                + Θ(1)   N/A        Θ(n)             Θ(log n)
Wasted space (average)            Θ(n)    0         Θ(n)[2]          Θ(n)

【讨论】:

  • 对不起,如果这是愚蠢的,但图表中的例外不包括添加到 LinkedList 的中间,是吗?或者,正如我猜测的那样,删除与插入相同。既不需要你移动元素,也都需要你遍历列表。
  • @Crowie - 它确实有,它是search time + O(1),其中搜索时间遍历列表(大致为O(n)),插入是O(1) 操作。
  • 啊...是的,我很愚蠢。插入/删除与插入或删除相同。谢谢老哥
  • 对于未排序的数组,在中间插入是没有意义的,这也是为什么学术文本不考虑该操作,而是说数组插入总是O(1)
【解决方案2】:

假设您正在谈论您已经知道插入点的插入,即这不考虑遍历列表以找到正确的位置:

数组中的插入取决于您插入的位置,因为您需要移动现有值。最坏情况(在数组 [0] 处插入)是 O(x)。

在列表中的插入是 O(1),因为您只需要修改相邻项的下一个/上一个指针。

【讨论】:

    【解决方案3】:

    我认为插入数组的速度较慢。当然,你必须迭代一个链表,但你必须分配、保存和释放内存才能插入到数组中。

    【讨论】:

      【解决方案4】:

      您参考了哪些文献?数组的大小在创建数组时确定,之后永远不会改变。插入实际上只能发生在数组末尾的空闲插槽上。任何其他类型的插入都可能需要调整大小,这肯定不是O(1)。链表的大小取决于实现,但必须始终至少大到足以存储其所有元素。元素可以插入到列表的任何位置,找到合适的索引需要遍历。

      【讨论】:

      • CLRS 和其他计算机科学文献中,数组被定义为可以在恒定时间内修改数组长度。显然,大多数语言并非如此,但这被视为实现细节。
      【解决方案5】:

      tldr 未排序的数组 类似于集合。像集合一样,元素可以添加和删除、迭代和读取。但是,与集合一样,谈论在特定位置插入元素是没有意义的,因为这样做会试图将排序顺序强加于定义上未排序的元素。


      根据学术文献,数组是常数 O(1),而链表是线性 O(n)。

      值得理解为什么学术文献引用数组插入为 O(1) 的数组。有几个概念需要理解:

      • 数组被定义为未排序的(除非另有明确说明)。

      • 数组的长度,定义为数组包含的元素个数,可以在O(1)时间内任意增加或减少,并且对数组的最大大小没有限制数组。

        (在真实的计算机中并非如此,由于内存大小、虚拟内存、交换空间等各种因素。但对于算法asymptotic analysis,这些因素并不重要——我们关心随着输入大小向无穷大增加,算法的运行时间如何变化,而不是它在具有特定内存大小和操作系统的特定计算机上如何执行。)

      • InsertdeleteO(1),因为数组是未排序的数据结构。

      • 插入不是赋值

      考虑将元素添加到未排序的数据结构的实际含义。由于没有定义排序顺序,因此实际发生的任何顺序都是任意的并且无关紧要。如果您从面向对象的 API 角度考虑,方法签名将类似于:

      Array.insert(Element e)
      

      请注意,这与其他数据结构的插入方法相同,例如链表或排序数组:

      LinkedList.insert(Element e)
      SortedArray.insert(Element e)
      

      在所有这些情况下,insert 方法的调用者并没有指定插入的值最终存储在哪里——它是数据结构的内部细节。此外,调用者尝试在数据结构中的特定位置插入元素是没有意义的——对于已排序或未排序的数据结构。对于(未排序的)链表,根据定义,该列表是未排序的,因此排序顺序无关紧要。对于已排序的数组,根据定义,插入操作将在数组的特定点插入一个元素。

      因此,将数组插入操作定义为:

      Array.insert(Element e, Index p)
      

      使用这样的定义,调用者将覆盖数据结构的内部属性并对未排序的数组施加排序约束——该约束在数组的定义中不存在,因为数组是未排序的。

      为什么这种误解发生在数组而不是其他数据结构上?可能是因为程序员习惯于使用赋值运算符来处理数组:

      array[0] = 10
      array[1] = 20
      

      赋值运算符给数组的值一个明确的顺序。这里需要注意的重要一点是 assignmentinsert 不同:

      • 插入 :将给定值存储在数据结构中,而不修改现有元素。
      • insert in unsorted :将给定的值存储在数据结构中,而不修改现有元素,检索顺序并不重要。
      • insert in sorted :将给定值存储在数据结构中,而不修改现有元素,检索顺序很重要。
      • assign a[x] = v :用给定的值 v 覆盖位置 x 中的现有数据。

      未排序的数组没有排序顺序,因此插入不需要允许覆盖位置。 insertassignment 不同。数组 insert 简单定义为:

      Array.insert(v):    
          array.length = array.length + 1
          // in standard algorithmic notation, arrays are defined from 1..n not 0..n-1
          array[array.length] = v
      

      这是O(1)。

      【讨论】:

        【解决方案6】:

        很久以前,在一个内存大于磁盘空间的系统上,我实现了一个索引链表,该链表在手动输入或从磁盘加载时被索引。每条记录都附加到内存中的下一个索引,磁盘文件打开附加到末尾的记录关闭。

        该程序在 I 型 Radio Shack 计算机上进行拍卖销售,并且写入磁盘只是防止电源故障和存档记录的保险,因为为了满足时间限制,必须从 RAM 中获取数据并打印到逆序,这样买家可能会被问到第一个出现的物品是否是他购买的最后一件。每个买家和卖家都与他们出售的最后一件商品相关联,并且该商品与之前的商品相关联。只是自下而上遍历的单链表。

        更正了颠倒的条目。我在几件事上使用了相同的方法,如果该方法适用于手头的工作并且索引已保存到磁盘并且不必在文件重新加载到内存时重新构建,那么我从未找到更快的系统,因为它可能在电源故障。

        后来我写了一个程序来进行更常规的编辑。它还可以重新组织数据,以便将其组合在一起。

        【讨论】: