Efficient queue in Haskell
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.
Introduction
A queue needs fast insertion at the back and fast removal from the front, which makes a plain Haskell list a poor fit for general FIFO workloads. The usual functional solution is a two-list queue that keeps one list for the front and another list for recently added items, giving amortized constant-time operations.
Why a Single List Is Not Enough
A Haskell list is excellent for adding or removing at the head with (:) and pattern matching. It is not efficient for appending at the end.
A naive queue might look like this:
dequeue is cheap, but enqueue is O(n) because ++ must walk the whole list. For queue-heavy code that cost adds up quickly.
The Two-List Queue
A better representation stores the front of the queue in one list and the rear in reverse order in another list.
When the front list becomes empty, reverse the rear list and use that as the new front.
Example use:
The rear list is built with (:), which is cheap, and reversed only occasionally.
Why the Complexity Is Amortized O(1)
Reversing the rear list costs O(n), so at first glance the queue may not look constant time. The important point is that each element moves from the rear list to the front list at most once before being removed.
That means the expensive reversal work is spread across many enqueue and dequeue operations. Over a long sequence of operations, the average cost per operation is constant. This is the standard amortized analysis for a persistent queue.
A More Complete API
A practical queue often includes peek and isEmpty.
Keeping normalize as a small helper makes the rest of the API easy to reason about.
Library Option: Data.Sequence
If you need a production-ready general-purpose queue, Data.Sequence is often an even better choice than writing your own. It provides efficient access at both ends and is already well tested.
This is especially appealing when the queue is only one part of a larger program and you do not want to maintain a custom data structure.
Common Pitfalls
The most common mistake is using xs ++ [x] for every enqueue. It works, but the performance is linear per insert.
Another issue is forgetting to normalize after the front list becomes empty. Without that step, dequeues can fail even though elements are waiting in the rear list.
It is also easy to overengineer the queue too early. If the application already depends on containers, Data.Sequence may be the better answer.
Finally, remember that amortized O(1) does not mean every single operation is constant time. Occasional reversals still happen, but the average remains efficient.
Summary
- A plain list makes queue insertion at the back too expensive.
- A two-list queue gives amortized
O(1)enqueue and dequeue. - '
normalizeis the key helper that moves reversed rear elements to the front.' - Use
Data.Sequencewhen you want a tested library implementation. - Think in amortized cost, not worst-case cost of one operation.
Related reading
- Efficient recursive random sampling
- Efficient set intersection of a collection of sets in C
- Efficient string truncation algorithm, sequentially removing equal prefixes and suffixes
- Efficient way of calculating likeness scores of strings when sample size is large?
- Efficient swapping of elements of an array in Java
- Efficient time and space complexity data structure for dense and sparse matrix
- Efficient substring Search in DynamoDB
- Efficient use of reflection in C

DSA Fundamentals
Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.