How to efficiently implement k Queues in a single array?
Last Updated :
02 May, 2023
Introduction :
One efficient way to implement k queues in a single array is to use a technique called “circular array implementation of k queues.” This approach uses a single array to store elements for all k queues, and it divides the array into k segments, one for each queue.
To implement this approach, we need to keep track of two pointers for each queue: a front pointer and a rear pointer. These pointers will indicate the start and end of the segment in the array for each queue. We also need to keep track of the size of each segment to know how many elements are currently in each queue.
To enqueue an element into a particular queue, we need to check if there is any space left in the segment for that queue. If there is space, we can add the element to the end of the segment and update the rear pointer for that queue. If there is no space, we need to return an error or resize the segment (if possible).
To dequeue an element from a particular queue, we need to check if there are any elements in that queue. If there are elements, we can remove the first element in the segment and update the front pointer for that queue. If there are no elements, we need to return an error.
To implement this approach efficiently, we can use a circular array to avoid wasting space. This means that if we reach the end of a segment, we wrap around to the beginning of the segment, effectively treating the array as a circle
We have discussed efficient implementation of k stack in an array. In this post, same for queue is discussed. Following is the detailed problem statement.
Create a data structure kQueues that represents k queues. Implementation of kQueues should use only one array, i.e., k queues should use the same array for storing elements. Following functions must be supported by kQueues.
- enqueue(int x, int qn) –> adds x to queue number ‘qn’ where qn is from 0 to k-1
- dequeue(int qn) –> deletes an element from queue number ‘qn’ where qn is from 0 to k-1
Method 1 (Divide the array in slots of size n/k):
A simple way to implement k queues is to divide the array in k slots of size n/k each, and fix the slots for different queues, i.e., use arr[0] to arr[n/k-1] for the first queue, and arr[n/k] to arr[2n/k-1] for queue2 where arr[] is the array to be used to implement two queues and size of array be n.
The problem with this method is an inefficient use of array space. An enqueue operation may result in overflow even if there is space available in arr[]. For example, consider k as 2 and array size n as 6. Let we enqueue 3 elements to first and do not enqueue anything to the second queue. When we enqueue the 4th element to the first queue, there will be overflow even if we have space for 3 more elements in the array.
Method 2 (A space efficient implementation):
The idea is similar to the stack post, here we need to use three extra arrays. In stack post, we needed two extra arrays, one more array is required because in queues, enqueue() and dequeue() operations are done at different ends.
Following are the three extra arrays are used:
- front[]: This is of size k and stores indexes of front elements in all queues.
- rear[]: This is of size k and stores indexes of rear elements in all queues.
- next[]: This is of size n and stores indexes of next item for all items in array arr[].
Here arr[] is the actual array that stores k stacks.
Together with k queues, a stack of free slots in arr[] is also maintained. The top of this stack is stored in a variable ‘free’.
All entries in front[] are initialized as -1 to indicate that all queues are empty. All entries next[i] are initialized as i+1 because all slots are free initially and pointing to the next slot. Top of the free stack, ‘free’ is initialized as 0.
Following is implementation of the above idea.
C++
#include<iostream>
#include<climits>
using namespace std;
class kQueues
{
int *arr;
int *front;
int *rear;
int *next;
int n, k;
int free ;
public :
kQueues( int k, int n);
bool isFull() { return ( free == -1); }
void enqueue( int item, int qn);
int dequeue( int qn);
bool isEmpty( int qn) { return (front[qn] == -1); }
};
kQueues::kQueues( int k1, int n1)
{
k = k1, n = n1;
arr = new int [n];
front = new int [k];
rear = new int [k];
next = new int [n];
for ( int i = 0; i < k; i++)
front[i] = -1;
free = 0;
for ( int i=0; i<n-1; i++)
next[i] = i+1;
next[n-1] = -1;
}
void kQueues::enqueue( int item, int qn)
{
if (isFull())
{
cout << "\nQueue Overflow\n" ;
return ;
}
int i = free ;
free = next[i];
if (isEmpty(qn))
front[qn] = i;
else
next[rear[qn]] = i;
next[i] = -1;
rear[qn] = i;
arr[i] = item;
}
int kQueues::dequeue( int qn)
{
if (isEmpty(qn))
{
cout << "\nQueue Underflow\n" ;
return INT_MAX;
}
int i = front[qn];
front[qn] = next[i];
next[i] = free ;
free = i;
return arr[i];
}
int main()
{
int k = 3, n = 10;
kQueues ks(k, n);
ks.enqueue(15, 2);
ks.enqueue(45, 2);
ks.enqueue(17, 1);
ks.enqueue(49, 1);
ks.enqueue(39, 1);
ks.enqueue(11, 0);
ks.enqueue(9, 0);
ks.enqueue(7, 0);
cout << "Dequeued element from queue 2 is " << ks.dequeue(2) << endl;
cout << "Dequeued element from queue 1 is " << ks.dequeue(1) << endl;
cout << "Dequeued element from queue 0 is " << ks.dequeue(0) << endl;
return 0;
}
|
Java
public class KQueues {
int k;
int n;
int [] arr;
int [] front;
int [] rear;
int [] next;
int free;
KQueues( int k, int n){
this .k = k;
this .n = n;
this .arr = new int [n];
this .front = new int [k];
this .rear = new int [k];
this .next = new int [n];
for ( int i= 0 ; i< k; i++) {
front[i] = rear[i] = - 1 ;
}
free = 0 ;
for ( int i= 0 ; i< n- 1 ; i++) {
next[i] = i+ 1 ;
}
next[n- 1 ] = - 1 ;
}
public static void main(String[] args)
{
int k = 3 , n = 10 ;
KQueues ks= new KQueues(k, n);
ks.enqueue( 15 , 2 );
ks.enqueue( 45 , 2 );
ks.enqueue( 17 , 1 );
ks.enqueue( 49 , 1 );
ks.enqueue( 39 , 1 );
ks.enqueue( 11 , 0 );
ks.enqueue( 9 , 0 );
ks.enqueue( 7 , 0 );
System.out.println( "Dequeued element from queue 2 is " +
ks.dequeue( 2 ));
System.out.println( "Dequeued element from queue 1 is " +
ks.dequeue( 1 ));
System.out.println( "Dequeued element from queue 0 is " +
ks.dequeue( 0 ) );
}
private boolean isEmpty( int i) {
return front[i] == - 1 ;
}
private boolean isFull( int i) {
return free == - 1 ;
}
private void enqueue( int item, int j) {
if (isFull(j)) {
System.out.println( "queue overflow" );
return ;
}
int nextFree = next[free];
if (isEmpty(j)) {
rear[j] = front[j] = free;
} else {
next[rear[j]] = free;
rear[j] = free;
}
next[free] = - 1 ;
arr[free] = item;
free = nextFree;
}
private int dequeue( int i) {
if (isEmpty(i)) {
System.out.println( "Stack underflow" );
return Integer.MIN_VALUE;
}
int frontIndex = front[i];
front[i] = next[frontIndex];
next[frontIndex] = free;
free = frontIndex;
return arr[frontIndex];
}
}
|
Python3
class KQueues:
def __init__( self , number_of_queues, array_length):
self .number_of_queues = number_of_queues
self .array_length = array_length
self .array = [ - 1 ] * array_length
self .front = [ - 1 ] * number_of_queues
self .rear = [ - 1 ] * number_of_queues
self .next_array = list ( range ( 1 , array_length))
self .next_array.append( - 1 )
self .free = 0
def is_empty( self , queue_number):
return True if self .front[queue_number] = = - 1 else False
def is_full( self , queue_number):
return True if self .free = = - 1 else False
def enqueue( self , item, queue_number):
if self .is_full(queue_number):
print ( "Queue FULL" )
return
next_free = self .next_array[ self .free]
if self .is_empty(queue_number):
self .front[queue_number] = self .rear[queue_number] = self .free
else :
self .next_array[ self .rear[queue_number]] = self .free
self .rear[queue_number] = self .free
self .next_array[ self .free] = - 1
self .array[ self .free] = item
self .free = next_free
def dequeue( self , queue_number):
if self .is_empty(queue_number):
print ( "Queue EMPTY" )
return
front_index = self .front[queue_number]
self .front[queue_number] = self .next_array[front_index]
self .next_array[front_index] = self .free
self .free = front_index
return self .array[front_index]
if __name__ = = "__main__" :
ks = KQueues( 3 , 10 )
ks.enqueue( 15 , 2 )
ks.enqueue( 45 , 2 )
ks.enqueue( 17 , 1 );
ks.enqueue( 49 , 1 );
ks.enqueue( 39 , 1 );
ks.enqueue( 11 , 0 );
ks.enqueue( 9 , 0 );
ks.enqueue( 7 , 0 );
print ( "Dequeued element from queue 2 is {}" . format (ks.dequeue( 2 )))
print ( "Dequeued element from queue 1 is {}" . format (ks.dequeue( 1 )))
print ( "Dequeued element from queue 0 is {}" . format (ks.dequeue( 0 )))
|
C#
using System;
public class KQueues
{
int k;
int n;
int [] arr;
int [] front;
int [] rear;
int [] next;
int free;
KQueues( int k, int n)
{
this .k = k;
this .n = n;
this .arr = new int [n];
this .front = new int [k];
this .rear = new int [k];
this .next = new int [n];
for ( int i = 0; i < k; i++)
{
front[i] = rear[i] = -1;
}
free = 0;
for ( int i = 0; i < n - 1; i++)
{
next[i] = i + 1;
}
next[n - 1] = -1;
}
public static void Main(String[] args)
{
int k = 3, n = 10;
KQueues ks = new KQueues(k, n);
ks.enqueue(15, 2);
ks.enqueue(45, 2);
ks.enqueue(17, 1);
ks.enqueue(49, 1);
ks.enqueue(39, 1);
ks.enqueue(11, 0);
ks.enqueue(9, 0);
ks.enqueue(7, 0);
Console.WriteLine( "Dequeued element from queue 2 is " +
ks.dequeue(2));
Console.WriteLine( "Dequeued element from queue 1 is " +
ks.dequeue(1));
Console.WriteLine( "Dequeued element from queue 0 is " +
ks.dequeue(0) );
}
private bool isEmpty( int i)
{
return front[i] == -1;
}
private bool isFull( int i)
{
return free == -1;
}
private void enqueue( int item, int j)
{
if (isFull(j))
{
Console.WriteLine( "queue overflow" );
return ;
}
int nextFree = next[free];
if (isEmpty(j))
{
rear[j] = front[j] = free;
}
else
{
next[rear[j]] = free;
rear[j] = free;
}
next[free] = -1;
arr[free] = item;
free = nextFree;
}
private int dequeue( int i)
{
if (isEmpty(i))
{
Console.WriteLine( "Stack underflow" );
return int .MinValue;
}
int frontIndex = front[i];
front[i] = next[frontIndex];
next[frontIndex] = free;
free = frontIndex;
return arr[frontIndex];
}
}
|
Javascript
<script>
class KQueues
{
constructor(k,n)
{
this .k = k;
this .n = n;
this .arr = new Array(n);
this .front = new Array(k);
this .rear = new Array(k);
this .next = new Array(n);
for (let i= 0; i< k; i++) {
this .front[i] = this .rear[i] = -1;
}
this .free = 0;
for (let i= 0; i< n-1; i++) {
this .next[i] = i+1;
}
this .next[n-1] = -1;
}
isEmpty(i)
{
return this .front[i] == -1;
}
isFull(i)
{
return this .free == -1;
}
enqueue(item,j)
{
if ( this .isFull(j)) {
document.write( "queue overflow<br>" );
return ;
}
let nextFree = this .next[ this .free];
if ( this .isEmpty(j)) {
this .rear[j] = this .front[j] = this .free;
} else {
this .next[ this .rear[j]] = this .free;
this .rear[j] = this .free;
}
this .next[ this .free] = -1;
this .arr[ this .free] = item;
this .free = nextFree;
}
dequeue(i)
{
if ( this .isEmpty(i)) {
document.write( "Stack underflow<br>" );
return Number.MIN_VALUE;
}
let frontIndex = this .front[i];
this .front[i] = this .next[frontIndex];
this .next[frontIndex] = this .free;
this .free = frontIndex;
return this .arr[frontIndex];
}
}
let k = 3, n = 10;
let ks= new KQueues(k, n);
ks.enqueue(15, 2);
ks.enqueue(45, 2);
ks.enqueue(17, 1);
ks.enqueue(49, 1);
ks.enqueue(39, 1);
ks.enqueue(11, 0);
ks.enqueue(9, 0);
ks.enqueue(7, 0);
document.write( "Dequeued element from queue 2 is " +
ks.dequeue(2)+ "<br>" );
document.write( "Dequeued element from queue 1 is " +
ks.dequeue(1)+ "<br>" );
document.write( "Dequeued element from queue 0 is " +
ks.dequeue(0)+ "<br>" );
</script>
|
Output
Dequeued element from queue 2 is 15
Dequeued element from queue 1 is 17
Dequeued element from queue 0 is 11
Time complexities of enqueue() and dequeue() is O(1).
The best part of the above implementation is, if there is a slot available in the queue, then an item can be enqueued in any of the queues, i.e., no wastage of space. This method requires some extra space. Space may not be an issue because queue items are typically large, for example, queues of employees, students, etc where every item is of hundreds of bytes. For such large queues, the extra space used is comparatively very less as we use three integer arrays as extra space.
Issuses in efficiently implement k Queues in a single array :
While the circular array implementation of k queues is an efficient way to implement multiple queues in a single array, there are several issues that need to be considered to ensure that the implementation is correct and efficient.
- Size allocation: One issue is deciding how to allocate the size of each queue segment in the array. If the size of one queue segment is too small, that queue may fill up quickly, causing a lot of unnecessary resizing and memory allocation. On the other hand, if the size of one queue segment is too large, there may be a lot of wasted space in the array.
- Overflow/underflow: Another issue is handling overflow and underflow. If the array becomes full, there will be no space to enqueue elements, and if the array becomes empty, there will be no elements left to dequeue. It is important to handle these cases properly to avoid errors or unexpected behavior.
- Tracking size: To properly implement the k queues in a single array, we need to keep track of the size of each queue segment to know how many elements are currently in each queue. This can add overhead to the implementation, as we need to update the size of each segment whenever we enqueue or dequeue an element.
- Implementation complexity: Finally, the circular array implementation of k queues can be more complex to implement and maintain than a simpler implementation using separate arrays for each queue. This is because we need to keep track of multiple pointers and manage the circular nature of the array.
Examples of Queues in a single array :
- Multi-Threaded Programming: In multi-threaded programming, where multiple threads need to access shared resources in a concurrent manner, a circular array implementation of k queues can be used to implement a thread-safe data structure. Each thread can access a particular queue, and the queues can be managed in a thread-safe manner.
- Resource Management: In a resource management system, such as a job scheduler or task manager, queues can be used to manage resources efficiently. Using a single array to implement multiple queues allows efficient management of multiple resources.
- Web Servers: In web servers, queues can be used to manage incoming requests from clients. A single array implementation of multiple queues can be used to manage multiple request queues, such as HTTP and FTP requests, in a single data structure.
- Operating Systems: In operating systems, queues can be used to manage system resources such as CPU time and memory. A circular array implementation of multiple queues can be used to manage multiple queues of processes or threads, allowing efficient resource management.
- Data Structures: Queues are a fundamental data structure used in many algorithms and software applications. A circular array implementation of multiple queues can be used to implement queue-based algorithms such as breadth-first search, shortest path algorithms, and simulation algorithms.
Like Article
Suggest improvement
Share your thoughts in the comments
Please Login to comment...