Custom Comparator In C For Priority Queue Implementation

10 min read 11-15- 2024
Custom Comparator In C For Priority Queue Implementation

Table of Contents :

In the world of programming, especially in C, data structures play a pivotal role in ensuring efficient data manipulation and retrieval. One such data structure is the Priority Queue, which is essential for applications where certain elements must be processed before others. In this article, we will delve into the implementation of a custom comparator for a priority queue in C, exploring its significance, methods of implementation, and practical use cases.

Understanding Priority Queue

A priority queue is an abstract data type that operates similarly to a regular queue but with an additional feature: each element is associated with a priority. In a priority queue, elements are dequeued based on their priority rather than their position in the queue.

How Priority Queues Work

In a standard queue, elements are processed in a first-in, first-out (FIFO) manner. However, in a priority queue:

  • The element with the highest priority is served before others.
  • If two elements have the same priority, they are served according to their order in the queue.

This structure is particularly useful in scenarios such as task scheduling, graph algorithms (like Dijkstra’s), and event simulation systems.

Custom Comparator: Why is it Important?

When implementing a priority queue, it's essential to establish a method for comparing the elements based on their priority. The custom comparator allows us to define how the elements should be prioritized, facilitating greater flexibility. This is especially beneficial when working with complex data types or multiple criteria for comparison.

Key Functions of a Custom Comparator

  1. Flexibility: Define unique rules for comparing elements.
  2. Versatility: Adapt the comparator for different data types and structures.
  3. Simplicity: Improve readability and maintainability of the code.

Implementing a Priority Queue with Custom Comparator in C

Let's break down the steps to create a priority queue using a custom comparator in C.

Step 1: Include Necessary Headers

We'll need to include the standard library headers for basic functions, memory allocation, and I/O operations.

#include 
#include 

Step 2: Define the Structure for the Elements

Define a structure for the elements that will be inserted into the priority queue. For example, we can create a structure representing a task:

typedef struct {
    int id;         // Unique identifier for the task
    int priority;   // Priority of the task
} Task;

Step 3: Custom Comparator Function

Create a custom comparator function that defines the priority of the tasks. The function should return:

  • A negative value if the first task has higher priority.
  • A positive value if the second task has higher priority.
  • Zero if both tasks have the same priority.
int compareTasks(const void *a, const void *b) {
    Task *task1 = (Task *)a;
    Task *task2 = (Task *)b;
    
    return task2->priority - task1->priority;  // Higher number = higher priority
}

Step 4: Implement the Priority Queue

For the priority queue, we can utilize a binary heap for efficient enqueue and dequeue operations. Let's create an array to store our tasks and maintain the heap properties.

typedef struct {
    Task *tasks;     // Array of tasks
    int size;        // Current number of tasks
    int capacity;    // Maximum capacity of the queue
} PriorityQueue;

Initialize the Priority Queue

PriorityQueue *createPriorityQueue(int capacity) {
    PriorityQueue *pq = (PriorityQueue *)malloc(sizeof(PriorityQueue));
    pq->capacity = capacity;
    pq->size = 0;
    pq->tasks = (Task *)malloc(capacity * sizeof(Task));
    return pq;
}

Insertion (Enqueue)

To insert tasks, we add the new task to the end of the array and then "bubble up" to maintain the heap property.

void enqueue(PriorityQueue *pq, Task task) {
    if (pq->size == pq->capacity) {
        printf("Priority Queue is full!\n");
        return;
    }
    
    pq->tasks[pq->size] = task;  // Add new task
    pq->size++;
    
    int i = pq->size - 1;
    
    // Bubble up
    while (i != 0 && compareTasks(&pq->tasks[(i - 1) / 2], &pq->tasks[i]) > 0) {
        Task temp = pq->tasks[i];
        pq->tasks[i] = pq->tasks[(i - 1) / 2];
        pq->tasks[(i - 1) / 2] = temp;
        
        i = (i - 1) / 2;
    }
}

Removal (Dequeue)

To remove the highest priority task, we take the root element, replace it with the last element, and then "bubble down" to maintain the heap structure.

Task dequeue(PriorityQueue *pq) {
    if (pq->size <= 0) {
        printf("Priority Queue is empty!\n");
        exit(EXIT_FAILURE);
    }
    
    Task root = pq->tasks[0];  // The highest priority task
    
    // Replace root with the last element
    pq->tasks[0] = pq->tasks[pq->size - 1];
    pq->size--;
    
    // Bubble down
    int i = 0;
    while (2 * i + 1 < pq->size) {
        int leftChild = 2 * i + 1;
        int rightChild = 2 * i + 2;
        int largest = i;

        if (compareTasks(&pq->tasks[largest], &pq->tasks[leftChild]) > 0) {
            largest = leftChild;
        }
        if (rightChild < pq->size && compareTasks(&pq->tasks[largest], &pq->tasks[rightChild]) > 0) {
            largest = rightChild;
        }
        if (largest == i) break;  // The heap property is satisfied

        // Swap
        Task temp = pq->tasks[i];
        pq->tasks[i] = pq->tasks[largest];
        pq->tasks[largest] = temp;
        i = largest;
    }
    
    return root;
}

Step 5: Testing the Priority Queue

Finally, let's create a main function to test our priority queue implementation.

int main() {
    PriorityQueue *pq = createPriorityQueue(10);
    
    Task task1 = {1, 3};
    Task task2 = {2, 5};
    Task task3 = {3, 1};
    
    enqueue(pq, task1);
    enqueue(pq, task2);
    enqueue(pq, task3);
    
    printf("Dequeued task ID: %d with priority: %d\n", dequeue(pq).id, dequeue(pq).priority);
    printf("Dequeued task ID: %d with priority: %d\n", dequeue(pq).id, dequeue(pq).priority);
    
    free(pq->tasks);
    free(pq);
    
    return 0;
}

Summary of the Implementation

Operation Time Complexity
Enqueue O(log n)
Dequeue O(log n)
Peek O(1)

Important Notes

The custom comparator allows the flexibility to change the ordering based on various criteria. Ensure that your comparator correctly reflects the desired priority logic.

Use Cases of Priority Queue

  1. Task Scheduling: Operating systems use priority queues to manage processes efficiently.
  2. Graph Algorithms: Algorithms like Dijkstra’s utilize priority queues to explore the shortest paths in a graph.
  3. Event Simulation: Simulation systems often need to prioritize events based on time.

In conclusion, implementing a priority queue with a custom comparator in C provides a robust solution for managing priorities efficiently. Through the steps outlined in this article, you should be equipped to build and customize your own priority queue to suit your specific needs.