Double-Ended Queue (Deque)
A deque (pronounced deck), short for double-ended queue, is a linear data structure that allows you to add or remove elements from both the front and the rear ends.
Unlike a regular queue (which only allows insertion at the rear and deletion at the front, following the FIFO (First In, First Out) principle, a deque gives you maximum flexibility by supporting insertion and deletion at either end.
This flexibility makes the deque a general-purpose data structure that combines the behavior of both queues and stacks.
For example, you can use it as a stack (LIFO) if you only operate on one end, or as a standard queue (FIFO) if you restrict insertions to one side and deletions to the other.
Internal Structure
In most implementations, a deque is built using either:
- A circular array, where the front and rear indices wrap around when they reach the array boundaries, or
- A doubly linked list, which has explicit next and previous pointers connecting nodes.
Types of Double-Ended Queues (Deque)
Although all deques allow insertion and deletion at both ends, we can classify them into two main types based on which operations are restricted or allowed at each end.
1. Input-Restricted Deque
An input-restricted deque is a type of deque where Insertion is allowed only at one end (usually the rear) and Deletion is allowed at both ends (front and rear).
- Push new elements only from the rear.
- Remove elements from either the front or the rear.
Example: Imagine a system where new tasks can only be added in the usual order (at the back of the line), but cancellations or priority removals can happen from either end.
Useful: It controls how data enters the system but gives flexibility in clearing or removing data.
2. Output-Restricted Deque
An output-restricted deque is the opposite. Insertion is allowed at both ends (front and rear) and Deletion is allowed only at one end (usually the front).
- Add new elements either to the front or the rear.
- Remove elements only from the front.
Example: Think of a buffer where incoming data can be prioritized (added to the front) or come in normally (added to the rear), but processing always happens from the front.
Useful: It gives full control over how data is added but restricts how it’s consumed, ensuring order.
3. Regular Deque (Unrestricted)
In contrast, a general deque allows Insertion at both ends and Deletion at both ends.
This is the most flexible version and is what people usually refer to when they just say “deque.”
Operations of Double-Ended Queue (Deque)
In an array-based deque, four main data operations are supported:
1. Insert Front
You add a new element just before the current front
position. This requires moving the front
index backward (circularly), which means computing (front - 1 + size) % size
. If the deque is initially empty, both front
and rear
are set to zero.
Algorithm
if (front == (rear + 1) % size) then
print "Deque is full"
return
end if
if front == -1 then
front ← 0
rear ← 0
else if front == 0 then
front ← size - 1
else
front ← front - 1
end if
deque[front] ← value
2. Insert Rear
You add a new element just after the current rear
position. This requires moving the rear
index forward, computed as (rear + 1) % size
. Again, if the deque was empty, both front
and rear
are set to zero.
Algorithm
if (front == (rear + 1) % size) then
print "Deque is full"
return
end if
if front == -1 then
front ← 0
rear ← 0
else if rear == size - 1 then
rear ← 0
else
rear ← rear + 1
end if
deque[rear] ← value
3. Delete Front
You remove the element currently at the front
index. After removal, you move the front
index forward: (front + 1) % size
. If after deletion the deque becomes empty (meaning front == rear
), both indices are reset to -1
.
Algorithm
if front == -1 then
print "Deque is empty"
return
end if
print "Deleted Element:", deque[front]
if front == rear then
// Only one element was in deque
front ← -1
rear ← -1
else
front ← (front + 1) % size
end if
4. Delete Rear
You remove the element at the rear
index. After removal, you move the rear
index backward: (rear - 1 + size) % size
. Again, if the deque becomes empty, both indices are reset.
Algorithm:
if front == -1 then
print "Deque is empty"
return
end if
print "Deleted Element:", deque[rear]
if front == rear then
// Only one element was in deque
front ← -1
rear ← -1
else
rear ← (rear - 1 + size) % size
end if
5. Peek Front
Check (or read) the element at the front of the deque without removing it.
This is useful when you want to know what’s next to be processed but don’t want to change the deque state.
Algorithm
if front == -1 then
print "Deque is empty"
return
end if
print "Front Element:", deque[front]
- We first check if the deque is empty by checking if
front == -1
. - If empty, we report that there’s no element to peek.
- Otherwise, we simply access the value at
deque[front]
and display or return it no deletion or shifting.
Additionally, peek operations are often supported (getFront()
and getRear()
), allowing access to the elements at both ends without modifying the structure.
6. Peek Rear
Check (or read) the element at the rear of the deque without removing it.
This is useful when you want to know what was most recently added at the back.
Algorithm
if front == -1 then
print "Deque is empty"
return
end if
print "Rear Element:", deque[rear]
- we first check if the deque is empty by checking
front == -1
. - If empty, we report no element to peek.
- Otherwise, we access the value at
deque[rear]
and display or return it.
Real-World Applications
Deques are useful in many real-world problems:
- Text editors, for managing undo/redo operations.
- Web browsers, to track forward and backward navigation history.
- Scheduling systems, where tasks might need to be prioritized by pushing to the front or added regularly at the rear.
- Algorithmic problems, such as finding the maximum in a sliding window over an array.
Deque Implementation in C Programming Deque (Circular Array-Based Deque)
#include <stdio.h>
#define SIZE 5
int deque[SIZE];
int front = -1, rear = -1;
// Check if deque is full
int isFull() {
return (front == (rear + 1) % SIZE);
}
// Check if deque is empty
int isEmpty() {
return (front == -1);
}
// Insert at front
void insertFront(int value) {
if (isFull()) {
printf("Deque is full\n");
return;
}
if (isEmpty()) {
front = rear = 0;
} else if (front == 0) {
front = SIZE - 1;
} else {
front--;
}
deque[front] = value;
}
// Insert at rear
void insertRear(int value) {
if (isFull()) {
printf("Deque is full\n");
return;
}
if (isEmpty()) {
front = rear = 0;
} else {
rear = (rear + 1) % SIZE;
}
deque[rear] = value;
}
// Delete from front
void deleteFront() {
if (isEmpty()) {
printf("Deque is empty\n");
return;
}
printf("Deleted from front: %d\n", deque[front]);
if (front == rear) {
front = rear = -1; // Now empty
} else {
front = (front + 1) % SIZE;
}
}
// Delete from rear
void deleteRear() {
if (isEmpty()) {
printf("Deque is empty\n");
return;
}
printf("Deleted from rear: %d\n", deque[rear]);
if (front == rear) {
front = rear = -1; // Now empty
} else if (rear == 0) {
rear = SIZE - 1;
} else {
rear--;
}
}
// Peek front
void peekFront() {
if (isEmpty()) {
printf("Deque is empty\n");
return;
}
printf("Front element: %d\n", deque[front]);
}
// Peek rear
void peekRear() {
if (isEmpty()) {
printf("Deque is empty\n");
return;
}
printf("Rear element: %d\n", deque[rear]);
}
// Display deque
void display() {
if (isEmpty()) {
printf("Deque is empty\n");
return;
}
printf("Deque elements: ");
int i = front;
while (1) {
printf("%d ", deque[i]);
if (i == rear)
break;
i = (i + 1) % SIZE;
}
printf("\n");
}
// Main function to test
int main() {
insertRear(10);
insertRear(20);
insertFront(5);
insertFront(2);
display();
peekFront();
peekRear();
deleteFront();
deleteRear();
display();
insertRear(30);
insertRear(40);
display();
insertRear(50); // Should show full
deleteFront();
insertRear(60);
display();
return 0;
}