Queue Data Structure

Queue Data Structure

Queue Data Structure

A Queue is a linear data structure that follows First-In-First-Out (FIFO) principle. This means that the first element added to the queue is the first element to be removed from the queue. Think of it like a line at a supermarket where the first person in line will be served first and so on.

The Queue data structure has two main operations - enqueue (add an element to the end of the queue) and dequeue (remove an element from the front of the queue). Additionally, there are two other important operations - peek (retrieve the element at the front of the queue without removing it) and isEmpty (check if the queue is empty)

Queue Data Structure

Common Use Cases of Queue Data Structure

  • Task Scheduling : Queues are used to schedule tasks in various applications, ensuring that tasks are processed in the order they are received.

  • Breadth-First Search : In graph algorithms, queues are employed in breadth-first search (BFS) to explore nodes level by level.

  • Print Queue in Printers : Printers use queues to manage print jobs, ensuring that documents are printed in the order they are sent.

  • Buffer Management : Queues are utilized in buffer management to control the flow of data between processes with different rates of production and consumption.

  • Call Center Systems : Queues are employed in call centers to manage incoming calls, ensuring that calls are handled in the order they are received.

Advantages and Limitations of Queues

Advantages
  • Order Preservation : Queues maintain the order in which elements are added, ensuring that the first element added is the first one to be removed.

  • Efficient for Task Scheduling : Queues are efficient for managing tasks, ensuring that tasks are processed in the order they are received.
Limitations
  • Fixed Size in Array Implementation : Array-based implementations may have a fixed size, leading to potential overflow conditions.

  • Memory Overhead in Linked List Implementation : Linked list-based implementations may have additional memory overhead due to node pointers.

Representation of a Queue

A Queue can be implemented using arrays and linked lists. In this tutorial, we will use arrays to implement the Queue. We will define a structure for the Queue that will have two variables: front and rear. The front variable will hold the index of the first element in the Queue, and the rear variable will hold the index of the last element in the Queue.

struct Queue {
    int front, rear;
    int arr[MAXSIZE];
};
MAXSIZE is the maximum size of the Queue. It is the maximum number of elements a queue can store.


Here's the Step-by-Step Implementation of the Common Queue Operations


Initialize : To Initialize a Queue

Before using the Queue, we need to initialize it. We will set the front and rear variables to -1, which will indicate that the Queue is empty.

void initializeQueue(struct Queue* q) {
    q->front = -1;
    q->rear = -1;
}

isEmpty : Check if the Queue is Empty or Not

To check if the Queue is empty, we will check if the front variable is equal to -1. If it is, then the Queue is empty.

int isEmpty(struct Queue* q) {
    return q->front == -1;
}

isFull : Check if the Queue is Full

To check if the Queue is full, we will check if the rear variable is equal to MAXSIZE-1. If it is, then the Queue is full.

int isFull(struct Queue* q) {
    return q->rear == MAXSIZE-1;
}

enqueue : Add an Element to the Queue

To add an element to the Queue, we will first check if the Queue is full. If it is, we cannot add any more elements. If the Queue is not full, we will increment the rear variable and add the new element to the Queue.

void enqueue(struct Queue* q, int x) {
    if (isFull(q)) {
        printf("Queue is full.\n");
        return;
    }
    q->rear++;
    q->arr[q->rear] = x;
    if (q->front == -1) {
        q->front = 0;
    }
}

dequeue : Remove an Element from the Queue

To remove an element from the Queue, we will first check if the Queue is empty. If it is, we cannot remove any element. If the Queue is not empty, we will remove the element at the front of the Queue and increment the front variable.

int dequeue(struct Queue* q) {
    if (isEmpty(q)) {
        printf("Queue is empty.\n");
        return -1;
    }
    int x = q->arr[q->front];
    q->front++;
    if (q->front > q->rear) {
        q->front = q->rear = -1;
    }
    return x;
}

printQueue : Display the Elements of the Queue

