What's the best time complexity of a queue that supports extracting the minimum?

Viewed 2026

I ran into the following very difficult interview question:

Consider Queue Data Structure with three operations:

- Add into the front of list (be careful front of list)

- Delete from Tail of the list (end of the list)

- Extract Min (remove)

The best implementation of this data structure has amortized time:

A) three operation at O(1)

B) three operation at O(log n)

C) add and delete in O(1) and Extract-min in O(log n)

D) add and delete in O(log n) and Extract-min in O(n)

After the interview I saw that (C) is the correct answer. Why is this the case?

The first challenge is comparing the options: which option is better than the others and how we can understand the final correct option?

5 Answers

Of the given running times, A is faster than C is faster than B is faster than D.

A is impossible in a comparison-based data structure (the unstated norm here) because it would violate the known Ω(n log n)-time lower bound for comparison sorts by allowing a linear-time sorting algorithm that inserts n elements and then extracts the min n times.

C can be accomplished using an augmented finger tree. Finger trees support queue-like push and pop in amortized constant time, and it's possible to augment each node with the minimum in its sub-tree. To extract the min, we use the augmentations to find the minimum value in the tree, which will be at depth O(log n). Then we extract this minimum by issuing two splits and an append, all of which run in amortized time O(log n).

Another possibility is to represent the sequence as a splay tree whose nodes are augmented by subtree min. Push and pop are O(1) amortized by the dynamic finger theorem.

Fibonacci heaps do not accomplish the same time bound without further examination because deletes cost Θ(log n) amortized regardless of whether the deleted element is the min.

Since C is feasible, there is no need to consider B or D.


Given the limitations on the data structure, we actually don't need the full power of finger trees. The C++ below works by maintaining a list of winner trees, where each tree has size a power of two (ignoring deletion, which we can implement as soft delete without blowing up the amortized running time). The sizes of the trees increase and then decrease, and there are O(log n) of them. This gives the flavor of finger trees with a much lesser implementation headache.

To push on the left, we make a size-1 tree and then merge it until the invariant is restored. The time required is O(1) amortized by the same logic as increasing a binary number by one.

To pop on the right, we split the rightmost winner tree until we find a single element. This may take a while, but we can charge it all to the corresponding push operations.

To extract the max (changed from min for convenience because nullopt is minus infinity, not plus infinity), find the winner tree containing the max (O(log n) since there are O(log n) trees) and then soft delete the max from that winner tree (O(log n) because that's the height of that tree).

#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;
    }
    }
  }
}

It sounds like they were probing you for knowing about priority queues implemented by way of fibonacci heaps.

Such implementations has the running times described in answer c.

O(1) time as only one operation has to be performed to locate it, so we can add at the start and delete from the end in a single operation.

O(log n) when we do divide and conquer type of algorithms like binary search, so as we have to extract the minimum instead of doing O(n) and increasing the time complexity we use the O(log n)

You will start by thinking a min-heap for the extract-min operation. This will take O(log n) time, but so will the operations add and delete. You need to think can we have any of these two operations in constant time? Is there any data structure which can do so?

Closest answer is : Fibonacci-heap, used for implementing priority-queues (quite popularly used for implementing Djistra's Algorithm), which has the amortized run-time complexities of O(1) for insert, delete in O(log n) though (but since the operation is always delete from tail, we may be able to achieve this by maintaining a pointer to the last node and doing the decrease-key operation in O(1) time) and O(log n) for delete-min operations.

Internally, fibonacci-heap is a collection of trees, all satisfying the standard min-heap condition (value of parent always lower than its children), where the roots of all these trees are linked using a circular doubly linked list. This section best explains the implementations of each operation alongside its run-time complexity in further detail.

Have a look at this great answer which explains the intuition behind fibonacci-heaps.

Edit: As per your query regarding choosing between B to D, let's discuss them one by one.

(B) would be your first-at-glance answer since it clearly strikes as a min-heap. This eliminates (D) as well since it says extract-min in O(n) time but we clearly know we can do better. Thus leaving (C), with better options for add/delete operations, that is O(1). If you can think of combining multiple min-heaps (roots and children) with a circular doubly linked list, keeping track of a pointer to the root containing the minimum key in a data structure, i.e. a fibonacci-heap, you know that option (C) is possible and since its better than option (B), you have your answer.

Let's explore all the answers.

A is impossible because you can't find the Min in O(1) . Because obviously you should find it before removing it. And you need to do some operations to find it.

B is wrong also because we know that adding is O(1). The delete is also O(1) because we can directly access the last and the 1st elements.

By the same argument D is also wrong.

So we're left with C.

Related