微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

支持提取最小值的队列的最佳时间复杂度是多少?

如何解决支持提取最小值的队列的最佳时间复杂度是多少?

我遇到了以下非常困难的面试问题:

考虑具有三个操作的队列数据结构:

- 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 可以使用增强的 finger tree 来完成。指状树支持在固定时间摊销的类似队列的推送和弹出,并且可以用其子树​​中的最小值来增加每个节点。为了提取最小值,我们使用增强来找到树中的最小值,该值将在深度 O(log n) 处。然后我们通过发出两个拆分和一个附加来提取这个最小值,所有这些都在分摊时间 O(log n) 内运行。

另一种可能性是将序列表示为一棵展开树,其节点由子树 min 增加。 push 和 pop 是 O(1) 分摊的动态手指定理。

斐波那契堆不会在没有进一步检查的情况下完成相同的时间限制,因为删除成本 Θ(log n) 摊销,无论删除的元素是否为最小值。

既然C是可行的,就不用考虑B或D了。


鉴于数据结构的限制,我们实际上不需要手指树的全部功能。下面的 C++ 通过维护一个获胜树列表来工作,其中每棵树的大小都是 2 的幂(忽略删除,我们可以将其实现为软删除,而不会增加分摊的运行时间)。树的大小先增后减,有 O(log n) 个。这赋予了指形树的味道,而且实现的麻烦要小得多。

为了向左推,我们制作了一个大小为 1 的树,然后合并它直到不变量恢复。所需时间为 O(1),按与二进制数加 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;
    }
    }
  }
}
,

听起来他们是在询问您是否了解通过 fibonacci heaps 实现的优先级队列。

此类实现具有答案 c 中描述的运行时间。

,

O(1) 时间,因为只需要执行一个操作来定位它,所以我们可以在一个操作中在开头添加和从结尾删除。

O(log n) 当我们进行分而治之类的算法(如二分搜索)时,因为我们必须提取最小值而不是执行 O(n) 并增加我们使用 O(log n) 的时间复杂度

,

您将首先考虑用于提取最小操作的最小堆。这将花费 O(log n) 时间,但操作 adddelete 也是如此。您需要考虑我们可以在恒定时间内进行这两种操作中的任何一种吗?有什么数据结构可以做到这一点吗?

最接近的答案是:Fibonacci-heap,用于实现优先级队列(非常普遍用于实现 Djistra 算法),它具有 O(1) 的分摊运行时复杂度,用于插入、删除在 O(log n) 虽然(但由于操作总是从尾部删除,我们可以通过维护指向最后一个节点的指针并在 O(1) 时间内执行减键操作来实现这一点)和 O(log n) 为删除最小操作。

在内部,fibonacci-heap 是一个树的集合,所有树都满足标准的最小堆条件(父级的值总是低于其子级的值),其中所有这些树的根使用循环双向链表链接。此 section 最好地解释了每个操作的实现及其运行时复杂性的更多细节。

看看这个 great 答案,它解释了斐波那契堆背后的直觉。

编辑:根据您关于在 B 到 D 之间进行选择的疑问,让我们一一讨论。

(B) 将是您第一眼看到的答案,因为它显然是一个最小堆。这也消除了 (D),因为它说在 O(n) 时间内提取分钟,但我们清楚地知道我们可以做得更好。从而使 (C) 具有更好的添加/删除操作选项,即 O(1)。如果您可以考虑将多个最小堆(根和子堆)与循环双向链表相结合,跟踪指向包含数据结构中最小键的根的指针,即斐波那契堆,您就知道该选项( C) 是可能的,因为它比选项 (B) 更好,所以你有你的答案。

,

让我们探索所有答案。

A 是不可能的,因为您无法在 O(1) 中找到 Min 。因为显然你应该在删除它之前找到它。而且你需要做一些操作才能找到它。

B 也是错误的,因为我们知道加法是 O(1)。删除也是 O(1) 因为我们可以直接访问最后一个和第一个元素。

同理,D 也是错误的。

所以我们只剩下 C。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。