To display the elements of the Queue, we will loop through the Queue and print the elements.

void display(struct Queue * q) {
    if (isEmpty(q)) {
        printf("Queue is empty.\n");
    } else {
        printf("Elements of Queue are: ");
        for (int i = q - > front; i <= q - > rear; i++) {
            printf("%d ", q - > arr[i]);
        }
        printf("\n");
    }
}

Array Implementation of Queue Data Structure

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

#define MAXSIZE 5

struct Queue {
    int front, rear;
    int arr[MAXSIZE];
};

void initializeQueue(struct Queue * q) {
    q->front = -1;
    q->rear = -1;
}

int isEmpty(struct Queue * q) {
    return q->front == -1;
}

int isFull(struct Queue * q) {
    return q->rear == MAXSIZE - 1;
}

void enqueue(struct Queue * q, int x) {
    if (isFull(q)) {
        printf("Queue is full.\n");
        return;
    }
    q->rear++;
    q->arr[q->rear] = x;
    if (q->front == -1) {
        q->front = 0;
    }
}

int dequeue(struct Queue * q) {
    if (isEmpty(q)) {
        printf("Queue is empty.\n");
        return -1;
    }
    int x = q->arr[q->front];
    q->front++;
    if (q->front > q->rear) {
        q->front = q->rear = -1;
    }
    return x;
}

void display(struct Queue * q) {
    if (isEmpty(q)) {
        printf("Queue is empty.\n");
    } else {
        printf("Elements of Queue are: ");
        for (int i = q->front; i <= q->rear; i++) {
            printf("%d ", q->arr[i]);
        }
        printf("\n");
    }
}

int main() {
    struct Queue q;
    
    initializeQueue(&q);
    
    enqueue(&q, 10);
    enqueue(&q, 20);
    enqueue(&q, 30);
    enqueue(&q, 40);
    enqueue(&q, 50);
    enqueue(&q, 60); // Queue is full.

    // Elements of Queue are: 10 20 30 40 50
    display(&q); 

    // Dequeued element: 10
    printf("Dequeued element: %d\n", dequeue(&q)); 
    // Dequeued element: 20
    printf("Dequeued element: %d\n", dequeue(&q)); 

    display(&q); // Elements of Queue are: 30 40 50

    return 0;
}
Output
Queue is full.
Elements of Queue are: 10 20 30 40 50 
Dequeued element: 10
Dequeued element: 20
Elements of Queue are: 30 40 50

In the above program, we have defined a Queue with a maximum size of 5 elements. We have added 6 elements to the Queue, and as the Queue is full, we cannot add any more elements. We have then displayed the elements of the Queue, and we have dequeued the first two elements. We have again displayed the elements of the Queue.


Best Practices of Using Queue Data Structure

  • Choose the Right Implementation : While queues are efficient for scenarios where the order of processing is crucial, it's essential to choose the right data structure based on the specific requirements of the application. If constant-time access and a fixed size are crucial, arrays may be more appropriate. On the other hand, if dynamic resizing and flexibility are required, linked lists might be a better choice.

  • Check for Underflow and Overflow : Always check for underflow (dequeueing from an empty queue) and overflow (enqueuing to a full queue) conditions to prevent runtime errors.

  • Use Exception Handling : Implement exception handling to gracefully manage errors such as queue underflow or overflow, providing informative error messages.

  • Clear Unused References : If using a linked list-based implementation, ensure that unused references are properly cleared to avoid memory leaks.

  • Document Assumptions and Constraints : Clearly document any assumptions or constraints related to queue usage, such as expected data types or potential modifications.

Conclusion

The queue data structure, with its First In, First Out (FIFO) principle, is a vital component in various algorithms and real-world applications. By understanding its definition, basic operations, implementation techniques, and best practices, developers can leverage the full potential of queues in their programming endeavors. Whether managing tasks, exploring graphs, or handling print jobs, the queue proves to be a versatile and indispensable tool in the world of computer science and software development.