algorithm - 支持提取最小值的队列的最佳时间复杂度是多少?
问题描述
我遇到了以下非常困难的面试问题:
考虑具有三个操作的队列数据结构:
- Add into the front of list (be careful front of list) - Delete from Tail of the list (end of the list) - Extract Min (remove)
这种数据结构的最佳实现具有摊销时间:
A) O(1) 的三个操作
B) O(log n) 的三个操作
C) 在 O(1) 中添加和删除,在 O(log n) 中提取最小
D) 在 O(log n) 中添加和删除,在 O(n) 中提取最小
面试后我看到(C)是正确的答案。为什么会这样?
第一个挑战是比较选项:哪个选项比其他选项更好,我们如何理解最终正确的选项?
解决方案
在给定的运行时间中,A 比 C 快 比 B 比 D 快。
A 在基于比较的数据结构中是不可能的(此处未说明的规范),因为它会违反已知的比较排序的 Ω(n log n) 时间下限,因为它允许插入 n 个元素然后提取的线性时间排序算法最少 n 次。
C 可以使用增强的手指树来完成。手指树支持分摊常数时间内的类似队列的推送和弹出,并且可以使用其子树中的最小值来扩充每个节点。为了提取最小值,我们使用增强来找到树中的最小值,其深度为 O(log n)。然后我们通过发出两个拆分和一个附加来提取这个最小值,所有这些都在摊销时间 O(log n) 内运行。
另一种可能性是将序列表示为一个展开树,其节点由 subtree min 增加。Push 和 pop 是由动态手指定理摊销的 O(1)。
斐波那契堆在没有进一步检查的情况下不会完成相同的时间限制,因为无论删除的元素是否是最小值,删除成本 Θ(log n) 都会摊销。
由于 C 是可行的,因此无需考虑 B 或 D。
鉴于数据结构的限制,我们实际上不需要手指树的全部功能。下面的 C++ 通过维护一个获胜者树列表来工作,其中每棵树的大小都是 2 的幂(忽略删除,我们可以将其实现为软删除,而不会破坏摊销的运行时间)。树的大小先增大后减小,有 O(log n) 个。这给了手指树的味道,实现的麻烦要少得多。
为了向左推,我们制作一棵大小为 1 的树,然后将其合并,直到恢复不变量。所需时间为 O(1),按与将二进制数加一相同的逻辑摊销。
要在右侧弹出,我们拆分最右边的获胜者树,直到找到一个元素。这可能需要一段时间,但我们可以将其全部计入相应的推送操作。
要提取最大值(为方便起见,从 min 更改,因为 nullopt 是负无穷大,而不是正无穷大),找到包含最大值的获胜者树(O(log n),因为有 O(log n) 树),然后软删除最大值从那棵获胜者树(O(log n),因为那是那棵树的高度)。
#include <stdio.h>
#include <stdlib.h>
#include <list>
#include <optional>
class Node {
public:
using List = std::list<Node *>;
virtual ~Node() = default;
virtual int Rank() const = 0;
virtual std::optional<int> Max() const = 0;
virtual void RemoveMax() = 0;
virtual std::optional<int> PopRight(List &nodes, List::iterator position) = 0;
};
class Leaf : public Node {
public:
explicit Leaf(int value) : value_(value) {}
int Rank() const override { return 0; }
std::optional<int> Max() const override { return value_; }
void RemoveMax() override { value_ = std::nullopt; }
std::optional<int> PopRight(List &nodes, List::iterator position) override {
nodes.erase(position);
return value_;
}
private:
std::optional<int> value_;
};
class Branch : public Node {
public:
Branch(Node *left, Node *right)
: left_(left), right_(right),
rank_(std::max(left->Rank(), right->Rank()) + 1) {
UpdateMax();
}
int Rank() const override { return rank_; }
std::optional<int> Max() const override { return max_; }
void RemoveMax() override {
if (left_->Max() == max_) {
left_->RemoveMax();
} else {
right_->RemoveMax();
}
UpdateMax();
}
std::optional<int> PopRight(List &nodes, List::iterator position) override {
nodes.insert(position, left_);
auto right_position = nodes.insert(position, right_);
nodes.erase(position);
return right_->PopRight(nodes, right_position);
}
private:
void UpdateMax() { max_ = std::max(left_->Max(), right_->Max()); }
Node *left_;
Node *right_;
int rank_;
std::optional<int> max_;
};
class Queue {
public:
void PushLeft(int value) {
Node *first = new Leaf(value);
while (!nodes_.empty() && first->Rank() == nodes_.front()->Rank()) {
first = new Branch(first, nodes_.front());
nodes_.pop_front();
}
nodes_.insert(nodes_.begin(), first);
}
std::optional<int> PopRight() {
while (!nodes_.empty()) {
auto last = --nodes_.end();
if (auto value = (*last)->PopRight(nodes_, last)) {
return value;
}
}
return std::nullopt;
}
std::optional<int> ExtractMax() {
std::optional<int> max = std::nullopt;
for (Node *node : nodes_) {
max = std::max(max, node->Max());
}
for (Node *node : nodes_) {
if (node->Max() == max) {
node->RemoveMax();
break;
}
}
return max;
}
private:
std::list<Node *> nodes_;
};
int main() {
Queue queue;
int choice;
while (scanf("%d", &choice) == 1) {
switch (choice) {
case 1: {
int value;
if (scanf("%d", &value) != 1) {
return EXIT_FAILURE;
}
queue.PushLeft(value);
break;
}
case 2: {
if (auto value = queue.PopRight()) {
printf("%d\n", *value);
} else {
puts("null");
}
break;
}
case 3: {
if (auto value = queue.ExtractMax()) {
printf("%d\n", *value);
} else {
puts("null");
}
break;
}
}
}
}
推荐阅读
- python - 如何从电报会话中保存数据 - django
- git - Git克隆到现有分支(覆盖现有分支)
- python - 为什么当我使用 OR 布尔运算符时,while 循环会一直持续到两个变量都达到目标而不是一个
- python - 当有两个接受字符串输入的 django 路径时,为什么我会收到 NoReverseError 消息?(我正在研究 CS50 项目 1。)
- apache-spark - 气流触发 Spark 应用程序导致错误“框架太大”
- html - 我怎样才能使这个词边框底部?
- javascript - 使用 AJAX 删除模型数据 - Django
- html - 网页图像正确加载而没有任何参考?
- go - 避免为字符串映射和切片重复相同的代码
- reactjs - 对象字面量只能指定已知属性,而“任务”在“SetStateAction”类型中不存在