Hinal Vithlani

Priority Queue in Data Structure

What is Priority Queue?

A priority queue serves as a specialized data structure where elements are organized based on their priority values, ensuring that higher-priority items are processed before lower-priority ones. This organization enables efficient retrieval of the most critical elements.

Various implementations, such as arrays, linked lists, heaps, or binary search trees, offer different trade-offs in terms of efficiency and flexibility.

Priority queues find extensive applications in scenarios demanding real-time processing, where prompt handling of high-priority tasks is crucial. They also play a pivotal role in algorithmic optimizations, facilitating tasks like Dijkstra’s algorithm for shortest path finding and the A* search algorithm for efficient pathfinding in graphs.

Characteristics of a Priority Queue

A queue is called a Priority Queue when it has the following characteristics:

  • Each item in the queue must have a priority associated with it.
  • Higher or lower priority elements must be dequeued before lower or higher priority elements respectively depending on priority order taken by user that is if user consider lower number as higher priority or higher number as higher priority.
  • If two elements in the queue have the same priority value then the first in first out rule is followed for these two elements alone i.e. the element that entered the priority queue first will be the first to be removed.

Operations of a Priority Queue

A Priority Queue supports the following operations:

Insertion in a Priority Queue

When adding a new element to a priority queue, it’s initially placed in the next available slot from top to bottom and left to right. However, if the new element is not in its correct position according to the priority order, it’s compared with its parent node. If the new element is out of order, it’s swapped with its parent, and this process continues recursively until the element is correctly positioned relative to its parent and all other elements in the queue.

Deletion in a Priority Queue

In a max heap, the root node holds the maximum element, and it’s the first to be removed when prioritized removal is required. This removal action leaves an empty slot at the root, which is subsequently filled by promoting the last inserted element to the root position. After insertion, the newly placed element is compared with its parent and possibly swapped to maintain the heap property, ensuring that the highest priority element remains at the root. This comparison and potential swapping process continue recursively down the heap until the entire structure adheres to the max heap invariant.

Peek in a Priority Queue

This operation facilitates retrieving the maximum element from a Max Heap or the minimum element from a Min Heap without removing the node from the priority queue.

Extract-Max/Min from the Priority Queue

The operation Extract-Max retrieves and removes the node with the maximum value from a Max Heap, while Extract-Min performs the same action for a Min Heap, returning the node with the minimum value.

Types of Priority Queue

There are two types of Priority Queues: types of priority queue

1. Ascending Order Priority Queue

In an ascending order priority queue, all the elements are compared with another and the rule: The smaller the element(number), the higher the priority is applied. Let us consider a priority queue having the following priorities: 4,2,6,1,8 The first step would be to arrange them in ascending order as follows: 1,2,4,6,8

ascending order priority queue

In this priority queue, 1 is the smallest number and is hence the item with the highest priority. On the other hand, 8 is the largest number and is hence the item with the lowest priority.

2. Descending Order Priority Queue

In a descending order priority queue, all the elements are compared with another and the rule: The larger the element(number), the higher the priority. Let us consider a priority queue having the following priorities: 4,2,6,1,8 The first step would be to arrange them in descending order as follows: 8,6,4,2,1

descending order priority queue

In this priority queue, 8 is the largest number and is hence the item with the highest priority. On the other hand, 1 is the smallest number and is hence the item with the lowest priority.

Implementation of Priority Queue in Data Structures

A Priority Queue is implemented using one of the 4 Data Structures:

1. Implementation Using Linked List

In the linked list, the element having higher priority will be present at the head of the list. Then, based on priority, all the elements in the list are arranged in descending order based on priority.

Class Declaration:

static class pqNode {
    int data;
    int priority;
    Node next;
}

To insert an element, the list is traversed until a suitable position is found for the element to be inserted in order to maintain the overall order of the linked list. Hence, the worst time complexity is O(n) as the worst case is when all elements of the list need to be traversed.

For example, let us consider the linked list:

implementation of priority queue in data structures

Let us consider that we need to insert an element with priority 8. 8 has a priority lower than 5 so we traverse the linked list till we find a position where it is suitable. The linked list will then look like this:

