In the world of C++ data structures, the priority queue stands out as a powerful tool for managing elements based on their priority. This heap-based container adaptor provides efficient access to the highest (or lowest) priority element, making it invaluable for various applications, from task scheduling to graph algorithms.

Understanding Priority Queues in C++

A priority queue in C++ is a container adaptor that provides constant time lookup of the largest (by default) or smallest element, at the cost of logarithmic insertion and extraction. It's implemented using a heap, which is a specialized tree-based data structure.

🔑 Key Features:

  • Elements are stored in a way that the highest priority element is always at the top.
  • Insertion and deletion operations take O(log n) time.
  • Accessing the top element is an O(1) operation.

Let's dive into how to use priority queues in C++ with practical examples.

Creating a Priority Queue

To use a priority queue in C++, you need to include the <queue> header. Here's a simple example of creating a max heap priority queue:

#include <iostream>
#include <queue>

int main() {
    std::priority_queue<int> pq;

    pq.push(10);
    pq.push(30);
    pq.push(20);

    std::cout << "Top element: " << pq.top() << std::endl;

    return 0;
}

Output:

Top element: 30

In this example, we create a priority queue of integers. By default, C++ uses a max heap, so the largest element is always at the top.

Creating a Min Heap Priority Queue

To create a min heap priority queue, we need to use the greater<> comparator:

#include <iostream>
#include <queue>
#include <vector>

int main() {
    std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;

    min_pq.push(10);
    min_pq.push(30);
    min_pq.push(20);

    std::cout << "Top element: " << min_pq.top() << std::endl;

    return 0;
}

Output:

Top element: 10

Here, we've created a min heap priority queue where the smallest element is always at the top.

Basic Operations on Priority Queue

Let's explore the basic operations you can perform on a priority queue:

#include <iostream>
#include <queue>

int main() {
    std::priority_queue<int> pq;

    // Pushing elements
    pq.push(30);
    pq.push(100);
    pq.push(25);
    pq.push(40);

    std::cout << "Size of priority queue: " << pq.size() << std::endl;

    // Accessing top element
    std::cout << "Top element: " << pq.top() << std::endl;

    // Removing top element
    pq.pop();
    std::cout << "After popping, new top: " << pq.top() << std::endl;

    // Checking if empty
    std::cout << "Is priority queue empty? " << (pq.empty() ? "Yes" : "No") << std::endl;

    return 0;
}

Output:

Size of priority queue: 4
Top element: 100
After popping, new top: 40
Is priority queue empty? No

🔍 Note: The pop() function removes the top element but doesn't return it. If you need the value, make sure to call top() before pop().

Priority Queue with Custom Objects

Priority queues aren't limited to built-in types. You can use them with custom objects by defining a comparison function. Let's create a priority queue of students sorted by their grades:

#include <iostream>
#include <queue>
#include <string>

struct Student {
    std::string name;
    int grade;

    Student(std::string n, int g) : name(n), grade(g) {}
};

struct CompareStudent {
    bool operator()(const Student& s1, const Student& s2) {
        return s1.grade < s2.grade;
    }
};

int main() {
    std::priority_queue<Student, std::vector<Student>, CompareStudent> student_pq;

    student_pq.push(Student("Alice", 85));
    student_pq.push(Student("Bob", 92));
    student_pq.push(Student("Charlie", 78));
    student_pq.push(Student("David", 95));

    std::cout << "Top student: " << student_pq.top().name 
              << " with grade " << student_pq.top().grade << std::endl;

    return 0;
}

Output:

Top student: David with grade 95

In this example, we've created a custom Student struct and a CompareStudent functor to define how students should be ordered in the priority queue.

Implementing a Task Scheduler

Let's use a priority queue to implement a simple task scheduler. Each task will have a priority and a description:

#include <iostream>
#include <queue>
#include <string>

struct Task {
    int priority;
    std::string description;

    Task(int p, std::string d) : priority(p), description(d) {}
};

struct CompareTask {
    bool operator()(const Task& t1, const Task& t2) {
        return t1.priority < t2.priority;
    }
};

class TaskScheduler {
private:
    std::priority_queue<Task, std::vector<Task>, CompareTask> tasks;

public:
    void addTask(int priority, const std::string& description) {
        tasks.push(Task(priority, description));
    }

