1Queue Operations
▶
A queue is a linear data structure governed by the First-In, First-Out (FIFO) principle: the element that has waited the longest is always the first one to be served. Think of a line at a ticket counter — the person who joined the line first is the first to be helped, and every new arrival joins at the back. The four fundamental operations that make a queue useful are enqueue, dequeue, peek, and isEmpty. Understanding exactly what each operation does, what invariants it must respect, and how it behaves under edge conditions is essential before implementing or using a queue in any real system.
Queues appear throughout computer science: scheduling tasks in an operating system, buffering data packets in a network router, managing print jobs, performing breadth-first graph traversal, and handling asynchronous messages between services. In every one of these scenarios, the same four operations are at work, and every one of them ideally runs in O(1) constant time, meaning the time required does not grow with the number of elements already in the queue.
The Enqueue Operation
Enqueue is the operation that adds a new element to the queue. The defining rule is that the new element is always inserted at the rear (tail) end of the queue, never anywhere else. This single rule is what enforces FIFO ordering: because elements join at the back and leave from the front, the relative order of arrival is perfectly preserved.
Every successful enqueue increases the size of the queue by exactly one. When a queue has a fixed capacity — as is common in array-based (circular buffer) implementations — the operation must first check whether the queue is already full. Attempting to enqueue into a full fixed-capacity queue causes an overflow condition, which should be signalled to the caller via an exception or an error return value. Dynamically sized implementations (backed by a linked list or a resizable array) do not have this limit under normal memory conditions, but even they must handle memory-allocation failures gracefully.
In a linked-list implementation, enqueue works as follows:
- A new node is allocated and its data field is set to the value being enqueued.
- The next pointer of the current tail node is updated to point to the new node.
- The tail pointer of the queue itself is advanced to reference the new node.
- The size counter is incremented by one.
In an array-based circular-buffer implementation, enqueue works like this:
- Check that
size < capacity; if not, raise an overflow error. - Calculate the new rear index as
rear = (rear + 1) % capacity. - Place the new element at
array[rear]. - Increment the size counter by one.
The modulo arithmetic is what makes the array circular, allowing the rear pointer to wrap around to the beginning of the array once it reaches the end, reusing slots freed by previous dequeues without shifting any elements.
// Pseudocode — linked-list enqueue
procedure enqueue(queue, value):
node = new Node(value)
node.next = null
if queue.tail is not null:
queue.tail.next = node
queue.tail = node
if queue.head is null: // first element ever inserted
queue.head = node
queue.size = queue.size + 1
The Dequeue Operation
Dequeue is the operation that removes and returns the element that has been in the queue the longest — the element sitting at the front (head) position. Removing from the front, combined with inserting at the rear, is precisely what upholds FIFO ordering. Dequeue never removes an arbitrary element from the middle or end; doing so would violate the queue contract entirely.
Before any removal takes place, the operation must check for an underflow condition: dequeuing from an empty queue is a logical error. Depending on the implementation contract, this check triggers either a thrown exception (e.g., NoSuchElementException in Java) or a return of a sentinel value such as null or -1. Silently ignoring the underflow — returning garbage data — is always the wrong choice.
After a successful dequeue:
- The value stored at the head node (or head index) is saved to be returned.
- The front pointer advances to the next element: in a linked list,
head = head.next; in a circular array,front = (front + 1) % capacity. - If the queue is now empty after removal, both head and tail pointers should be reset to
null(linked list) or the size counter should reach zero (array). - The size counter is decremented by one.
- The saved value is returned to the caller.
// Pseudocode — linked-list dequeue
procedure dequeue(queue):
if queue.head is null:
raise UnderflowException("Queue is empty")
value = queue.head.data
queue.head = queue.head.next
if queue.head is null: // queue is now empty
queue.tail = null
queue.size = queue.size - 1
return value
Notice that in the linked-list version, the old head node becomes unreferenced and is reclaimed by the garbage collector (in managed languages) or must be explicitly freed (in C/C++). In the circular-array version, the slot at the old front index is logically vacated and will be overwritten by a future enqueue — no data movement is required.
The Peek (Front) Operation
Peek (sometimes called front or element) answers a simple question: What is the next element that would be dequeued, without actually removing it? It returns the value at the head of the queue while leaving the queue's structure completely unchanged. The head pointer, tail pointer, and size counter are all unmodified.
Like dequeue, peek must first verify that the queue is not empty. Peeking into an empty queue is just as erroneous as dequeuing from one, because there is no element whose value can be returned. The same signalling mechanisms apply — throw an exception or return a sentinel value.
- In a linked-list queue:
return queue.head.data - In an array-based queue:
return array[front]
// Pseudocode — peek
procedure peek(queue):
if isEmpty(queue):
raise UnderflowException("Queue is empty")
return queue.head.data // linked list
// or: return array[front] // circular array
Peek is particularly useful when a program needs to make a decision based on what comes next before committing to the removal. For example, a scheduler might peek at the next task's priority before deciding whether to interrupt the current task. Because peek is non-destructive, calling it any number of times in a row always returns the same value as long as no enqueue or dequeue occurs in between.
The isEmpty Operation
isEmpty is the queue's sentinel guard. It returns true when the queue holds no elements and false otherwise. Nearly every other queue operation calls isEmpty (or an equivalent check) as a prerequisite before performing its work, making it arguably the most frequently invoked operation of all four.
Implementation of isEmpty is straightforward and varies slightly by the underlying data structure:
- Array-based with a size counter:
return size == 0 - Array-based tracking front and rear indices (no explicit counter): a common convention is to treat the queue as empty when
front == rearin a circular buffer that sacrifices one slot, or to maintain a boolean flag. - Linked-list implementation:
return head == null— if there is no head node, there are no elements.
// Pseudocode — isEmpty
procedure isEmpty(queue):
return queue.size == 0 // array-based with counter
// or: return queue.head is null // linked-list
isEmpty executes in true O(1) time because it inspects only a single field — no traversal or comparison loop is needed. Its simplicity is precisely why it can be invoked freely as a guard inside dequeue, peek, and any other logic that operates on queue contents.
Operation Behavior and Expected Outcomes
Taken together, the four operations define the complete behavioral contract of a queue. The table below summarises their key characteristics side by side:
| Operation | Modifies Queue? | Where It Acts | Edge-Case Risk | Time Complexity |
|---|---|---|---|---|
| enqueue(value) | Yes — adds element, updates tail, increments size | Rear (tail) | Overflow on fixed-capacity queue | O(1) |
| dequeue() | Yes — removes element, advances front, decrements size | Front (head) | Underflow on empty queue | O(1) |
| peek() | No — read-only inspection | Front (head) | Underflow on empty queue | O(1) |
| isEmpty() | No — read-only check | Whole queue (size or head) | None | O(1) |
A key insight is that enqueue and dequeue work as complementary opposites: enqueue grows the queue at the rear while dequeue shrinks it from the front. Executed in any sequence, they jointly enforce the FIFO invariant — the element enqueued earliest among all currently present elements is always the next one dequeued. This invariant is not just a convention; it is the entire reason queues exist as a distinct data structure.
Peek and isEmpty are non-destructive (also called accessor or observer) operations. No matter how many times you call them in any order, the queue's state remains identical to what it was before the first call. This property allows them to be used freely inside conditional logic, loops, and debugging code without any risk of accidental data loss.
Consider the following sequence of operations to see all four in action together:
Queue q (initially empty)
isEmpty(q) → true
enqueue(q, 10) → queue: [10] size = 1
enqueue(q, 20) → queue: [10, 20] size = 2
enqueue(q, 30) → queue: [10, 20, 30] size = 3
isEmpty(q) → false
peek(q) → 10 (queue unchanged: [10, 20, 30])
dequeue(q) → 10 queue: [20, 30] size = 2
dequeue(q) → 20 queue: [30] size = 1
peek(q) → 30 (queue unchanged: [30])
dequeue(q) → 30 queue: [] size = 0
isEmpty(q) → true
dequeue(q) → *** UnderflowException ***
peek(q) → *** UnderflowException ***
This trace illustrates several important points simultaneously. The values emerge in exactly the order they were inserted (10 → 20 → 30), confirming FIFO. Peek at any point returns the same value as the next dequeue would, without altering the queue. Once the queue is drained, both dequeue and peek correctly signal an error rather than producing undefined behaviour.
When designing or selecting a queue implementation for a real application, keep the following considerations in mind:
- Overflow handling: If a bounded queue is used (e.g., a ring buffer for network packets), enqueue must gracefully reject or drop elements when the buffer is full. The policy — drop oldest, drop newest, block, or throw — depends on application requirements.
- Underflow handling: Consistent signalling on underflow (always throw, or always return a sentinel, but never mix both for the same method) makes the queue easier to use correctly.
- Thread safety: In concurrent environments, all four operations must be protected by synchronisation mechanisms (locks, atomic operations, or lock-free algorithms) to prevent race conditions; a common mistake is checking isEmpty and then calling dequeue in two unsynchronised steps.
- Memory management: In linked-list implementations, dequeue must release the memory of the removed node in languages without garbage collection to prevent memory leaks.
Mastering these four operations — their mechanics, their invariants, their edge cases, and their time complexities — provides the complete foundation needed to implement queues from scratch, use them in algorithms such as breadth-first search, and reason confidently about system-level constructs like job schedulers and message buffers that queue operations power every day.