Double Ended Queue - Programming and Data Structures - Computer Science

Introduction

A double-ended queue (commonly written deque or double-ended queue) is a linear data structure that allows insertion and removal of elements from both ends - the front and the back. It generalises both stacks and queues: a deque supports push and pop operations at both ends. Deques are used where flexibility of both LIFO and FIFO behaviour is required.

Introduction

Implementation of a Double-ended Queue

One common implementation of a deque uses a circular array. The circular array lets front and rear indices wrap around when they reach the array boundaries, enabling efficient use of fixed array space. The implementation below provides the typical operations supported by a deque.

  • push_back: insert element at back (rear)
  • push_front: insert element at front
  • pop_back: remove element from back
  • pop_front: remove element from front
  • get_back: return element at back without removing it
  • get_front: return element at front without removing it
  • empty: returns true if deque is empty
  • full: returns true if deque is full (for fixed-size array)

The following C++ style outline shows a circular-array implementation with a fixed maximum size.

#define SIZE 5 class Dequeue { int *arr; int front, rear; public: Dequeue() { arr = new int[SIZE]; front = -1; rear = -1; } void push_front(int key); void push_back(int key); void pop_front(); void pop_back(); int get_front(); int get_back(); bool full(); bool empty(); };

Insert Element at Front (push_front)

Before inserting, check whether the deque is full. If not full, insert at the front using circular behaviour for the index.

  • If the deque is empty, set front = rear = 0.
  • Else if front == 0, wrap around and set front = SIZE - 1.
  • Else decrement front by 1 and insert the element at arr[front].
Insert Element at Front (push_front)
Insert Element at Front (push_front)

