Circular Queue in Data Structure: Overview, Types & Implementation

Updated on July 29, 2024

Article Outline

Circular queues have a very important role in data structures since they offer a solution to various problems that are associated with linear queues. While in a linear queue, the first and last points are distinct; it is not the same for a circular queue since the last point is linked to the first, forming a circular formation. This approach is beneficial since it aids in avoiding memory waste and further improves the efficiency of the queue. But first, what is a circular queue? And how does this kind of data structure actually function?

 

A circular queue in the data structure can also be described as a queue that works based on the FIFO (First In First Out) concept. The primary dissimilarity is that it creates a circle, which means there is a link between the first and the last. This connection enables the use of the space that once housed the elements that have been removed from the deque. This reuse is quite important in scenarios where resources must be utilised infinitely and in a cyclical manner, such as in CPU allocation or memory allocation.

The Need for Circular Queues and Their Importance

Linear queues have a significant drawback: they can lead to memory wastage if frequently used by the CPU in processing the data. Because of the above, when the rear of the queue reaches the maximum size, there can always be spaces in the front due to the earlier dequeued elements. Nevertheless, such spaces cannot be reused, which compromises the efficient use of memory in programs and applications.

 

This is solved by a circular queue in the data structure where the rear of the queue directly correlates with the front by moving to the beginning of the queue. This wrapping around lets us use all the space significantly for our needs. In this way, circular queues are useful in managing memory effectively as well as achieving enhanced performance in different systems.

 

For instance, we may require dealing with tasks on a round-robin basis. A circular queue is preferred since it is possible to cycle through the given tasks without reaching the end and running out of further space. This feature is essential in cases like CPU scheduling, where processes are worked in cyclic manners.

*Image
Get curriculum highlights, career paths, industry insights and accelerate your technology journey.
Download brochure

Properties and Characteristics of Circular Queues

A circular queue in a data structure has several distinct properties and characteristics that make it efficient:

 

  • Circular Nature: The most relevant characteristic is that the last element links to the first, creating a cycle.

 

  • FIFO Principle: Circular queues also operate under the FIFO principle, in which the first element to be added is the first element to be deleted.

 

  • Queue Pointers: Two pointers, the front, and the rear, are used to indicate the start of the queue and the end of the queue, respectively. These pointers move around circularly within the queue.

 

  • Efficient Memory Usage: The circular queues are efficient since they avoid the wastage of memory as the empty spaces are utilised again.

For a better understanding of these properties, there is now a need to look at how the circular increment is implemented. Whenever we increase the rear pointer, and it gets to the last position of the list, it circles around to the first position. This is done using the modulo operation and is used to ensure that the pointer never goes beyond the actual size of the queue.

Detailed Explanation of Circular Queue Operations

It is vital to understand how the circular queue in data structure works before one can implement it or use it in a program. The major functions are enqueue, which stands for adding an element and dequeue, which is the process of deleting an element. It is important to dig deeper into these operations.

Queue Pointers: Front and Rear

In a circular queue, two pointers are used: In front and at the back. Specifically, the front pointer points to the first element, and the rear pointer points to the last element. To start with, both indicators are initialised to -1 as a signal that the queue is still empty.

EnQueue Operation: Steps to Insert an Element

  1. Check if the Queue is Full: First, we start by checking whether the queue is full or not. This condition is satisfied when the first position to the right of the rear pointer is the front pointer. In the equation, if (rear + 1) % size is equal to front, this indicates that the queue is full and we are not able to insert a new element.
  2. Insert the First Element: If the number of elements in the queue is 0 (front and rear both pointing to -1), we put both of them to point to the first element and insert the element at that position.
  3. Insert Subsequent Elements: For other elements, we increase the rear using the expression (rear + 1) % size and place the element at this index.
  4. Handle Wrapping Around: The rear pointer has also been made to traverse circularly because of the modulo operation when it has reached the final position in the queue.

Here’s a simplified code snippet for the enQueue operation in Python:

def enQueue(self, data): if (self.rear + 1) % self.size == self.front: print(“Queue is full”) elif self.front == -1: self.front = 0 self.rear = 0 self.queue[self.rear] = data else: self.rear = (self.rear + 1) % self.size self.queue[self.rear] = data

DeQueue Operation: Steps to Remove an Element

  1. Check if the Queue is Empty: First of all, we need to determine if there are any elements in the queue. This condition is true when both front and rear pointers are -1.
  2. Retrieve the Element: If the queue is not empty, the element in the front position of the queue is removed.
  3. Handle Single Element: If the current front is the only element in the queue, then the front and rear pointers are set to -1 to indicate that the queue is empty again.
  4. Increment the Front Pointer: For other cases, we advance the front pointer to (front + 1) % size, as explained earlier. This step makes sure that if the front pointer is pointing to the last element of the queue it will scroll back to the first element.