    void executeHighestPriorityTask() {
        if (!tasks.empty()) {
            std::cout << "Executing task: " << tasks.top().description 
                      << " (Priority: " << tasks.top().priority << ")" << std::endl;
            tasks.pop();
        } else {
            std::cout << "No tasks to execute." << std::endl;
        }
    }
};

int main() {
    TaskScheduler scheduler;

    scheduler.addTask(3, "Send email");
    scheduler.addTask(1, "File report");
    scheduler.addTask(2, "Call client");
    scheduler.addTask(5, "Urgent meeting");

    for (int i = 0; i < 5; ++i) {
        scheduler.executeHighestPriorityTask();
    }

    return 0;
}

Output:

Executing task: Urgent meeting (Priority: 5)
Executing task: Send email (Priority: 3)
Executing task: Call client (Priority: 2)
Executing task: File report (Priority: 1)
No tasks to execute.

This task scheduler uses a priority queue to always execute the highest priority task first.

Performance Considerations

🚀 Performance characteristics of priority queues:

Operation Time Complexity
Push O(log n)
Pop O(log n)
Top O(1)
Empty O(1)
Size O(1)

While priority queues offer excellent performance for their intended use cases, it's important to consider alternatives for different scenarios:

  • If you need fast insertion and deletion at both ends, consider std::deque.
  • For sorted data with fast insertion at any position, std::set might be more appropriate.
  • If you need to frequently access elements by index, std::vector could be a better choice.

Advanced Usage: Dijkstra's Algorithm

Priority queues are often used in graph algorithms. Let's implement Dijkstra's algorithm for finding the shortest path in a weighted graph using a priority queue:

#include <iostream>
#include <vector>
#include <queue>
#include <limits>

typedef std::pair<int, int> pii;

class Graph {
    int V;
    std::vector<std::vector<pii>> adj;

public:
    Graph(int vertices) : V(vertices) {
        adj.resize(V);
    }

    void addEdge(int u, int v, int w) {
        adj[u].push_back(std::make_pair(v, w));
        adj[v].push_back(std::make_pair(u, w));
    }

    void shortestPath(int src) {
        std::priority_queue<pii, std::vector<pii>, std::greater<pii>> pq;
        std::vector<int> dist(V, std::numeric_limits<int>::max());

        pq.push(std::make_pair(0, src));
        dist[src] = 0;

        while (!pq.empty()) {
            int u = pq.top().second;
            pq.pop();

            for (auto i = adj[u].begin(); i != adj[u].end(); ++i) {
                int v = (*i).first;
                int weight = (*i).second;

                if (dist[v] > dist[u] + weight) {
                    dist[v] = dist[u] + weight;
                    pq.push(std::make_pair(dist[v], v));
                }
            }
        }

        std::cout << "Vertex   Distance from Source\n";
        for (int i = 0; i < V; ++i)
            std::cout << i << "\t\t" << dist[i] << "\n";
    }
};

int main() {
    Graph g(9);

    g.addEdge(0, 1, 4);
    g.addEdge(0, 7, 8);
    g.addEdge(1, 2, 8);
    g.addEdge(1, 7, 11);
    g.addEdge(2, 3, 7);
    g.addEdge(2, 8, 2);
    g.addEdge(2, 5, 4);
    g.addEdge(3, 4, 9);
    g.addEdge(3, 5, 14);
    g.addEdge(4, 5, 10);
    g.addEdge(5, 6, 2);
    g.addEdge(6, 7, 1);
    g.addEdge(6, 8, 6);
    g.addEdge(7, 8, 7);

    g.shortestPath(0);

    return 0;
}

Output:

Vertex   Distance from Source
0        0
1        4
2        12
3        19
4        21
5        11
6        9
7        8
8        14

In this implementation, we use a priority queue to always process the vertex with the smallest known distance first, which is the core idea of Dijkstra's algorithm.

Conclusion

Priority queues in C++ offer a powerful way to manage elements based on their priority. Whether you're implementing a task scheduler, optimizing graph algorithms, or solving complex programming challenges, the priority queue data structure provides an efficient solution for priority-based operations.

Remember these key points:

  • Priority queues in C++ are implemented as heap-based container adaptors.
  • By default, C++ priority queues are max heaps, but can be configured as min heaps.
  • Custom objects can be used with priority queues by defining appropriate comparison functions.
  • Priority queues are particularly useful in scenarios where you always need quick access to the highest (or lowest) priority element.

By mastering priority queues, you'll add a valuable tool to your C++ programming toolkit, enabling you to solve a wide range of problems more efficiently and elegantly.