【问题标题】:LinkedList vs Stack链表与堆栈
【发布时间】:2014-11-13 08:33:53
【问题描述】:

在java中,你可以使用LinkedList实现的栈。也就是说,你可以使用linkedlist来实现栈的所有功能。从这个意义上说,为什么我们仍然需要堆栈类,为什么我们不坚持使用链表来保持简单? 谢谢

【问题讨论】:

  • 这有点像在说如果我们可以使用 Arrays,为什么我们需要 ArrayList。
  • 大多数时候,Stack 是使用 LinkedList 实现的。
  • 既然我们可以为了简单而坚持使用房屋,为什么还需要使用砖块?
  • 我认为Stack 的使用更清楚地传达了意图,并与应用程序的其余部分就合适的操作形成了强有力的契约。即使使用现代的Deque 接口和LinkedList 实现,如果我需要强制执行LIFO,我仍然会实现一个具体的Stack 类。

标签: java linked-list stack


【解决方案1】:

首先,在Stack的文档介绍中说:

Deque 接口及其实现提供了一组更完整和一致的 LIFO 堆栈操作,应优先使用此类。

这告诉我们Stack 类主要是一个剩余部分,随着更新的Java 集合框架,它或多或少变得多余。

其次,“提供相同的功能”并不是衡量一个类是否应该存在的好方法。虽然LinkedList 提供了创建堆栈所需的所有操作,但它的性能会很差。链表适用于在随机位置插入和删除元素。在堆栈中,我们只在末尾追加或删除,这使得 ArrayList 对实现堆栈更具吸引力。

此时,我们意识到ArrayListLinkdList 都提供了List 的功能,这让我们更接近良好的面向对象设计的核心:

  • 我们在一个接口中定义了一组操作(例如List)。
  • 我们提供该接口的一个或多个实现,它们必须都提供所需的功能,但可以针对特定用例(例如ArrayListLinkedList)进行优化。
  • 如果某个使用模式出现得特别频繁,我们可能会决定添加另一个在其名称中引用此模式的类,这将使代码结构更好。例如,我们可以通过简单地委托ArrayList来实现Stack,但提供一个在其名称中清楚地说明其含义并且不提供操作的类型(如随机访问)可能违反堆栈的概念。 (这不是 java.util.Stack 所做的,这让我们回到了文档中的引用。)

请注意,List 和较新的Deque 接口之间的继承关系比ListStack 之间的继承关系更加一致。 LinkedList 实现了 Deque,这是正确的,因为 Deque 要求可以从 / 到开头或结尾添加和删除元素,而 LinkedList 通过在随机位置提供插入和删除来满足此要求。另一方面,Stack 实现了 List,这应该被认为是有问题的。


后期更新:因为我对 “链接列表有利于在随机位置插入和删除元素的说法表示反对。在堆栈中,我们只在末尾追加或删除,这使得 ArrayList 对实现堆栈更具吸引力。”,我想对此进行扩展。

链表允许在恒定时间内在任意位置插入和删除元素。另一方面,在给定索引的情况下,找到一个元素需要线性时间。当我说它们适合在随机位置插入和删除时,我的意思是由迭代器给出的位置,而不是索引。如果给定索引,则插入和删除对于链表和数组都是线性时间操作,但链表的常数因子会高得多

基于数组的列表允许在末尾进行分摊的常量时间插入和删除,以及按索引进行常量时间访问。在随机位置添加和删除元素是线性时间操作,无论位置是由索引还是由迭代器(基本上只是数组的索引)给出。

在堆栈实现中,链表的唯一优点——它能够在恒定时间内在任意位置(由迭代器给出)插入和删除元素——是不需要的。另一方面,它的内存开销要高得多,而且它的内存访问也远不如连续数组。鉴于在列表末尾添加和删除项目的渐近复杂度在任何一种情况下都是摊销常数,因此基于数组的列表是实现堆栈存储的更好选择。

更好的数据结构是可变数量的固定大小的缓冲区,通过指针链接在一起。这样的数据结构通常用于实现所谓的双端队列。它提供了数组的大多数优点,并且几乎没有额外的开销,并且从末尾(或开头)添加和删除不仅是摊销的,而且始终是恒定时间的操作。