Here’s a simplified code snippet for the deQueue operation in Python:

def deQueue(self): if self.front == -1: print("Queue is empty") elif self.front == self.rear: temp = self.queue[self.front] self.front = -1 self.rear = -1 return temp else: temp = self.queue[self.front] self.front = (self.front + 1) % self.size return temp

Handling Special Cases: Full and Empty Queues

We must carefully manage the unique situations of full and empty lines in a circular queue:

 

  • Full Queue: When the front pointer is in the same location as the rear pointer, this circumstance arises. In this instance, we are unable to add another element.
  • Empty Queue: When the front and rear pointers are both -1, this circumstance happens. In this scenario, we are unable to delete an element.

Step-by-Step Guide to Implementing Circular Queue Using Arrays

Implementing a circular queue in data structure using arrays is straightforward and efficient. We can achieve this by following a series of steps to ensure proper functionality and memory utilisation. Here’s a detailed guide to help us implement a circular queue using arrays.

Initial Setup: Array Declaration and Pointer Initialization

First, we need to declare an array and initialise the front and rear pointers. These pointers help us keep track of the start and end of the queue.

 

  • Array Declaration: Define an array of a fixed size to hold the elements of the queue.
  • Pointer Initialization: Set the front and rear pointers to -1, indicating that the queue is initially empty.
class CircularQueue: def __init__(self, size): self.size = size self.queue = [None] * size self.front = -1 self.rear = -1

EnQueue Operation in Arrays: Detailed Steps and Code Example

The enQueue operation inserts an element at the rear of the queue. Let’s break down the steps and provide a code example:

 

  1. Check if the Queue is Full: Ensure there is space to insert a new element. If the position next to the rear is the front, the queue is full.
  2. Insert the First Element: If the queue is empty, set both the front and rear pointers to 0 and insert the element.
  3. Insert Subsequent Elements: Increment the rear pointer and insert the new element at the new position.
  4. Handle Wrapping Around: Use the modulo operator to wrap around if needed.
def enQueue(self, data): if (self.rear + 1) % self.size == self.front: print("Queue is full") elif self.front == -1: self.front = 0 self.rear = 0 self.queue[self.rear] = data else: self.rear = (self.rear + 1) % self.size self.queue[self.rear] = data

DeQueue Operation in Arrays: Detailed Steps and Code Example

The deQueue operation removes an element from the front of the queue. Here are the steps and the corresponding code:

 

  1. Check if the Queue is Empty: Ensure there are elements to remove. If both the front and rear are -1, the queue is empty.
  2. Retrieve the Element: Store the element at the front before removing it.
  3. Handle Single Element: If the queue has only one element, reset both pointers to -1.
  4. Increment the Front Pointer: Move the front pointer forward to the next element.
def deQueue(self): if self.front == -1: print("Queue is empty") elif self.front == self.rear: temp = self.queue[self.front] self.front = -1 self.rear = -1 return temp else: temp = self.queue[self.front] self.front = (self.front + 1) % self.size return temp

Handling Circular Increment Using Modulo Operator

The modulo operator is essential for controlling the queue’s circular structure. The modulo operation returns the rear or front pointer to the beginning when it reaches the end of the array after being incremented. This process guarantees that the pointers remain inside the array’s boundaries.

Complete Code Example with Comments

class CircularQueue: def __init__(self, size): self.size = size self.queue = [None] * size self.front = -1 self.rear = -1 def enQueue(self, data): # Check if the queue is full if (self.rear + 1) % self.size == self.front: print("Queue is full") # Check if the queue is empty elif self.front == -1: self.front = 0 self.rear = 0 self.queue[self.rear] = data else: # Increment rear pointer and add the element self.rear = (self.rear + 1) % self.size self.queue[self.rear] = data def deQueue(self): # Check if the queue is empty if self.front == -1: print("Queue is empty") elif self.front == self.rear: # Only one element in the queue temp = self.queue[self.front] self.front = -1 self.rear = -1 return temp else: # Increment front pointer and remove the element temp = self.queue[self.front] self.front = (self.front + 1) % self.size return temp def display(self): # Check if the queue is empty if self.front == -1: print("Queue is empty") elif self.rear >= self.front: print("Elements in the circular queue are:", end=" ") for i in range(self.front, self.rear + 1): print(self.queue[i], end=" ") print() else: print("Elements in Circular Queue are:", end=" ") for i in range(self.front, self.size): print(self.queue[i], end=" ") for i in range(0, self.rear + 1): print(self.queue[i], end=" ") print()

