【问题标题】:Designing a data structure acts like improved stack设计数据结构就像改进堆栈
【发布时间】:2014-06-11 00:21:51
【问题描述】:

我被要求设计一个数据结构,它的作用就像一个堆栈,大小不受限制,它将支持以下方法,并具有给定的运行时限制。

push(s) - 将 s 推送到数据结构 - O(1)
pop() - 移除并返回最后插入的元素 O(1)
middle() - 按插入顺序返回索引为 n/2 的元素(不删除),其中 n 是当前数量数据结构中的元素。 - O(1)
peekAt(k) - 按插入顺序返回第k个元素(栈底为k=1) - O (log(k))

我曾想过使用链表,并始终保留一个指向中间元素的指针,但后来我在实现 peekAt(k) 时遇到了问题。任何想法我该如何实现?

【问题讨论】:

  • @Codor "我想用链表" ... ?

标签: algorithm data-structures stack runtime time-complexity


【解决方案1】:

如果 O(1) 限制可以放宽到摊销 O(1),典型的可变长度数组实现就可以了。当您为当前长度为 N 的数组分配空间时,请在最后保留 N 个额外空间。一旦超出此边界,请按照相同的策略重新分配新的大小,将旧内容复制到那里并释放旧内存。当然,您必须同时维护堆栈的分配长度和实际长度。 middlepeekAt 操作可以在 O(1) 中轻松完成。

相反,如果需要,如果它占用的空间小于分配空间的 1/4,您也可以缩小数组。

所有操作都将摊销 O(1)。确切的意思是,对于任何 K 个堆栈操作,从一开始,您就必须总共执行 O(K) 条指令。特别是,N次推送后的重新分配次数将是O(log(N)),并且由于重新分配而复制的元素总数将不超过1 + 2 + 4 + 8 ... + N


如果内存管理器的allocatefree 对于任何大小都在 O(1) 中执行,这可以渐进地更好地完成,每个操作都需要非摊销 O(1)。基本思想是维护当前分配的栈和2x大的未来栈,并提前开始准备更大的副本。每次将一个值压入当前堆栈时,再将两个元素复制到未来堆栈中。当前堆栈已满时,其所有元素都将已复制到未来堆栈中。之后,丢弃当前堆栈,声明未来堆栈现在是当前堆栈,并分配一个新的未来堆栈(当前为空,但分配比当前堆栈大 2 倍)。

如果您还需要收缩,当您的堆栈占用分配空间的 1/2 到 1/4 之间时,您可以以类似的方式维护一个较小的副本。

从描述中可以看出,虽然这在理论上可能更好,但通常速度较慢,因为它必须维护堆栈的两个副本而不是一个。但是,如果您对每个操作都有严格的实时 O(1) 要求,这种方法可能会很有用。

【讨论】:

  • 或者不是重新分配整个数组,而是维护一个堆栈数组。也就是说,您分配了一个大小为 X 的堆栈。当它填满时,您分配另一个大小为 X 的堆栈并将另一个项目添加到您的数组中。当有人要项目 i 时,您可以将 i 除以 X(块大小),索引到您的引用数组,然后定位块。然后一个简单的模运算将为您提供块中的偏移量。当你增长时,你仍然在调整数组的大小,但你只移动了几个指针,而且很少移动。比复制整个堆栈内容要好。
  • @JimMischel:在这里,您仍然需要一个动态增长的数据结构,这一次是为了维护指向堆栈的指针数组。本质上,您只是将这个动态结构的大小除以 X,代价是在每个操作上取消引用一两个指针。
【解决方案2】:

使用双向链表的实现对我来说很有意义。 PushPop 将像通常对堆栈一样实现;对“中间”元素的访问将通过附加参考来完成,该参考将在PushPop 上更新,具体取决于所包含元素的数量是否会从偶数变为奇数,反之亦然。 peekAt 操作可以使用二分查找来完成。

【讨论】:

  • 在典型的链表中,二分查找无法跳转到 O(1) 中的任意索引。
  • 指向中心元素的指针将在 PushPop 上更新。
  • 中心,是的。但是当你从 [0, 1000] 段继续到 [500, 1000] 时,你如何在 O(1) 中找到第 750 个元素?
  • @Gassa 感谢您的评论,我犯了一个错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-29
  • 2018-06-02
  • 2020-07-08
  • 2023-03-22
  • 2018-04-27
  • 2015-10-12
  • 2015-10-21
相关资源
最近更新 更多