【讨论】:

  • “链表适用于在随机位置插入和删除元素。在堆栈中,我们只从末尾追加或删除,这使得 ArrayList 对实现堆栈更具吸引力。”这是废话,LinkedList适合添加/删除第一个/最后一个元素,而ArrayList适合随机访问。
  • 我认为 ArrayList 和 LinkedList 之间的区别在于,添加到 ArrayList 对象可能会达到该集合的容量,然后下一次尝试添加 smth 会导致在正在保存哪些数据。在 LinkedList 中,这种情况永远不会发生,这使得当我们不知道集合的最终大小并且我们不需要通过索引快速访问时,它更加安全和有效。
【解决方案2】:

Stack 类扩展了Vector,因此它是同步的。 LinkedList 类不是。最佳选择取决于您要执行的操作和约束,但一般来说,同步类的开销要多一些。此外,正如@NESPowerGlove 提到的,Deque 及其实现优于Stack

【讨论】:

    【解决方案3】:

    这并不是一个真正的 Java 问题,而是一个通用的编程问题:为什么要使用抽象?

    堆栈是一种数据结构,具有两个明确定义的操作:push 和 pop。 我们可以用不同的方式来实现堆栈。我们可以使用数组、链表或其他结构。

    堆栈类将是链表或数组之上的一个非常薄的层,因此问题出现了:为什么要麻烦定义一个单独的类?

    有几个原因基本上是我们在一般编程中使用抽象的原因:

    1. 可读性- 如果您拥有的是一个堆栈,那么如果您实际使用堆栈对象及其显式推送和弹出操作,而不仅仅是从列表中添加和删除,那么任何正在阅读您的代码的人(包括您未来的自己)都会更清楚。

    2. 数据结构与实现- 堆栈是由其操作定义的数据结构。链表只是一种可能的实现。定义堆栈允许您在将来更改实现,而无需更改使用堆栈的任何其他位置的代码。 为了清楚起见,假设您已经使用链表实现了一个堆栈,但过了一段时间您改变了主意,您决定将实现更改为使用数组。您所要做的就是更改堆栈类的实现,而无需更改您实际使用堆栈的任何其他位置的代码。如果您只是使用链表,您将不得不检查您的代码并随处更改它。

    3. 类型检查- Java 像许多其他语言一样检查函数输入和输出的类型,从而帮助我们捕捉错误。因此定义不同的类型可以帮助编写更好的代码并帮助编译器捕获错误。

    4. 添加独特的功能- 我说过堆栈将是链表上的一个薄层,但它仍然可以包含一些链表没有的功能。 也许当您打印堆栈时,您想从上到下进行,而在链表中则相反。通过定义一个单独的类,您可以更改或添加这个所需的功能。

    5. 限制功能- 你可以对列表做很多事情,而你不能用堆栈做这些事情。主要区别在于,在列表中,您可以从任何位置添加或删除值,而在堆栈中,您只能从顶部添加和删除。 如果您使用链表来模拟堆栈,您可能会在不知不觉中做错事,如果您定义了一个限制您只能推送和弹出的堆栈类,这是不可能的。

    【讨论】:

      【解决方案4】:

      当您可以使用范围更窄或更正确的数据结构时使用一种数据结构是有问题的,原因有两个:

        1234563通常,您可能需要考虑 LinkedList 与 ArrayList,相同的操作,但 LinkedList 更适合,因为 insert(n) 操作针对其他操作/功能进行了优化)。
      1. 代码的未来读者将会对为什么使用不太适合的数据结构感到困惑,因为它是一种代码味道。读者会花更多时间阅读您的代码,甚至可能会替换它,即使当前使用的操作在切换到更合适的数据结构时不再优化。

      顺便说一句,Java 中更现代的堆栈实现是 java.util.Deque。

      【讨论】:

        猜你喜欢
        • 2014-06-23
        • 1970-01-01
        • 2016-03-08
        • 2015-11-16
        • 2012-10-25
        • 2013-03-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多