implementation of priority queue-1

For deletion, the highest priority element will be removed first from the priority queue and the highest priority element is at the head of the list and hence it will simply be removed.

peek() is used to retrieve the highest priority element without removing it and this is also present at the head of the list.

Since the highest priority element is present at the head of the list, it takes just O(1) time to remove it or to do the peek() operation.

2. Implementation Using Binary Heap

Binary Heaps are a preferred data structure for implementing priority queues due to their efficient performance compared to arrays or linked lists. The heap properties ensure that the entry with the largest key is always at the top, allowing for immediate removal. Although restoring the heap property after removal takes O(log n) time for the remaining keys, this time can be combined with the O(log n) time needed for inserting a new entry if required immediately. This efficiency is particularly advantageous for large values of n, as heaps are efficiently represented in contiguous storage and guarantee logarithmic time complexity for both insertions and deletions.

3. Implementation Using Array

There are two ways to go about implementation of priority queues using arrays. The first is to use an ordered array and the other is to use an unordered array. In an ordered array, all the elements are ordered according to priority.

To insert a new element, you must traverse the array and insert it in such a way that the ordering is maintained. The worst case is hence O(n). Since they are in order, both the delete and the peek operations take O(1) time.

In an unordered array, all the elements are not arranged according to priority. Hence to insert an element, it does not matter where you insert it and hence it takes O(1) time.

However, to do the delete and peek operation, you must traverse the array and hence, the worst-case time complexity is O(n) for both.

4. Implementation Using Binary Search Tree

To maintain items efficiently in sorted order, a binary search tree is utilised. The binary search tree becomes a priority queue if the sort order is based on priority.

The time complexity to locate the top priority element is constant.

An extra pointer is kept to store the highest priority element, which will be updated as the insertion and deletion operations are completed. We will update the additional pointer with the deletion action by identifying the inorder successor.

Priority Queue Implementations in Python, Java, C, and C++

Implementation in Python

# To heapify
def heapify(array, num, x):
    # Find the largest among root, left child and right child
    max = x
    left = 2 * x + 1
    right = 2 * x + 2
 
    if left < num and array[x] < array[left]:
        max = left
 
    if right < num and array[max] < array[right]:
        max = right
 
    # If root is not largest, swap and continue heapifying 
    if max != x:
        array[x], array[max] = array[max], array[x]
        heapify(array, num, max)
 
 
