堆栈ADT(抽象数据类型)实现 - 数组与链接

时间:2015-11-18 12:53:09

标签: java arrays collections linked-list stack

基于数组与链接实现Stack的优缺点是什么?从我有限的知识,我觉得链接将总是是一个更好的方式来实现Stack,因为:

1)不需要随机访问。

2)数组是低效的,因为它们必须调整大小(浪费时间)并且它们使用存储效率低下(某些空间总是被浪费)

我确定我在这里遗漏了一些东西,因为:

1)java.util.Stack是基于数组实现的(它是java.util.Vector的子类,它是创建java集合接口之前的遗留类,实际上类似于ArrayList)。所以java的创建者选择了基于数组的实现。

2)我已经在stackoverflow上读到了一个答案here“另一方面,基于数组的实现可能具有更好的运行时行为”。这是什么意思,虽然我不知道。

我正在寻找的比较应该包括以下参数:

1)理论时间和储存要求。

2)运行时性能(如果与理论比较不同)。

由于我缺乏知识,请包括我未提及的任何其他重要参数。我使用java,如果在结论上完全没有任何差异。

PS-我无法在本网站上的任何其他答案中找到此问题中提出的所有要点,所以请仅将此问题标记为重复,以防我的所有问题在其他问题中得到正确回答并且足够详细。< / p>

PPS-我知道这是一个很长的问题所以TIA为你付出的努力:)同样如果你觉得它太宽泛,那么在你将它标记为“过于宽泛”之前请如何分解,以便我可以编辑它根据需要。

1 个答案:

答案 0 :(得分:2)

首先,您应该知道java.util.Stack是一个&#34;遗留系列&#34;这可以追溯到Java 1.0。它扩展了java.util.Vector,这确实是基于数组的。但是,这通常被视为不良对象设计。这并不意味着基于数组的堆栈是一件坏事,但你应该知道,因为JDK中的某些东西并不意味着它是一个好主意。对于较旧的API尤其如此。

更现代的类似堆栈的数据结构是java.util.ArrayDeque。它也是基于数组的。它有许多其他功能,但如果您坚持使用pushpop方法(相当于addFirstremoveFirst),它基本上就是一个堆栈。请注意,在其文档中说,

  

当用作堆栈时,此类可能比Stack更快。

如果你看一下这些实现,Stack就像Vector一样,是同步的,这样可以减慢它的速度。 Stack pushpop方法是根据Vector方法实现的,这些方法也是同步的。这意味着其他方法调用加上嵌套同步。 (尽管如此,JIT可能会优化掉大部分内容。)相比之下,ArrayDeque不同步,其类似堆栈的方法使用直接在其内部数组上运行的简单操作。请注意,我尚未在此处进行任何基准测试以验证文档的声明。

在任何情况下,ArrayDeque都是首选的Java集合,用于需要类似堆栈行为的问题。

但是你问的是链接数据结构而不是基于数组的数据结构。让我们将ArrayDeque与另一个Java链接数据结构LinkedList进行比较。这实现了Deque,因此它也可以用作堆栈。你说,

  

1)不需要随机访问。

真。请注意ArrayDeque并不提供随机访问,即使它是基于数组的。两者都没有优势。

  

2)数组效率低,因为它们必须调整大小(浪费时间),并且它们使用存储效率低下(某些空间总是浪费)

任何数据结构都会有一些低效率。但是,不同的数据结构将有不同的权衡。如果ArrayDeque的数组的大小不适合堆栈的典型容量,则必须调整其大小。但是一旦阵列足够大,它就不需要不断调整大小。如果堆栈缩小,则阵列仍将占用空的空间。这可能被视为浪费,或者可以将其视为保留内存,以便在堆栈再次增长时不必调整大小和复制。

将情况与LinkedList进行比较。在内部,每个列表元素都需要一个Node实例。 (请参阅源here。)每个实例包含三个引用:一个引用数据元素,一个引用到下一个Node,一个引用到前一个Node。假设一个带有压缩指针的64位JVM,每个引用的32位。每个对象都有一个包含64位标记字和32位类指针的标头。这给出了总共六个32位字,或每个列表元素24个字节。六个单词中只有一个是&#34;有效载荷&#34; - 元素本身 - 每个元素的20个字节或83%的开销

相比之下,数组中的每个附加元素仅消耗用于引用该元素的空间,或32位。

例如,在LinkedList中存储1,000个元素会消耗大约24K的内存,但是将它们存储在ArrayDeque中仅消耗大约4K的内存。即使数组是保存1,000个元素所需的两倍大,也只有8K - 仍然只是LinkedList的一小部分。

  

&#34;另一方面,基于数组的实现可能具有更好的运行时行为&#34;

这可能是指遍历链表比遍历数组慢。有两个原因。首先,链接节点占用更多内存,因此必须读取更多内存才能获得相同的信息。每个节点24个字节,2.67个节点可以适合典型的64字节高速缓存行。其次,链接节点可能在某种程度上随机地在存储器周围扩散,因此平均而言,每个高速缓存行的节点可能少于此。结果是,遍历链表将导致大量缓存未命中,因为这种不良的引用位置。

另一方面,由于数组中的引用密集而没有开销,因此16个数组元素可以容纳在单个64字节高速缓存行中。遍历数组将导致更少的缓存未命中。此外,内存子系统针对顺序访问进行了优化,因此它们可能能够预取下一个缓存行,从而进一步降低内存开销。

考虑到内存消耗和内存访问的性能成本,基于阵列的结构通常更可取。在某些情况下,链接结构的表现会更好,但似乎比大多数人认为的要少。

除了设置性能之外,对于堆栈的数组结构,链接结构有一个明显的优势:不可变持久性。在基于阵列的堆栈上推送和弹出元素本身会改变数组,因此以前的版本不再存在。在链接结构上推送和弹出节点并不需要改变任何链接节点,因此对堆栈的过去状态的引用可以持久保持并且将保持不变,即使其他人在此堆栈上推送和弹出元素也是如此。实际上,它们是不同的堆栈,共享公共部分。这在函数式编程上下文中非常有用。