使用 push(x)、pop() 和 pop_max() 方法实现队列
Posted
技术标签:
【中文标题】使用 push(x)、pop() 和 pop_max() 方法实现队列【英文标题】:Implement a queue with push(x), pop() and a pop_max() method 【发布时间】:2021-12-06 09:58:06 【问题描述】:我遇到了这样一个问题:用 push(x)、pop() 和 pop_max() 方法实现一个队列。
pop_max() 函数应该按照 FIFO 规则弹出最大的元素。
例如: pop_max()之前:front-> 2,3,4,5,1,5
pop_max() 之后:front-> 2,3,4,1,5
以下是我的一些尝试。
用基本队列实现它,使用支持队列通过 O(n) 扫描找到最大元素。
pop()/push() 是 O(1),pop_max() 应该是 O(n)。
用双链表和单调栈实现它。
pop()/pop_max() 是 O(1),push() 应该是 O(n)。
有人知道以最小的时间复杂度做到这一点的方法是什么?我看过这个Implement a queue in which push_rear(), pop_front() and get_min() are all constant time operations,它提供的方法似乎不适合这个场景。
【问题讨论】:
对于你的(1.),“基本队列”仍然需要实现;您可以使用单链表来实现它,并且 pop_max 操作可以“作弊”,因为它不尊重队列约束。 您可以实现一个带有双链表的队列,以及一个在链表中存储节点的最大堆和一个跟踪当前队列中值频率的计数器。对于重复的元素,不同元素的推送将是对数且恒定的,而 pop 将是恒定的。 pop_max 将按对数摊销,但在某些情况下,需要许多 pop_max(与推送次数成线性关系)来同步堆和计数器。 @wLui155 我明白了你的意思,但是 pop 怎么可能保持不变呢?因为您也应该更新最大堆。 @sugarfree 它不能。您可以使用您尝试实现的抽象数据类型通过推送整个列表然后重复弹出最大化来进行排序,因此具有通用可比较元素的 O(1) 是不可能的。 pop 将删除链表中最早的元素并减少计数器中删除的值。因为它不对堆做任何修改并且由两个恒定时间操作组成,所以它也是恒定的。同时,权衡是使堆赶上队列的当前状态可能会有点慢(当在许多正常的 push/pop 操作之后调用 pop_max 时)。 【参考方案1】:根据要求,这里有一个关于我为什么相信的详细答案 有一个最坏情况 O(1) 推送和弹出以及 O(log n) 的解决方案 流行音乐。 太复杂了,你不需要理解 它用于采访。真的。我写这个答案主要是为了娱乐 [算法] 标签正则。
变量
n 是当前结构中的元素个数,p 是 结构生命周期内的推动次数。显然 n ≤ p,并且在 一般来说,log p 不是 O(log n)。
比赛树
主要构建块是锦标赛树。锦标赛树是 带有标签的完整二叉树(每个节点都有零个或两个子节点) 节点,使得每个具有两个子节点标记为 x 和 y 的节点都被标记 最大值(x,y)。从语义上讲,这个数据结构的内容是 具有零个子节点(叶子)的节点的标签。如果你感到困惑,请查看 单淘汰锦标赛的完整括号。
锦标赛树的有用之处在于我们可以对叶子进行排序 任何我们想要的方式。对于这个问题,我们需要队列顺序。根 元素给出整体最大标签。找到最左边的叶子 标签,如果标签相同,则重复下降到左孩子 当前节点,否则为右节点。要软删除叶子,请将其设置为 值为 -∞ 并将其祖先从父更新为根。
摊销 O(1) 推送和最坏情况 O(log p) 弹出最大值
在实践中有更好的方法来实现这一点,但我们的目标是 是为了展示想法。
我们保留一个 O(log p) 锦标赛树的链表。串联起来,他们的 叶子代表队列。每棵树都是一棵完全二叉树 2k 个叶子(计数中包括软删除的元素) 对于某个整数 k ≥ 0。
推送操作类似于将二进制数加一 表示。我们将新元素单独放入锦标赛树中 并将该树附加到列表中。而列表中的最后两棵树 具有相同的大小,通过制作第二个将它们组合成一棵树 最后一个新树的左孩子和最后一个右孩子。
pop-max 操作扫描树根以找到整体最大值,然后 软删除最左边的匹配项。
最坏情况 O(1) 推送
我们可以更懒惰地合并树木。而不是完成合并 立即循环,我们保留一个延续队列。每一个延续 可以表示为指向列表中树的可变指针。踏步 它,我们将树的大小与其左邻居的大小进行比较;如果 它们是相同的,然后合并树并更新指向 合并树。否则,继续完成。
push 操作附加一个单例树,附加一个延续 指向那棵树到队列的后面,然后继续 在前面工作几步。任何时候都会有 O(log p) 合并要继续,所以 pop-max 仍然运行得足够快。 (这来自摊销分析。)
定期流行音乐
我们可以在最坏情况时间 O(log p) 中实现弹出操作 向锦标赛树添加双向链表结构不会 尚未删除。比赛使用软删除;此列表使用硬 删除。
显然,我们希望 pop 在恒定时间内运行。我们可以得到常数 摊销时间,通过分割最左边的锦标赛树直到它 软删除之前的一个元素(带有某种障碍以确保 之前的合并延续不理会这个前缀)。
通过像我们这样的更多调度应该可以实现最坏情况下的恒定时间 为推动而做。
最坏情况 O(log n) pop-maxes
别介意挥手,此时基本上是我的整个手臂。我们的 策略是通过定期将 p 的有效值限制为 O(n) 在后台重建整个结构。这意味着发出 pop 重建操作并记住我们在重建中的距离 这样我们就可以在需要时发出 pop-maxes。假设我们做多个 每次操作都会推动重建,我们将在弹出之前完成 pop-maxes 可以将元素计数减少超过一个常数 分数。
开放式问题
我确信有一种更简洁的方法可以完成这一切。这是什么?
【讨论】:
@kcsquared 我们必须摊销推,而不是树。树的大小变得双调(先增大后减小)。真的,我们应该保留两个列表,以便延续知道在哪里停止。 @kcsquared 如果您想从消防软管中喝水,Kaplan--Tarjan(“具有串联的纯功能、实时双端队列”)就是这些论点在已发布的详细信息中的样子。 这比我从这个问题中预期的要有趣得多。如果要进行增量重建,则不需要锦标赛树。如果您使用其最大子节点注释每个节点,则任何基于递归减速(IIRC 这是正确的术语)的 O(1) 功能队列都将允许您在 O(log n) 时间内使用软删除执行 pop_max。 这太棒了——我将不得不更多地考虑 pop() 之后左端的树分裂行为,因为这似乎很重要。我发现的最相关的论文是 amortized cost of find-min 上的这篇论文,它表明如果插入和 任意 删除可以在恒定时间内完成,那么即使 find-min(或 find-max)也不能在少于线性的时间内完成。您链接的 Tarjan 的论文提到仅将他们的想法扩展为 find-min。我相信这篇文章可能是这种数据结构的第一篇文章。 @kcsquared 绑定一个松散的一端:O(log n) push、O(1) pop 和 pop-max 可以通过交叉引用队列和 Levcopoulos and Overmars tree 来处理。跨度> 【参考方案2】:首先让我们争论一个目标运行时间。我们可以使用这种抽象数据类型对一个 n 元素列表进行排序,其中 n 个 pushes 后跟 n 个 pop-maxes。假设通用可比较元素,由于可能的最快比较排序是 Θ(n log n),因此最坏情况的 push/pop-max 对必须是 Ω(log n)。
在下面的 C++ 中实现了为所有三个操作获得 O(log n) 最坏情况的一种方法。通过摊销会计,我们可以使推送 O(log n) 以及 pop 和 pop-maxes 免费。
这确实留下了我们是否可以获得最坏情况下的 O(1) 推送、O(1) 弹出和 O(log n) 最大弹出的问题。我相信答案是肯定的,但是我想到的解决方案相当复杂,涉及在队列的大小呈几何级数减小的段上定期维护 O(log n) 锦标赛树。
#include <list>
#include <map>
template <typename T> class QueueWithPopMax
public:
void Push(T element)
typename std::list<ListElement>::iterator back =
list_.insert(list_.end(), ListElement);
back->iterator = multimap_.insert(element, back);
T Pop()
T element = list_.front().iterator->first;
multimap_.erase(list_.front().iterator);
list_.pop_front();
return element;
T PopMax()
T element = multimap_.begin()->first;
list_.erase(multimap_.begin()->second);
multimap_.erase(multimap_.begin());
return element;
private:
struct ListElement
typename std::multimap<T, typename std::list<ListElement>::iterator,
std::greater<T>>::iterator iterator;
;
std::multimap<T, typename std::list<ListElement>::iterator, std::greater<T>>
multimap_;
std::list<ListElement> list_;
;
#include <iostream>
int main()
QueueWithPopMax<int> queue;
queue.Push(2);
queue.Push(3);
queue.Push(4);
queue.Push(5);
queue.Push(1);
queue.Push(5);
std::cout << queue.PopMax() << "\n";
std::cout << queue.Pop() << "\n";
std::cout << queue.Pop() << "\n";
std::cout << queue.Pop() << "\n";
std::cout << queue.Pop() << "\n";
std::cout << queue.Pop() << "\n";
【讨论】:
您确定有解决“最坏情况 O(1) 推送、O(1) 弹出和 O(log n) 最大弹出”的解决方案吗?这似乎是一个非常、非常困难的问题,并且足够有趣,值得在它自己的帖子中发表(尽管这里似乎有几个古老的、未回答的问题正是关于这一点的)。但也许 cs.stackexchange 会是一个更好的讨论网站 @kcsquared 是的,我用我的推理发布了另一个答案。以上是关于使用 push(x)、pop() 和 pop_max() 方法实现队列的主要内容,如果未能解决你的问题,请参考以下文章