Java 关于数组时间复杂度的问题“;“调整大小”;在爪哇

Java 关于数组时间复杂度的问题“;“调整大小”;在爪哇,java,arrays,memory,time-complexity,Java,Arrays,Memory,Time Complexity,注意:正如标题已经暗示的,这个问题不是关于基于数组的列表的特定java.util.ArrayList实现,而是关于原始数组本身,以及它们在“纯”(即完全未优化的)基于数组的列表实现中的行为。我之所以选择提及java.util.ArrayList,是因为它是java中最突出的基于数组的列表示例,尽管它在技术上并不“纯粹”,因为它利用预分配来减少add()的操作时间。如果您想知道为什么我问这个特定的问题而对java.util.ArrayList()preallocation优化不感兴趣,我在下面添加

注意:正如标题已经暗示的,这个问题不是关于基于数组的列表的特定
java.util.ArrayList
实现,而是关于原始数组本身,以及它们在“纯”(即完全未优化的)基于数组的列表实现中的行为。我之所以选择提及
java.util.ArrayList
,是因为它是java中最突出的基于数组的列表示例,尽管它在技术上并不“纯粹”,因为它利用预分配来减少
add()
的操作时间。如果您想知道为什么我问这个特定的问题而对
java.util.ArrayList()
preallocation优化不感兴趣,我在下面添加了一点对我的用例的解释

众所周知,您可以访问基于数组的列表中的元素(如Java的
ArrayList
),时间复杂度为
O(1)
,而向该列表中添加元素需要
O(n)
。对于链表,情况正好相反(对于双链表,您可以将访问优化到执行时间的一半)

将元素添加到基于数组的列表需要
O(n)
的原因是数组不能简单地调整大小,必须重新分配和填充。最简单的方法是:

String arr[] = new String[n];
//...
String newElem = "foo";
String[] newArr = new String[n + 1];
int i = 0;
for (String elem : arr) {
    newArr[i] = arr[i++];
}
newArr[i] = newElem;
arr = newArr;
由于for循环,时间复杂度
O(n)
清晰可见。但是在Java中还有其他复制数组的方法,例如
System.arraycopy()

按照常规的for循环解决方案,即使缩小数组也需要
O(n)
,因为数组的大小是固定的,为了“缩小”它,您必须将要保留的所有元素复制到一个新的更小的数组中

下面是我关于这些数组操作及其时间复杂性的问题:

  • 虽然vanilla for循环总是需要
    O(n)
    ,但是如果内存中有足够的空间来展开数组,那么
    System.arraycopy()
    是否可以优化“add”操作,这意味着它会将原始数组保留在原来的位置,只在其末尾添加新元素

  • 由于收缩操作在理论上总是可以使用
    O(1)
    执行,因此
    System.arraycopy()
    是否总是将此操作优化为
    O(1)

  • 如果
    System.arraycopy()
    无法使用这些优化,那么Java中是否有其他方法可以实际使用这些理论上可行的优化,或者无论在何种情况下,数组“调整大小”总是需要
    O(n)

  • TL;DR是否存在Java中数组“调整大小”所需的时间小于
    O(n)
    的情况

    其他信息:

    我使用的是openJDK11(最新版本),但是如果答案是依赖于JVM的,那么我想知道与之相比,其他JVM的表现如何

    为了好奇的人 谁想知道我想对这些信息做什么:

    我正在研究一个新的
    java.util.List
    实现,即一个混合列表,它可以将数据存储在数组和链接缓冲区中。在某些情况下,缓冲区将被刷新到数组中,这当然需要调整现有数组的大小。但是除了这个想法之外,我还想在阵列部分使用尽可能多的其他优化。为了避免数组通常的大小调整,我尝试了让数组保持恒定大小的想法,但是用一些其他字段来管理它的“有效”范围。这意味着,如果要弹出数组的最后一个元素,它不会缩小数组,而是缩小有效元素的范围。然后,在数组部分插入新元素时,可以使用以前的无效部分将值转移到中,基本上重用了现在已删除的元素以前使用的空间。如果插入操作超过了实际数组大小,元素仍然可以传输到链接缓冲区以避免调整大小。为了进一步优化这一点,我选择在删除某些元素时使用数组的中间部分作为轴心。现在,有效范围可能不再从数组的开头开始。基本上,这意味着如果删除轴左侧的元素,则有效范围起点和删除元素之间的所有元素都将移向轴右侧。移除枢轴右侧的图元会相应地起作用。因此,经过一些删除后,阵列可能会如下所示:

    [null null|elem0 elem1 elem2||elem3 elem4 elem5|null null null]
    
    (其中开始和结束处的|标记有效范围,|标记轴)

    那么,这与我的问题有什么关系呢

    所有这些优化都建立在阵列大小调整在时间上非常昂贵的说法之上,即
    O(n)
    。因此,尽可能避免调整阵列大小。这些优化听起来很整洁,但实现它们的代码可能会变得非常混乱,尤其是在实现批处理操作时(
    addAll()
    removeAll()
    retainal()
    )。因此,如果事实证明数组大小调整操作本身在某些情况下(特别是收缩)的成本较低,我将减少许多优化,这些优化随后变得无用,从而使代码在过程中变得更容易


    因此,在坚持我的优化想法和实验之前,我想知道是否需要它们。

    这些问题似乎非常特定于JVM供应商。至少可以说Javadoc(甚至在SE 14中)没有提到这一点。感谢这些信息,我正在使用openjdk 11,但我会将这些信息添加到我的问题
    系统中。arraycopy使原始数组保持工作状态,因此无法在O(1)中实现除非使用奇怪的内存映射技术,否则肯定会产生意外的性能结果