# Example usage:

  • cq = CircularQueue(5)
  • cq.enQueue(1)
  • cq.enQueue(2)
  • cq.enQueue(3)
  • cq.enQueue(4)
  • cq.enQueue(5)
  • cq.display()
  • cq.deQueue()
  • cq.display()
  • cq.enQueue(6)
  • cq.display()

Step-by-Step Guide to Implementing Circular Queue Using Linked Lists

Implementing a circular queue in data structure using linked lists offers flexibility as the size can dynamically adjust. We use nodes instead of fixed-size arrays. Let’s explore this implementation step by step.

Initial Setup: Node Structure and Pointer Initialization

First, we define the structure of a node and initialise the front and rear pointers.

 

  • Node Structure: Each node contains data and a reference to the next node.
  • Pointer Initialization: Set both front and rear pointers to None, indicating an empty queue.
class Node: def __init__(self, data): self.data = data self.next = None class CircularQueueLinkedList: def __init__(self): self.front = None self.rear = None

EnQueue Operation in Linked Lists: Detailed Steps and Code Example

The enQueue operation inserts an element at the rear of the queue. Here are the steps:

 

  1. Create a New Node: Allocate memory for a new node and set its data.
  2. Check if the Queue is Empty: If the queue is empty, set both front and rear pointers to the new node.
  3. Insert at Rear: If the queue is not empty, link the new node at the end and update the rear pointer.
  4. Maintain Circular Structure: Ensure the rear node points to the front node.
def enQueue(self, data): new_node = Node(data) if self.front is None: self.front = self.rear = new_node self.rear.next = self.front else: self.rear.next = new_node self.rear = new_node self.rear.next = self.front

DeQueue Operation in Linked Lists: Detailed Steps and Code Example

The deQueue operation removes an element from the front. Here are the steps:

 

  1. Check if the Queue is Empty: If the queue is empty, return an error message.
  2. Retrieve the Element: Store the data of the front node before removing it.
  3. Handle Single Element: If the queue has only one node, reset both pointers to None.
  4. Update Front Pointer: Move the front pointer to the next node and update the rear pointer’s next reference.
def deQueue(self): if self.front is None: print("Queue is empty") elif self.front == self.rear: temp = self.front self.front = self.rear = None return temp.data else: temp = self.front self.front = self.front.next self.rear.next = self.front return temp.data

Handling Circular Increment Using Modulo Operator

Managing the circular nature of the queue requires the use of the modulo operator. The modulo operation circles back to the beginning of the array when the front or rear pointer is incremented and reaches its conclusion. The pointers are kept inside the array’s boundaries thanks to this technique.

Complete Code Example with Comments

class Node: def __init__(self, data): self.data = data self.next = None class CircularQueueLinkedList: def __init__(self): self.front = None self.rear = None def enQueue(self, data): new_node = Node(data) if self.front is None: # If the queue is empty, both front and rear point to the new node self.front = self.rear = new_node self.rear.next = self.front else: # Add the new node at the end of the queue and adjust the rear pointer self.rear.next = new_node self.rear = new_node self.rear.next = self.front def deQueue(self): if self.front is None: print("Queue is empty") elif self.front == self.rear: # Only one element in the queue temp = self.front self.front = None self.rear = None return temp.data else: # Remove the front element and adjust the front pointer temp = self.front self.front = self.front.next self.rear.next = self.front return temp.data def display(self): if self.front is None: print("Queue is empty") return temp = self.front print("Elements in the circular queue are:", end=" ") while temp.next != self.front: print(temp.data, end=" ") temp = temp.next print(temp.data)

# Example usage:

  • cq = CircularQueueLinkedList()
  • cq.enQueue(1)
  • cq.enQueue(2)
  • cq.enQueue(3)
  • cq.enQueue(4)
  • cq.enQueue(5)
  • cq.display()
  • cq.deQueue()
  • cq.display()
  • cq.enQueue(6)
  • cq.display()

In-depth Analysis of Time Complexity in Circular Queues

Understanding the time complexity of operations in a circular queue helps us evaluate its efficiency. Both enQueue and deQueue operations have a constant time complexity, O(1), meaning they take a fixed amount of time regardless of the number of elements.

 

EnQueue Operation

 

  • Time Complexity: O(1)
  • Reason: Insertion involves a few pointer updates, and a simple condition check.

DeQueue Operation

  • Time Complexity: O(1)
  • Reason: Removal involves updating pointers and retrieving the front element.

Space Complexity Considerations

  • Array Implementation: Space complexity is O(n), where n is the size of the array.
  • Linked List Implementation: Space complexity depends on the number of elements, also O(n).

