When working with data structures in Kotlin, arrays and lists often come to mind first. They’re built-in, simple, and cover most scenarios. But sometimes you need more control over how elements are connected, inserted, or removed. That’s where a Doubly Linked List in Kotlin shines.
In this blog, we’ll explore what a doubly linked list is, why it’s useful, real-world applications, and most importantly — how to implement one in Kotlin.
Doubly Linked List in Kotlin
A doubly linked list is a data structure made up of nodes. Each node stores three things:
- Data — the actual value.
- A reference to the next node.
- A reference to the previous node.
This dual-link system allows navigation forward and backward through the list. That’s the main difference from a singly linked list, which only moves forward.
Why Use a Doubly Linked List in Kotlin?
You might ask: “Why bother with a doubly linked list when Kotlin already has List
and MutableList
?”
Here are a few reasons:
- Fast insertions and deletions: Unlike arrays, you don’t need to shift elements when adding or removing.
- Bidirectional traversal: You can move in both directions, which can be handy in scenarios like undo/redo features.
- Custom data structures: Sometimes you want full control over memory and connections.
Real-World Use Cases
Let’s look at where a Doubly Linked List in Kotlin can be practical:
- Browser history navigation (go back and forward between pages).
- Undo/Redo operations in editors.
- Music playlists where you can jump to the previous or next song.
- Deque (Double-Ended Queue) implementations for efficient queue operations.
Implementing a Doubly Linked List in Kotlin
Let’s write a clean, easy-to-follow implementation.
Define the Node
class Node<T>(
var data: T,
var prev: Node<T>? = null,
var next: Node<T>? = null
)
Here, Node
is generic (<T>
) so it can store any type (Int, String, custom objects, etc.). Each node keeps track of its data
, the previous node (prev
), and the next node (next
).
Create the DoublyLinkedList Class
class DoublyLinkedList<T> {
private var head: Node<T>? = null
private var tail: Node<T>? = null
fun isEmpty() = head == null
}
We keep track of two references:
- head → the first node.
- tail → the last node.
Add Elements
Let’s add items to the end of the list.
fun append(data: T) {
val newNode = Node(data)
if (head == null) {
head = newNode
tail = newNode
} else {
tail?.next = newNode
newNode.prev = tail
tail = newNode
}
}
- If the list is empty, both
head
andtail
point to the new node. - Otherwise, we connect the new node after the current
tail
and updatetail
.
Prepend Elements
Adding to the beginning works similarly:
fun prepend(data: T) {
val newNode = Node(data)
if (head == null) {
head = newNode
tail = newNode
} else {
newNode.next = head
head?.prev = newNode
head = newNode
}
}
Remove Elements
Removing a node requires updating both previous and next references.
fun remove(data: T) {
var current = head
while (current != null) {
if (current.data == data) {
if (current.prev != null) {
current.prev?.next = current.next
} else {
head = current.next
}
if (current.next != null) {
current.next?.prev = current.prev
} else {
tail = current.prev
}
break
}
current = current.next
}
}
Here we search for the node, reconnect neighbors around it, and update head
or tail
if needed.
Print the List
For debugging, let’s add a print function.
fun printForward() {
var current = head
while (current != null) {
print("${current.data} ")
current = current.next
}
println()
}
fun printBackward() {
var current = tail
while (current != null) {
print("${current.data} ")
current = current.prev
}
println()
}
Full Example in Action
Before running the code, make sure all the above functions are inside the DoublyLinkedList<T>
class.
fun main() {
val list = DoublyLinkedList<Int>()
list.append(10)
list.append(20)
list.append(30)
list.prepend(5)
println("Forward traversal:")
list.printForward()
println("Backward traversal:")
list.printBackward()
println("Removing 20...")
list.remove(20)
list.printForward()
}
Output:
Forward traversal:
5 10 20 30
Backward traversal:
30 20 10 5
Removing 20...
5 10 30
Conclusion
A Doubly Linked List in Kotlin gives you more control when working with dynamic data. While Kotlin’s standard library handles most needs with List
or MutableList
, knowing how to build and use a doubly linked list can be a powerful skill.
You now know:
- What a doubly linked list is.
- Real-world scenarios where it’s useful.
- How to implement it step by step in Kotlin.
This structure shines in apps where insertion, deletion, or bidirectional navigation matters — like history tracking, playlists, or undo/redo stacks.