void Dequeue::push_front(int key) { if (full()) { std::cout <>
";="" }="" else="" {="" if="" (front="=" -1)="" empty="" deque="" front="rear" =="" 0;="" else="" if="" (front="=" 0)="" wrap="" front="" to="" end="" front="SIZE" -="" 1;="" else="" --front;="" arr[front]="key;" }="" }="">

Insert Element at Back (push_back)

Before inserting, check whether the deque is full. If not full, insert at the rear using circular index updates.

  • If the deque is empty, set front = rear = 0.
  • Else if rear == SIZE - 1, wrap around and set rear = 0.
  • Else increment rear by 1 and insert the element at arr[rear].
Insert Element at Back (push_back)

void Dequeue::push_back(int key) { if (full()) { std::cout <>
";="" }="" else="" {="" if="" (front="=" -1)="" empty="" deque="" front="rear" =="" 0;="" else="" if="" (rear="=" size="" -="" 1)="" wrap="" rear="" to="" start="" rear="0;" else="" ++rear;="" arr[rear]="key;" }="" }="">

Delete First Element (pop_front)

To remove the element at the front, first check if the deque is empty. If not empty, remove and update indices according to these rules.

  • If only one element is present (front == rear), set front = rear = -1 to make the deque empty.
  • Else if front == SIZE - 1, wrap and set front = 0.
  • Else increment front by 1.
Delete First Element (pop_front)

void Dequeue::pop_front() { if (empty()) { std::cout <>
";="" }="" else="" {="" if="" (front="=" rear)="" only="" one="" element="" front="rear" =="" -1;="" else="" if="" (front="=" size="" -="" 1)="" wrap="" front="" to="" start="" front="0;" else="" ++front;="" }="" }="">

Delete Last Element (pop_back)

To remove the element at the rear, first check if the deque is empty. If not empty, remove and update indices according to these rules.

  • If only one element is present (front == rear), set front = rear = -1 to make the deque empty.
  • Else if rear == 0, wrap and set rear = SIZE - 1.
  • Else decrement rear by 1.
Delete Last Element (pop_back)

void Dequeue::pop_back() { if (empty()) { std::cout <>
";="" }="" else="" {="" if="" (front="=" rear)="" only="" one="" element="" front="rear" =="" -1;="" else="" if="" (rear="=" 0)="" wrap="" rear="" to="" end="" rear="SIZE" -="" 1;="" else="" --rear;="" }="" }="">

Check if Deque is Empty (empty)

A deque implemented with the above convention is empty when front == -1.

bool Dequeue::empty() { if (front == -1) return true; else return false; }

Check if Deque is Full (full)

For a circular array, the deque is full when the next position of rear is front. The common tests are shown below.

bool Dequeue::full() { if ((front == 0 && rear == SIZE - 1) || (front == rear + 1)) return true; else return false; }

Return First Element (get_front)

If the deque is not empty, return the value at arr[front]. If it is empty, indicate underflow or return a sentinel value (for example -1) as a simple error signal.

int Dequeue::get_front() { if (empty()) { std::cout <>
";="" return="" -1;="" }="" else="" {="" return="" arr[front];="" }="" }="">

Return Last Element (get_back)

If the deque is not empty, return the value at arr[rear]. If it is empty, indicate underflow or return a sentinel value.

int Dequeue::get_back() { if (empty()) { std::cout <>
";="" return="" -1;="" }="" else="" {="" return="" arr[rear];="" }="" }="">

Time and Space Complexity

  • Time complexity: All fundamental deque operations (push_front, push_back, pop_front, pop_back, get_front, get_back, empty, full) take O(1) time in the circular-array implementation.
  • Space complexity: The array implementation uses O(SIZE) space. For dynamic-size deques, a dynamic array or a linked list may be used.

Advantages and Limitations

  • Advantages: constant-time insertions and deletions at both ends; simple array-based implementation when maximum size is known; useful in many algorithms (sliding window, caching, BFS with two-ended extensions).
  • Limitations: fixed-size circular array has limited capacity and requires careful index management; resizing a circular array requires copying elements in correct order. A linked-list implementation removes fixed-size limitation but uses extra memory per node and has less cache locality.

Alternative Implementations

Deques can also be implemented using a doubly linked list. In a doubly linked list:

  • Each node stores a value, a pointer to the next node and a pointer to the previous node.
  • push_front and push_back add new nodes at the two ends; pop_front and pop_back remove nodes from the ends.
  • All operations are O(1) time and the size can grow dynamically; however, extra memory is needed per node for pointers.

Worked Example - Trace on Array of SIZE = 5

Consider an initially empty deque with SIZE = 5 and the following sequence of operations:

  • push_back(10)
  • push_back(20)
  • push_front(5)
  • pop_back()
  • push_back(30)
  • push_back(40)
  • push_back(50)
  • pop_front()

Trace of front, rear and array contents after each operation:

  • Initial: front = -1, rear = -1, arr = [ , , , , ]
  • After push_back(10): front = 0, rear = 0, arr = [10, , , , ]
  • After push_back(20): front = 0, rear = 1, arr = [10, 20, , , ]
  • After push_front(5): front = SIZE-1 = 4, rear = 1, arr = [10, 20, , , 5]
  • After pop_back(): removes arr[rear]=20, rear becomes 0, arr (logical) = [10, , , , 5]
  • After push_back(30): rear becomes 1, arr = [10, 30, , , 5]
  • After push_back(40): rear becomes 2, arr = [10, 30, 40, , 5]
  • After push_back(50): rear becomes 3, arr = [10, 30, 40, 50, 5]
  • After pop_front(): removes arr[front]=5, front wrapped from 4 to 0, logical contents = [10, 30, 40, 50]

This example demonstrates the wrap-around of indices and how the logical order differs from physical positions in the array.

Applications of Deque

  • Implementing undo/redo functionality where operations are added and removed from both ends.
  • Sliding window algorithms (e.g., find maximum in each window of size k) - deque optimises to O(n).
  • Deque is used in certain BFS variants and bidirectional search algorithms.
  • Circular buffers, scheduling, and caching schemes where both ends need updates.

Implementation Notes and Edge Cases

  • Always check full() before a push in fixed-size array implementation to avoid overflow.
  • Always check empty() before a pop or get to avoid underflow.
  • When resizing a circular array, copy elements starting from front in logical order into a new larger array and reset front to 0 and rear to size-1 of the filled elements.
  • Decide on an error-handling policy for underflow/full conditions: printing an error, throwing exceptions, or returning sentinel values are common choices in C++ implementations.

Summary

A deque is a flexible linear structure that permits insertion and deletion at both ends in constant time. The circular-array implementation is efficient when an upper bound on size is known, while a doubly linked list provides dynamic sizing. Understanding index updates (wrap-around) and careful handling of empty/full states are essential for a correct implementation.

The document Double Ended Queue is a part of the Computer Science Engineering (CSE) Course Programming and Data Structures.
All you need of Computer Science Engineering (CSE) at this link: Computer Science Engineering (CSE)

FAQs on Double Ended Queue

1. What is a Double Ended Queue (Deque)?
Ans. A Double Ended Queue, also known as Deque, is a data structure that allows insertion and deletion of elements from both ends. It can be visualized as a queue at both ends or as a doubly linked list.
2. What are the basic operations supported by a Double Ended Queue?
Ans. The basic operations supported by a Double Ended Queue are: - Insertion at the front (enqueueFront) - Insertion at the rear (enqueueRear) - Deletion from the front (dequeueFront) - Deletion from the rear (dequeueRear)
3. How is a Double Ended Queue different from a regular queue?
Ans. A Double Ended Queue differs from a regular queue in the sense that it allows insertion and deletion of elements from both ends, whereas a regular queue only supports insertion at one end and deletion from the other end.
4. What are the advantages of using a Double Ended Queue?
Ans. Some advantages of using a Double Ended Queue include: - It provides flexibility in implementing algorithms that require insertion and deletion from both ends. - It can be used to efficiently implement data structures like stacks, queues, and lists. - It allows easy access to both ends of the queue, making it suitable for certain problem-solving scenarios.
5. In what scenarios is a Double Ended Queue useful?
Ans. A Double Ended Queue can be useful in various scenarios, such as: - Implementing a queue with both FIFO (First-In-First-Out) and LILO (Last-In-Last-Out) functionalities. - Implementing algorithms that require efficient insertion and deletion operations from both ends. - Solving problems that involve maintaining a sliding window of elements, where elements are added or removed from both ends.
Explore Courses for Computer Science Engineering (CSE) exam
Get EduRev Notes directly in your Google search
Related Searches
Sample Paper, Important questions, MCQs, Viva Questions, study material, ppt, Semester Notes, Exam, Double Ended Queue, Previous Year Questions with Solutions, shortcuts and tricks, Summary, past year papers, Free, Double Ended Queue, video lectures, Double Ended Queue, Extra Questions, mock tests for examination, practice quizzes, pdf , Objective type Questions;