Practical Applications of Circular Queues in Real-World Systems

A circular queue in data structure has numerous applications in various systems due to its efficiency and cyclic nature. Let’s explore some common applications.

Memory Management in Operating Systems

Circular queues are widely used in memory management. Operating systems use them to manage processes in a round-robin fashion. By cycling through processes, the system ensures fair resource allocation without wasting memory.

CPU Scheduling Using Round-Robin Algorithm

The round-robin scheduling algorithm employs circular queues to manage process scheduling. Each process gets an equal share of CPU time. Once a process completes its time slice, it moves to the back of the queue, making room for the next process. This approach ensures all processes get fair CPU time.

Traffic Light Management in Computer-Controlled Systems

Traffic management systems often use circular queues to control traffic lights. Each light gets a fixed time interval to stay green, and then the control moves to the next light. This cycle repeats continuously, ensuring smooth traffic flow.

Use Cases in Networking and Buffer Management

A circular queue in the data structure is essential in networking, particularly in buffering data. It helps manage the flow of packets, ensuring efficient transmission and reception. For example, network routers use circular queues to handle data packets in a cyclic order, preventing overflow and ensuring smooth data flow.

 

By understanding these applications, we see how circular queues play a crucial role in enhancing system performance and efficiency. From operating systems to traffic management, their impact is widespread and significant.

Advantages and Disadvantages of Circular Queues

A circular queue in data structure offers numerous benefits, but it also comes with some drawbacks. Understanding both sides helps us make informed decisions about their use.

Advantages of Circular Queues

  1. Efficient Memory Usage: Circular queues make the best use of available space by reusing previously occupied positions.
  2. Constant Time Complexity: Both enQueue and deQueue operations run in O(1) time, making them highly efficient.
  3. Elimination of Memory Wastage: By connecting the end of the queue to the beginning, circular queues prevent memory wastage common in linear queues.
  4. Simplified Buffer Management: Circular queues are ideal for buffer management in various systems, ensuring smooth data flow.

Disadvantages of Circular Queues

  1. Implementation Complexity: Implementing circular queues can be more complex than linear queues due to the need to manage circular pointer increments.
  2. Pointer Management: Proper handling of front and rear pointers is crucial to avoid errors, which can be challenging.
  3. Fixed Size Limitation: When implemented using arrays, circular queues have a fixed size, which might not be suitable for all applications.

Conclusion

Circular queues offer a robust solution to the limitations of linear queues. By connecting the last element back to the first, we can efficiently use available memory and enhance performance in various applications. Understanding the properties, operations, and implementations of circular queues helps us leverage their benefits in real-world scenarios. Whether it’s for CPU scheduling, memory management, or traffic systems, circular queues play a crucial role. Implementing them using arrays or linked lists provides flexibility and adaptability to meet different needs. Embracing circular queues in our data structures toolkit can significantly improve our systems’ efficiency and reliability.

FAQs
The primary advantage of a circular queue in the data structure is its efficient use of memory. Unlike a linear queue, it reuses empty spaces created by dequeued elements, preventing memory wastage and ensuring continuous operation.
A circular queue in the data structure connects the last element back to the first. When the rear pointer reaches the end, it wraps around to the beginning, allowing the reuse of previously occupied spaces. This wrapping around optimises memory usage and avoids unnecessary memory wastage.
Yes, circular queues are commonly used in CPU scheduling with the round-robin algorithm. They manage processes in a cyclic order, ensuring each process gets a fair share of CPU time without wasting memory.
Both EnQueue and DeQueue operations in circular queues have a time complexity of O(1). This constant time complexity ensures efficient insertion and removal of elements.
In array implementations, the modulo operator helps in wrapping around the queue. When the rear pointer reaches the end of the array, ‘(rear + 1) % size’ resets the rear to the beginning of the array, ensuring a continuous circular structure.

Updated on July 29, 2024

Link
left dot patternright dot pattern

Programs tailored for your success

Popular

IIT Courses

Management

Data Science

Finance

Technology

Future Tech

Upskill with expert articles

View all
Hero Vired logo
Hero Vired is a leading LearnTech company dedicated to offering cutting-edge programs in collaboration with top-tier global institutions. As part of the esteemed Hero Group, we are committed to revolutionizing the skill development landscape in India. Our programs, delivered by industry experts, are designed to empower professionals and students with the skills they need to thrive in today’s competitive job market.
Blogs
Reviews
Events
In the News
About Us
Contact us
Learning Hub
18003093939     ·     hello@herovired.com     ·    Whatsapp
Privacy policy and Terms of use

|

Sitemap

© 2024 Hero Vired. All rights reserved