12 essential data structures for Kotlin developers. Each one explained with Big O complexity, animated visuals, and real code samples you can copy.
Array<T>Fixed-size, contiguous block of memory. Elements are stored sequentially and accessed by index in constant time. The foundation of most other data structures.
arr[0] = 7
Complexity
When to use
// Fixed-size typed array
val nums = intArrayOf(10, 20, 30, 40, 50)
val words = arrayOf("cat", "ant", "bat")
nums[0] = 99 // O(1) - direct index
val third = nums[2] // O(1) - read by index
// Transform and search
val doubled = nums.map { it * 2 } // O(n) - returns List<Int>
val has = 30 in nums // O(n) - linear scan
val idx = nums.indexOf(40) // O(n) - linear search
nums.sort() // O(n log n) - in-place
val found = nums.binarySearch(30) // O(log n) - requires sorted
val sum = nums.sum() // O(n)MutableList (ArrayList)Resizable array that automatically grows when capacity is exceeded. The most commonly used data structure in most languages. Doubles its internal storage when full, giving amortized O(1) appends.
arr[0] = 7
Complexity
When to use
val names = mutableListOf("Alice", "Bob", "Charlie")
names.add("Diana") // O(1) amortized
names.add(1, "Eve") // O(n) - shifts elements right
names.removeAt(0) // O(n) - shifts elements left
names.removeLast() // O(1)
val has = "Bob" in names // O(n) - linear scan
val first = names[0] // O(1) - direct index
val idx = names.indexOf("Eve") // O(n)
names.sort() // O(n log n)
names.retainAll { it.length > 3 } // O(n) - filter in place
// Kotlin idioms
val squares = (1..10).map { it * it } // O(n) - list comprehensionArrayDeque (LIFO)Last-In-First-Out (LIFO) collection. Only the top element is accessible. Used for tracking state that must be unwound in reverse order.
Push 10
Complexity
When to use
// ArrayDeque as stack - push/pop from the end
val stack = ArrayDeque<Int>()
stack.addLast(10) // O(1) - push
stack.addLast(20)
stack.addLast(30)
val top = stack.last() // O(1) - peek -> 30
val popped = stack.removeLast() // O(1) - pop -> 30
// Interview pattern: valid parentheses
fun isValid(s: String): Boolean {
val st = ArrayDeque<Char>()
for (c in s) {
when (c) {
'(' -> st.addLast(')')
'[' -> st.addLast(']')
'{' -> st.addLast('}')
else -> if (st.isEmpty() || st.removeLast() != c) return false
}
}
return st.isEmpty()
}ArrayDeque (FIFO)First-In-First-Out (FIFO) collection. Elements are added at the back and removed from the front. Fundamental for breadth-first processing.
Enqueue 10
Complexity
When to use
// ArrayDeque as queue - FIFO
val queue = ArrayDeque<String>()
queue.addLast("Task A") // O(1) - enqueue
queue.addLast("Task B")
queue.addLast("Task C")
val front = queue.first() // O(1) - peek -> "Task A"
val next = queue.removeFirst() // O(1) - dequeue -> "Task A"
// Interview pattern: BFS level-order
fun bfs(root: TreeNode?): List<List<Int>> {
val result = mutableListOf<List<Int>>()
val q = ArrayDeque<TreeNode>()
root?.let { q.addLast(it) }
while (q.isNotEmpty()) {
val level = (0 until q.size).map {
val node = q.removeFirst()
node.left?.let { q.addLast(it) }
node.right?.let { q.addLast(it) }
node.value
}
result.add(level)
}
return result
}HashMap<K,V>Maps keys to values using a hash function for near-constant-time lookups. The single most important data structure for coding interviews. Every language has a built-in implementation.
hash("age") = 0
Complexity
When to use
val map = hashMapOf("apple" to 3, "banana" to 5)
map["apple"] = 10 // O(1) - update
map["cherry"] = 2 // O(1) - add
val has = "banana" in map // O(1) - contains key
map.remove("banana") // O(1)
// Safe lookup with default
val count = map.getOrDefault("mango", 0) // O(1) - 0
// Interview pattern: Two Sum
fun twoSum(nums: IntArray, target: Int): IntArray {
val seen = HashMap<Int, Int>()
for ((i, num) in nums.withIndex()) {
val need = target - num
seen[need]?.let { return intArrayOf(it, i) }
seen[num] = i // O(1) insert
}
return intArrayOf()
}HashSet<T>Unordered collection of unique elements. Uses hashing internally for O(1) membership testing. Supports mathematical set operations like union, intersection, and difference.
hash("age") = 0
Complexity
When to use
val set = hashSetOf(1, 2, 3, 4, 5)
set.add(6) // O(1) - true (added)
set.add(3) // O(1) - false (duplicate)
set.remove(1) // O(1)
val has = 4 in set // O(1) - true
// Set operations
val other = hashSetOf(4, 5, 6, 7)
val inter = set intersect other // {4, 5, 6}
val union = set union other // {2, 3, 4, 5, 6, 7}
val diff = set subtract other // {2, 3}
// Interview pattern: contains duplicate
fun containsDuplicate(nums: IntArray): Boolean {
val seen = HashSet<Int>()
return nums.any { !seen.add(it) } // add returns false if dup
}LinkedList<T>Sequence of nodes where each node points to the next (singly linked) or both next and previous (doubly linked). Efficient insertion and deletion at any known position, but no index-based access.
traversing: 5
Complexity
When to use
import java.util.LinkedList
val list = LinkedList<Int>()
list.addLast(10) // O(1)
list.addLast(20)
list.addFirst(5) // O(1)
list.add(1, 15) // O(n) - traverse to index
val has = 20 in list // O(n) - linear scan
list.remove(20) // O(n) - find then unlink
val first = list.peekFirst() // O(1) - 5
val last = list.peekLast() // O(1) - 15
// Iterate
for (val in list) print("$val ") // 5 15 10
// Interview: reverse a linked list
fun reverse(head: ListNode?): ListNode? {
var prev: ListNode? = null; var curr = head
while (curr != null) {
val next = curr.next; curr.next = prev; prev = curr; curr = next
}
return prev
}TreeSet<T>Collection of unique elements maintained in sorted order, typically backed by a balanced binary search tree (red-black tree). Supports range queries and O(log n) min/max.
search(8)
Complexity
When to use
import java.util.TreeSet
val sorted = TreeSet(listOf(5, 3, 8, 1, 9))
// Maintains sorted order: {1, 3, 5, 8, 9} (red-black tree)
sorted.add(4) // O(log n)
sorted.remove(3) // O(log n)
val has = 8 in sorted // O(log n) - true
val min = sorted.first() // O(log n) - 1
val max = sorted.last() // O(log n) - 9
// Range and navigation queries
val range = sorted.subSet(4, 9) // [4, 9) -> {4, 5, 8}
val floor = sorted.floor(6) // 5 - greatest <= 6
val ceil = sorted.ceiling(6) // 8 - smallest >= 6
val higher = sorted.higher(5) // 8 - strictly greaterTreeMap<K,V>Key-value pairs maintained in sorted key order, typically backed by a balanced BST. Enables ordered iteration and range lookups that hash maps cannot provide.
search(8)
Complexity
When to use
import java.util.TreeMap
val tm = TreeMap<String, Int>()
tm["banana"] = 2 // O(log n)
tm["apple"] = 5
tm["cherry"] = 1
tm["date"] = 3
val v = tm["apple"] // O(log n) -> 5
tm.remove("cherry") // O(log n)
// Iterates in sorted key order
for ((key, value) in tm) {
println("$key: $value") // apple: 5, banana: 2, date: 3
}
// Navigation methods
val firstKey = tm.firstKey() // "apple"
val floorKey = tm.floorKey("cat") // "banana" - greatest <= "cat"
val sub = tm.subMap("apple", "cherry") // {apple=5, banana=2}PriorityQueue<T>Collection where elements are dequeued by priority rather than insertion order. Typically implemented as a binary heap. Essential for shortest-path algorithms and top-K problems.
min-heap
Complexity
When to use
import java.util.PriorityQueue
// Min-heap by default
val pq = PriorityQueue<Int>()
pq.offer(30) // O(log n)
pq.offer(10)
pq.offer(20)
val min = pq.peek() // O(1) - 10
val top = pq.poll() // O(log n) - 10, removed
// Max-heap using reversed comparator
val maxPQ = PriorityQueue<Int>(compareByDescending { it })
// Interview pattern: K closest points
fun kClosest(points: Array<IntArray>, k: Int): Array<IntArray> {
val pq = PriorityQueue<IntArray>(
compareBy { it[0] * it[0] + it[1] * it[1] }
)
points.forEach { pq.offer(it) }
return Array(k) { pq.poll() }
}ConcurrentHashMap<K,V>Thread-safe hash map designed for concurrent read/write access from multiple threads. Uses fine-grained locking or lock-free techniques instead of a single global lock.
hash("age") = 0
Complexity
When to use
import java.util.concurrent.ConcurrentHashMap
val cache = ConcurrentHashMap<String, Int>()
// Thread-safe atomic operations
cache["hits"] = 0 // O(1)
cache.putIfAbsent("sessions", 0) // O(1)
cache.merge("hits", 1, Int::plus) // O(1) - atomic increment
// Compute if absent - factory called once even under contention
val value = cache.computeIfAbsent("users") { key ->
expensiveComputation(key) // called only if key missing
}
// Atomic compute
cache.compute("hits") { _, v -> (v ?: 0) + 1 }
// Safe iteration - weakly consistent, no ConcurrentModificationException
cache.forEach { (k, v) -> println("$k: $v") }ByteBufferZero-copy view over a contiguous region of memory. Lets you reference a portion of an array or buffer without allocating new memory. Critical for performance-sensitive parsing and processing.
Span[0..3] = [1, 2, 3]
Complexity
When to use
import java.nio.ByteBuffer
// Heap-allocated buffer
val buf = ByteBuffer.allocate(256)
val direct = ByteBuffer.allocateDirect(256) // off-heap, faster I/O
buf.putInt(42) // O(1) - write at position
buf.putDouble(3.14)
buf.flip() // switch from write to read mode
val intVal = buf.getInt() // O(1) - read 42
val dblVal = buf.getDouble() // O(1) - read 3.14
// Slice creates a shared view (zero-copy, like Span)
buf.position(0).limit(4)
val view = buf.slice() // O(1) - shared underlying memory
// Kotlin extension: convert to byte array when needed
val bytes = ByteArray(buf.remaining())
buf.get(bytes) // O(n) - copy into arrayAverage-case time complexity. * = amortized.
| Structure | Access | Search | Insert | Delete |
|---|---|---|---|---|
| Array | O(1) | O(n) | O(n) | O(n) |
| Dynamic Array | O(1) | O(n) | O(1)* | O(n) |
| Stack | O(n) | O(n) | O(1)* | O(1) |
| Queue | O(n) | O(n) | O(1)* | O(1) |
| Hash Map | O(1) | O(1) | O(1)* | O(1) |
| Hash Set | N/A | O(1) | O(1)* | O(1) |
| Linked List | O(n) | O(n) | O(1) | O(1) |
| Sorted Set | O(n) | O(log n) | O(log n) | O(log n) |
| Sorted Map | O(log n) | O(log n) | O(log n) | O(log n) |
| Priority Queue | O(n) | O(n) | O(log n) | O(log n) |
| Concurrent Map | O(1) | O(1) | O(1)* | O(1) |
| Memory View | O(1) | O(n) | N/A | N/A |
| I need to... | Use |
|---|---|
| Store items by index, resize dynamically | List / Dynamic Array |
| Map keys to values with O(1) lookup | HashMap / Dictionary |
| Track unique items, check existence in O(1) | HashSet / Set |
| Last-in-first-out (undo, DFS, brackets) | Stack |
| First-in-first-out (BFS, task queues) | Queue |
| Keep elements sorted at all times | SortedSet / TreeSet |
| Process items by priority (Dijkstra, top-K) | PriorityQueue / Heap |
| Insert/delete at a known position in O(1) | LinkedList |
| Sorted key-value pairs | SortedDictionary / TreeMap |
| Thread-safe shared cache | ConcurrentDictionary |
| Slice arrays/strings without copying | Span / Slice / memoryview |
The most commonly used are dynamic arrays (List/ArrayList/vector), hash maps (Dictionary/HashMap/dict), and hash sets. For interviews, also know stacks, queues, trees, and priority queues. These cover 90%+ of coding interview problems.
Start with the dynamic array and hash map. Together they solve the majority of interview problems. Then learn stacks (for DFS, bracket matching) and queues (for BFS). After that, tackle trees, heaps, and graphs.
No. Big O measures algorithmic complexity, not language-specific performance. A hash map lookup is O(1) whether you use Python dict, Java HashMap, or C# Dictionary. Constant factors differ (C++ is faster than Python in wall-clock time), but Big O is the same.
It depends on the language runtime. Check the Kotlin standard library documentation for heap or priority queue support.
Practice with an AI interviewer that asks you to implement, optimize, and explain data structure choices in real time.
Try a free mock interviewarrow_forward