递归使用多少堆栈(从列表中删除节点)? - java

更新:请注意,这个问题并没有多大意义,因为调用栈将独立于类型T增长。调用栈仅取决于要处理的列表中节点的数量-这些节点中包含的数据无效在调用堆栈的大小上。

假设我有一个“ remove-node-from-list”方法的递归java实现,如下所示:

// head
public void remove(T data) {
    if (data == null)
        throw new IllegalArgumentException();
    head = remove(head, data);
}

// other nodes of list
private ListNode remove(ListNode current, T data) {
    if (current == null)
        return null;
    if (current.data.compareTo(data) == 0)
        return current.next;
    current.next = remove(current.next, data);
    return current;
} 

让我们进一步假设类型Tint

我的问题是:与我正在处理的列表大小相比,调用栈将变大(在最坏的情况下)?

到目前为止,我一直在想:
从我看到的Java 8不能优化递归(对吗?我的整个分析都假设这是正确的)。因此,我们将拥有一个随列表大小线性增长的调用栈。

更准确地说:对于我列表中的每个节点,此方法remove(ListNode current, T data)基本上会将一个节点引用(32位拱上的32位)和一个int(也是32位)放在调用堆栈上。

现在,由于列表中的一个节点还包含一个引用和一个int,因此一个节点实际上具有与那些递归调用之一几乎相同的大小。实际上,这些调用之一甚至可能占用更多空间(返回地址也必须推入堆栈,对吗?或者可以通过某种方式对其进行优化吗?)。

因此,如果我是对的,那么在最坏的情况下(当要删除的元素是列表中的最后一个元素时),调用栈基本上会和列表本身一样大!真的是这样吗?如果我是正确的,那么对于处理int列表所需的调用栈,这将意味着1甚至更大!结果,当处理大型列表(实际上甚至不是那个大列表)时,如果1M使用默认的stacksize设置(0.5M)运行该列表,则会出现stackoverflow。

我知道以下事实:对于较大的数据类型,该因素会减少,但是无论如何我仍然坚持的基本原则是:如果没有进行任何优化,则调用栈将随着列表的大小-因此,应该优先选择迭代而不是递归过程。

总的来说,我会说,调用堆栈将随着因子(size_of_all_arguments_in_call + size_of_return_address)/size_of_a_single_listelement线性增长,在int列表的情况下几乎是相同的,对吗?请不要误解size_of_all_arguments:我知道我们只将引用的值传递给那些调用中的对象,而不是整个对象本身。因此,对于大型物体,该因素可能不会那么引人注目,但我仍然要说,不应接受不必要的线性空间开销。但是,对于int的列表,该因子约为。 1。

这种分析是正确的还是我缺少什么?

背景:我正在与之讨论的人试图让我相信Java中的调用堆栈不会增长得如此之快,并指出我缺少明显的东西(不幸的是,我无法告诉我什么,而对我而言,这实际上一点都不明显) )-因为我不知道那是什么,所以我很想学习;)

有关:

Recursive vs. Iterative algorithms
Recursion or iteration?
Is recursion ever faster than looping?

对我来说,相关问题中缺少的主要内容是调用栈实际使用多少空间

参考方案

递归使用多少堆栈(从列表中删除节点)?

如果不对代码进行概要分析或对特定JVM进行详细说明,这是一个很难回答的问题。这是因为Java Virtual Machine Specification (JVMS)给实现者留下了许多细节。

JVMS Chapter 2:实施细节,不属于
Java虚拟机的规范将不必要地限制
实现者的创造力。例如,运行时的内存布局
数据区域,使用的垃圾收集算法以及任何内部
Java虚拟机指令的优化(例如,
将其翻译成机器代码)由
实现者。

这些细节的一部分是堆栈和堆栈框架的实现。而且由于您似乎实际上是在问递归删除所创建的堆栈与算法正在处理的列表之间的关系,因此我们必须考虑到堆栈帧甚至可能不如您想像的那样大(或提出您的问题)。

JVMS (§2.5.2):因为Java虚拟机堆栈从不
直接操作,除了推和弹出框架外,框架可能是
堆已分配。

这意味着对于每个堆栈帧,仅可以在堆栈上放置一个引用。如果是这样,那么对于您提供的示例来说,这将使堆栈与要处理的列表之间的关系确实微不足道。让我们看一些数字:

根据Algorithms (Sedgewick, Wayne):

Integer是24个字节
开销+实例变量(int)+填充
= 16 + 4 + 4 = 24个字节
嵌套在关联对象中的非静态递归定义的Node
List是40个字节
开销+引用+额外开销(引用List类)
= 16 + 8 + 8 + 8 = 40字节

因此,我们可以看到列表中每个条目有64个字节。因此,如果有一个实现是在堆中分配帧,则堆栈与要处理的列表之间的关系将为8/64 = 1/8 bytes

当在堆栈上分配堆栈帧时,确定堆栈的大小更加困难。实际上,我们需要更多的实现细节来确定它。

JVMS (§2.6. Frames):
局部变量数组和操作数堆栈的大小为
在编译时确定,并与代码一起提供
与框架关联的方法(第4.7.3节)。因此,
框架数据结构仅取决于Java的实现
虚拟机,并且可以分配这些结构的内存
同时调用方法。

即使这样,我还是要说,以您的示例为例,堆栈相对于列表的增长不可能达到或接近1:1(在这里谈论内存的总大小)。当然,您可以提出一个具体的问题来验证您的论点,但可能很琐碎,而且可能与您提供的示例不符。

为什么我不能在Java中重写方法wait()? - java

Improve this question 我在类wait()中找到了方法Object。这是最终的,这意味着该方法不能被覆盖。有什么想法为什么是最终的? 参考方案 @Flavio-这实际上是一个很好的问题。当然,您不能覆盖它的原因是设计师将其“确定为最终”。做出此决定的一些潜在原因:您不希望人们弄乱基本类(“对象”类)上基本操作的语义。由于它是“最终的”,因…

Web应用程序上的恶意用户是否可以操纵Web应用程序前端发送的输入(在表单数据旁边)? - java

Web应用程序上的恶意用户是否可以通过任何可能的方式来操纵Web应用程序前端发送的输入(当然,这不是在谈论FORM DATA),但是发送的请求例如当我允许他编辑他的个人资料或他的内容时,他可能会操纵ID(userId或contentId),从而可能恶意地对其他用户的内容进行邪恶?这些输入固定在网页上并且不可编辑,但用户仍然可以操纵它们吗?用户是否可能以这种方…

什么时候在Hibernate中调用flush()和commit()? - java

我有以下情况: openSession() tx = session.beginTransaction(); try { ... session.saveOrUpdate(obj_1); ... session.saveOrUpdate(obj_2); ... session.saveOrUpdate(obj_3); session.flush(); tx.…

如果没有它,为什么还要使用collect(Collectors.toList())? - java

我在许多Java 8参考资料和示例中都看到了以下代码:List <Integer> l = Arrays.asList(7, 3, 9, 8, 6, 5, -1, -100); l.stream().filter(y -> y <l.get(0)).collect(Collectors.toList()). forEach(Syste…

Java-固定大小的列表与指定初始容量的列表之间的差异 - java

我在理解这一点上遇到了问题。当我们做 List<Integer> list = Arrays.asList(array); 我们不能在该列表上使用添加,删除之类的方法。我知道Arrays.asList()返回固定大小的列表。我不明白的是,如果我们创建一个具有指定初始容量的列表,例如List<Integer> list2 = new A…