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

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

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

- 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 个元素然后提取,从而违反比较排序的已知 ?(n log n) 时间下限最少 n 次。

C 可以使用增强的指树来完成。指状树支持在摊销固定时间内类似队列的推送和弹出,并且可以用其子树​​中的最小值来增加每个节点。为了提取最小值,我们使用增强来找到树中的最小值,这将是深度 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("%dn", *value);
      } else {
        puts("null");
      }
      break;
    }
    case 3: {
      if (auto value = queue.ExtractMax()) {
        printf("%dn", *value);
      } else {
        puts("null");
      }
      break;
    }
    }
  }
}

  • @EmmaNic. B beats D and is achievable at least two ways using data structures and techniques that appear in CLRS. You could keep a doubly linked list and a heap with pointers between corresponding nodes in each and then cascade the deletion (list determines which element is the tail, cascading delete from heap; heap determines which element is the min, cascading delete from list). You could also augment a regular balanced binary tree (*not* a search tree). Honestly I think this question is way too hard for an interview, and if it were forced on me, I wouldn't downgrade anyone for picking B.

以上是支持提取最小值的队列的最佳时间复杂度是多少?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>