# To insert an element 
def insert(arr, new):
    s = len(arr)
    if s == 0:
        arr.append(new)
    else:
        arr.append(new)
        for i in range((s // 2) - 1, -1, -1):
            heapify(arr, s, i)
 
 
# To delete an element 
def delete(arr, n):
    s = len(arr)
    i = 0
    for i in range(0, s):
        if n == arr[i]:
            break
 
    arr[i], arr[s - 1] = arr[s - 1], arr[i]
 
    arr.remove(s - 1)
 
    for i in range((len(arr) // 2) - 1, -1, -1):
        heapify(arr, len(arr), i)
 
 
array = []
 
insert(array, 1)
insert(array, 4)
insert(array, 2)
insert(array, 7)
insert(array, 8)
insert(array, 5)
insert(array, 6)
 
print ("Max-Heap array: " + str(array))
 
delete(array, 6)
print("After deletion: " + str(array))

Implementation in Java

import java.util.*;
 
public class Main {
    
  // To heapify the tree
  void heapify(ArrayList<Integer> arr, int x) {
    int s = arr.size();
    // Find the largest among root, left child and right child
    int max = x;
    int left = 2 * x + 1;
    int right = 2 * x + 2;
    if (left < s && arr.get(left) > arr.get(max))
      max = left;
    if (right < s && arr.get(right) > arr.get(max))
      max = right;
 
    // If root is not largest, swap and continue heapifying 
    if (max != x) {
      int temp = arr.get(max);
      arr.set(max, arr.get(x));
      arr.set(x, temp);
 
      heapify(arr, max);
    }
  }
 
  // To insert an element 
  void insert(ArrayList<Integer> arr, int newN) {
    int s = arr.size();
    if (s == 0) {
      arr.add(newN);
    } else {
      arr.add(newN);
      for (int i = s / 2 - 1; i >= 0; i--) {
        heapify(arr, i);
      }
    }
  }
 
  // To delete an element 
  void deleteNode(ArrayList<Integer> arr, int n) {
    int s = arr.size();
    int i;
    for (i = 0; i < s; i++) {
      if (n == arr.get(i))
        break;
    }
 
    int temp = arr.get(i);
    arr.set(i, arr.get(s - 1));
    arr.set(s - 1, temp);
 
    arr.remove(s - 1);
    for (int j = s / 2 - 1; j >= 0; j--) {
      heapify(arr, j);
    }
  }
 
  // To print the tree
  void printArray(ArrayList<Integer> arr, int s) {
    for (Integer i : arr) {
      System.out.print(i + " ");
    }
    System.out.println();
  }
 
  // Driver code
  public static void main(String args[]) {
 
    ArrayList<Integer> arr = new ArrayList<Integer>();
    int s = arr.size();
 
    Main h = new Main();
    h.insert(arr, 1);
    h.insert(arr, 4);
    h.insert(arr, 2);
    h.insert(arr, 7);
    h.insert(arr, 8);
    h.insert(arr, 5);
    h.insert(arr, 6);
    
    System.out.println("Max-Heap array: ");
    h.printArray(arr, s);
 
    h.deleteNode(arr, 6);
    System.out.println("After deletion: ");
    h.printArray(arr, s);
  }
}

Implementation in C

#include <stdio.h>
#include <stdlib.h>

// To swap position of two elements
void swap(int *x, int *y) {
  int temp = *y;
  *y = *x;
  *x = temp;
}

// To heapify the tree
void heapify(int arr[], int s, int x) {
  // Find the largest among root, left child and right child
  int max = x;
  int left = 2 * x + 1;
  int right = 2 * x + 2;
  if (left < s && arr[left] > arr[max])
    max = left;
  if (right < s && arr[right] > arr[max])
    max = right;

  // Swap and continue heapifying if root is not largest
  if (max != x) {
    swap(&arr[x], &arr[max]);
    heapify(arr, s, max);
  }
}

// To insert an element
void insert(int arr[], int *s, int newN) {
  if (*s == 0) {
    arr[(*s)++] = newN;
  } else {
    arr[(*s)++] = newN;
    for (int i = (*s) / 2 - 1; i >= 0; i--) {
      heapify(arr, *s, i);
    }
  }
}

// To delete an element from the tree
void deleteNode(int arr[], int *s, int n) {
  int i;
  for (i = 0; i < *s; i++) {
    if (n == arr[i])
      break;
  }
  swap(&arr[i], &arr[(*s) - 1]);

  (*s)--;
  for (int i = (*s) / 2 - 1; i >= 0; i--) {
    heapify(arr, *s, i);
  }
}

// To print the tree
void printArray(int arr[], int s) {
  for (int i = 0; i < s; ++i)
    printf("%d ", arr[i]);
  printf("\n");
}

// Driver code
int main() {
  int arr[100];
  int s = 0;

  insert(arr, &s, 1);
  insert(arr, &s, 4);
  insert(arr, &s, 2);
  insert(arr, &s, 7);
  insert(arr, &s, 8);
  insert(arr, &s, 5);
  insert(arr, &s, 6);

  printf("Max-Heap array: ");
  printArray(arr, s);

  deleteNode(arr, &s, 6);

  printf("After deletion: ");

  printArray(arr, s);

  return 0;
}

Implementation in C++

#include <iostream>
#include <vector>
using namespace std;
 
// To swap position of two elements
void swap(int *x, int *y) {
  int temp = *y;
  *y = *x;
  *x = temp;
}
 
//To heapify the tree
void heapify(vector<int> &arr, int x) {
  int s = arr.size();
  
  // Find the largest among root, left child and right child
  int max = x;
  int left = 2 * x + 1;
  int right = 2 * x + 2;
  if (left < s && arr[left] > arr[max])
    max = left;
  if (right < s && arr[right] > arr[max])
    max = right;
 
  // Swap and continue heapifying if root is not largest
  if (max != x) {
    swap(&arr[x], &arr[max]);
    heapify(arr, max);
  }
}
 
// To insert an element 
void insert(vector<int> &arr, int newN) {
  int s = arr.size();
  if (s == 0) {
    arr.push_back(newN);
  } else {
    arr.push_back(newN);
    for (int i = s / 2 - 1; i >= 0; i--) {
      heapify(arr, i);
    }
  }
}
 
// To delete an element from the tree
void deleteNode(vector<int> &arr, int n) {
  int s = arr.size();
  int i;
  for (i = 0; i < s; i++) {
    if (n == arr[i])
      break;
  }
  swap(&arr[i], &arr[s - 1]);
 
  arr.pop_back();
  for (int i = s / 2 - 1; i >= 0; i--) {
    heapify(arr, i);
  }
}
 
// To print the tree
void printArray(vector<int> &arr) {
  for (int i = 0; i < arr.size(); ++i)
    cout << arr[i] << " ";
  cout << "\n";
}
 
// Driver code
int main() {
  vector<int> arr;
 
  insert(arr, 1);
  insert(arr, 4);
  insert(arr, 2);
  insert(arr, 7);
  insert(arr, 8);
  insert(arr, 5);
  insert(arr, 6);
 
  cout << "Max-Heap array: ";
  printArray(arr);
 
  deleteNode(arr, 6);
 
  cout << "After deletion: ";
 
  printArray(arr);
}

Output:

Max-Heap array: 8 7 6 4 1 5 2 
After deletion: 8 7 5 4 1 2 

Applications of Priority Queue

  • Djikstra’s Algorithm – To find the shortest path between nodes in a graph.
  • Prim’s Algorithm – To find the Minimum Spanning Tree in a weighted undirected graph.
  • Heap Sort – To sort the Heap Data Structure
  • Huffman Coding – A Data Compression Algorithm
  • It is used in Operating Systems for:
    1. Priority Scheduling – Where processes must be scheduled according to their priority.
    2. Load Balancing – Where network or application traffic must be balanced across multiple servers.
    3. Interrupt Handling – When a current process is interrupted, a handler is assigned to the same to rectify the situation immediately.
  • A* Search Algorithm – A graph traversal and a path search algorithm

Advantages of Priority Queue

  1. Priority queues offer fast access to the highest priority element, streamlining retrieval without exhaustive searching.
  2. Dynamic ordering enables priority values to be updated, facilitating automatic reordering as priorities shift.
  3. They support efficient algorithm implementation, enhancing performance in tasks like Dijkstra’s shortest path and A* search.
  4. Priority queues find application in real-time systems for their ability to swiftly retrieve high-priority elements, critical in time-sensitive environments.

Disadvantages of Priority Queue

  1. Priority queues pose complexity challenges compared to simpler data structures like arrays or linked lists, potentially demanding more intricate implementation and upkeep.
  2. They can consume significant memory due to the storage of priority values for each element, which may be problematic in resource-constrained environments.
  3. Despite their versatility, priority queues may not always be the optimal choice, as alternative structures like heaps or binary search trees might offer better efficiency for specific operations.
  4. Predictability may vary with priority queues, as element retrieval depends on priority values rather than strict FIFO or LIFO order, leading to less deterministic outcomes in certain scenarios.

Conclusion

  1. Priority queues, a crucial data structure, organize elements based on priority for efficient retrieval of critical tasks.
  2. Various implementations like arrays, linked lists, heaps, or binary search trees offer flexibility with trade-offs in performance.
  3. They find extensive use in real-time systems and algorithmic optimizations, supporting tasks like pathfinding and scheduling.
  4. While advantageous for fast access and dynamic ordering, priority queues can be complex and memory-intensive.
  5. Careful consideration of alternative data structures may be necessary for optimal performance in specific scenarios.
  6. Despite challenges, priority queues remain indispensable tools for managing prioritized tasks in diverse applications.

Author