访问元素 - 真的是O(1)吗?

14

据说,O(1)操作的一个示例是访问数组中的元素。根据一种来源,可以以下列方式定义O(1):

[Big-O of 1]表示算法的执行时间不取决于输入的大小。其执行时间是恒定的。

然而,如果想要访问数组中的一个元素,操作的效率难道不取决于数组中元素的数量吗?例如

int[] arr = new int[1000000];
addElements(arr, 1000000); //A function which adds 1 million arbitrary integers to the array. 

int foo = arr[55]; 

我不明白最后一句话怎么能被描述为O(1)运行; 数组中的1000000个元素难道不会影响操作的运行时间吗?查找元素55肯定比查找元素1要花费更长的时间,对吧?如果有什么区别的话,这看起来像O(n)。
我确信我的推理是有缺陷的,但我只是想弄清楚“如何说它运行在O(1)时间复杂度”?
7个回答

26

数组是一种数据结构,其中对象存储在连续的内存位置上。因此,原则上,如果您知道基本对象的地址,您就能够找到第 i 个对象的地址。

addr(a[i]) = addr(a[0]) + i*size(object)

这使得访问数组的第 i 个元素的时间复杂度为 O(1)。

编辑
从理论上讲,当我们讨论访问数组元素的复杂度时,我们是针对固定的索引 i 进行讨论的。
输入大小为 O(n)
要访问第 i 个元素,则为 addr(a[0]) + i*size(object)。该项与 n 无关,因此它被称为 O(1)。

然而,乘法仍然取决于 i,但不取决于 n。它的时间复杂度是常数级的 O(1)。


大多数架构上,乘法是恒定时间的,与操作数无关。两个w位数的乘法电路深度为O(log w),即使从理论上讲,在机器字中的比特数是恒定的可能存在问题(因为如果w是常数,则数学上2^w也是常数,因此适合机器字的所有大小都是常数),但对于任何给定的CPU,它肯定是固定的,O(log w)的电路深度操作通常会在恒定数量的时钟周期内完成,而不管i - njlarsson

5

一个元素在内存中的地址将是数组的基地址加上元素在数组中的下标乘以元素大小。因此,要访问该元素,您只需访问 memory_location + 55 * sizeof(int)

当然,这假设乘法需要恒定时间,而不考虑输入的大小,如果您非常精确,则可以认为这是不正确的。


1
理论上,你不能假设乘法需要恒定的时间。但在我们的情况下,由于它涉及与输入大小无关的数字,我们将其视为恒定的。 - Rishit Sanmukhani
没错,这更多地是在“访问第ith个索引”的上下文中。 - C.B.
这是最准确的答案。假设内存使用常量位大小表示的数字进行寻址。 - BKE
2
大O符号描述的是极限行为,是一个相当理论的概念,其中内存(磁带)是无限的和线性的 - 因此通用的“访问”操作应该被普遍认为是O(1),在我看来。 - Robert Goldwein

4

要查找一个元素并不是O(1)——但访问数组中的元素与查找元素无关——确切地说,您不与其他元素进行交互,您不需要访问除单个元素之外的任何内容——您只需始终计算地址,无论数组有多大,这就是单个操作——因此是O(1)。


4

对于语句生成的机器码(或者在Java中生成的虚拟机器码)

int foo = arr[55];

本质上是:

  1. 将 arr 的起始内存地址放入 A 中
  2. A 加上 55
  3. 将 A 中的内存地址内容放入 foo 的内存地址中

这三条指令在标准计算机上都只需要 O(1) 的时间。


3

理论上,如其他人已经解释的那样,数组访问是O(1)的。我猜你的问题或多或少是一个理论性的问题。但我还是想带入另一个方面。

实际上,如果数组变得很大,数组访问会变慢。原因有两个:

  • 缓存:数组将无法适应缓存,或者只适应于更高级别(较慢)的缓存。
  • 地址计算:对于大型数组,需要更大的索引数据类型(例如long而不是int)。这将使地址计算变慢,至少在大多数平台上是如此。

1
我点赞了这个答案,因为我认为这种知识越来越重要。然而,我反对“理论上”与“实践中”的二分法。你的解释在理论上也是正确的,只是需要一个不太常见但更精确的理论。 (如果你的理论在任何重要方面与实践不同,那么你正在使用一个不合适的理论。) - njlarsson

1
如果我们说下标操作符(索引)的时间复杂度为O(1),我们是在排除任何其他操作/语句/表达式等的运行时间的情况下做出这个声明的。因此,addElements不会影响操作。
“查找”?哦,不!“查找”意味着相对复杂的搜索操作。我们知道数组的基地址。要确定arr[55]处的值,我们只需将551加到基地址上,并检索该内存位置处的值。这绝对是O(1)。

1 由于在使用C语言时,int数组的每个元素至少占用两个字节,因此这并不完全正确。需要先将55乘以int的大小。


1

数组将数据连续存储,不同于使用引用查找下一个/上一个元素的链表、树或图表等其他数据结构。

对于第一个元素的访问时间为O(1)是直观的。然而,你可能认为第55个元素的访问时间是O(55),这就是你犯错的地方。你知道第一个元素的地址,所以它的访问时间是O(1)。

但是,你也知道第55个元素的地址。它只是1号元素的地址+每个元素的大小*54。

因此,你可以以O(1)的时间访问数组的任何元素,这也是为什么你不能在数组中拥有多种类型的元素,因为那会完全搞乱查找数组第n个元素的地址的数学运算。

因此,对于数组中的任何元素,访问都是O(1),所有元素必须是相同类型的


O(55)和O(1)不是一样的吗? - Frank Puffer
@FrankPuffer:是的,但是当你使用通用的n代替具体的55时,它就变成了O(n)。 - displayName
已接受,但您的答案可能会给人留下有所区别的错误印象。 - Frank Puffer
@FrankPuffer:我认为不应该。我是从问题中获取的示例。任何完全阅读问题的人都不会感到困惑。此外,这个评论线程将消除任何进一步的疑虑。 - displayName

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接