Kotlin Built-In Data Structures for Coding Interviews

mutableListOf()returns anArrayList; always use mutable factory functions unless you're building a constantmutableMapOf()returns aLinkedHashMap, not aHashMap; useHashMap<K,V>()directly when insertion order doesn't matterjava.util.TreeMapgives youfloorKey/ceilingKey/higherKey/lowerKey;sortedMapOf()does not expose these methodsArrayDeque(Kotlin stdlib, no import needed) handles stacks and queues in O(1) amortized; rejects null elementsPriorityQueueis a min-heap by default; iterating it does not yield sorted order, onlypoll()andpeek()are priority-orderedmutableSetOf()returns aLinkedHashSet; usehashSetOf()for unordered hashing,TreeSetfor sorted range queries
You reach for listOf("a", "b"), pass it to your BFS helper, and call .add(). The compiler kills it immediately. You switch to mutableListOf(), but now you discover that mutableMapOf() gives you a LinkedHashMap, not a HashMap, and iteration order suddenly "works" in a way you never asked for. This is the moment you realize Kotlin is not Java with better syntax. It's its own thing, and it will let you know. Kotlin's built-in data structures are a real source of interview friction if you treat the language like Java with better syntax.
The 45-minute clock is already running.
The Read-Only / Mutable Split Is Real
Most languages treat mutability as convention or a comment in the code review that nobody reads. Kotlin enforces it at the type level. List<T>, Map<K,V>, and Set<T> are read-only interfaces. They expose no write methods. You cannot add to them, even if the backing object is technically mutable.
mutableListOf() returns MutableList<T>. listOf() returns List<T>. These are different types.
The practical rule for interviews: always use the mutable factory functions (mutableListOf, mutableMapOf, mutableSetOf) unless you're building a constant that never changes. When in doubt, go mutable. The compiler will tell you if you're wrong.
Kotlin Built-In Data Structures: Which One Do You Need?
| Need | Reach For |
|---|---|
| Dynamic array | mutableListOf<T>() |
| Key-value lookup (no order needed) | HashMap<K, V>() |
| Key-value lookup (insertion order) | mutableMapOf<K, V>() |
| Key-value lookup (sorted keys) | java.util.TreeMap<K, V>() |
| Unique values (no order) | hashSetOf<T>() |
| Unique values (insertion order) | mutableSetOf<T>() |
| Unique values (sorted) | java.util.TreeSet<T>() |
| Stack or queue | ArrayDeque<T>() |
| Min/max heap | PriorityQueue<T>() |
MutableList Is Your Default Array
val list = mutableListOf<Int>() list.add(3) // O(1) amortized list.add(0, 7) // O(n), shifts right list.removeAt(0) // O(n), shifts left list[2] // O(1) list.size // O(1) // Pre-size when you know the count val sized = ArrayList<Int>(100)
mutableListOf() returns an ArrayList, so all the Java ArrayList guarantees apply. Append is amortized O(1). Insert or remove at an arbitrary index costs O(n) because the backing array shifts. If you're doing a lot of front-insertions, use ArrayDeque instead.
For 2D grids, use Array(rows) { IntArray(cols) } for a flat int grid, or Array(rows) { MutableList<Int>() } when you need mixed row lengths.
mutableMapOf Is a LinkedHashMap. Surprise.
This is the most common Kotlin gotcha for Java developers, and it gets people every time because the name does not hint at it at all.
// mutableMapOf() returns LinkedHashMap, preserves insertion order val linked = mutableMapOf("b" to 2, "a" to 1) println(linked.keys) // [b, a] // HashMap() has no guaranteed iteration order val hash = HashMap<String, Int>() hash["b"] = 2 hash["a"] = 1
mutableMapOf() returns a LinkedHashMap, not a HashMap. If you just need a hash map and don't care about order, use HashMap<K, V>() directly. The difference is rarely observable in correctness, but it will absolutely bite you the one time you're explaining your solution and the output comes out in the wrong order and you have no idea why.
Frequency counting idiom:
val freq = HashMap<Char, Int>() for (c in s) { freq[c] = freq.getOrDefault(c, 0) + 1 } // Even shorter for (c in s) freq.merge(c, 1, Int::plus)
TreeMap: When You Need Sorted Keys
When the problem involves "find the closest value" or "range iteration", you need a sorted map. Declare it as java.util.TreeMap<K, V>() directly to get floorKey, ceilingKey, higherKey, and lowerKey.
sortedMapOf() exists in the Kotlin stdlib, but it returns SortedMap<K, V>, a narrower interface where floorKey doesn't exist. You have to cast to get it back, which is annoying and looks exactly as bad as it sounds in a live interview.
import java.util.TreeMap val tm = TreeMap<Int, String>() tm[10] = "ten" tm[20] = "twenty" tm[30] = "thirty" tm.floorKey(25) // 20, greatest key <= 25 tm.ceilingKey(25) // 30, smallest key >= 25 tm.lowerKey(20) // 10, strictly less than 20 tm.higherKey(20) // 30, strictly greater than 20 tm.firstKey() // 10 tm.lastKey() // 30
All operations are O(log n). Use this for calendar interval problems, event sweeps, and any problem where you need the "nearest" key.
Three Ways to Store Unique Values
val hashSet = hashSetOf<Int>() // no order, O(1) ops val linkedSet = mutableSetOf<Int>() // insertion order, O(1) ops val treeSet = java.util.TreeSet<Int>() // sorted, O(log n) ops
mutableSetOf() returns a LinkedHashSet, which preserves insertion order. Seeing a pattern? Kotlin's default factory functions consistently favor insertion-order variants. If you want true unordered hashing, call hashSetOf() explicitly.
TreeSet gives you the same navigation methods as TreeMap (floor, ceiling, higher, lower) for O(log n) per operation. Use it when you need to query a sorted pool of values by proximity. "Find the closest appointment" or "nearest booked seat" problems live here.
Use ArrayDeque for Everything Stack-or-Queue-Shaped
Kotlin's standard library ships its own ArrayDeque in kotlin.collections. It is distinct from java.util.ArrayDeque. Prefer the Kotlin version: it has a cleaner API, no import needed, and you don't have to remember whether push is the front or the back (because the names are just addFirst and addLast, which are self-explanatory in a way that push never was).
val dq = ArrayDeque<Int>() // As a stack (LIFO) dq.addLast(1) dq.addLast(2) dq.removeLast() // 2 // As a queue (FIFO) dq.addLast(1) dq.addLast(2) dq.removeFirst() // 1 // Peek without removing dq.first() // front dq.last() // back
All four operations (addFirst, addLast, removeFirst, removeLast) run in O(1) amortized. One constraint: ArrayDeque does not accept null elements. If your algorithm needs nullable slots, use a sentinel value or a MutableList<T?>.
PriorityQueue Is a Min-Heap (and Will Not Sort for You)
Kotlin does not have its own priority queue. You use java.util.PriorityQueue, which is a min-heap by default. This is not a problem, as long as you remember that it is a heap and not a sorted list. Spoiler: people forget.
import java.util.PriorityQueue // Min-heap (smallest element at head) val minHeap = PriorityQueue<Int>() minHeap.offer(5) minHeap.offer(1) minHeap.offer(3) minHeap.peek() // 1 minHeap.poll() // 1 // Max-heap val maxHeap = PriorityQueue<Int>(reverseOrder()) // Custom comparator, min by second element of pair val pq = PriorityQueue<Pair<Int, Int>>(compareBy { it.second })
offer and poll are O(log n). peek is O(1). The classic interview mistake: iterating over a PriorityQueue does not give sorted output. Only the head (peek()) is guaranteed to be the minimum. The rest of the queue is in heap order, which means "close-ish to sorted but not really." If you need sorted output, poll everything out one at a time.
For top-K problems, a max-heap of size K gives you O(n log K) time. See the heap data structure deep dive for the full analysis.
The Idioms Worth Memorising
Each of these replaces several lines of manual bookkeeping.
// Group by key, count per group val freq = "hello".groupingBy { it }.eachCount() // {h=1, e=1, l=2, o=1} // Split into two lists by predicate val (evens, odds) = listOf(1,2,3,4,5).partition { it % 2 == 0 } // Sort by multiple keys val sorted = people.sortedWith(compareBy({ it.age }, { it.name })) // Build a list inside an expression val result = buildList { add(0) addAll(input.filter { it > 0 }) } // Safe null-handling with getOrDefault val count = map.getOrDefault(key, 0)
Five Traps That Cost Marks
1. listOf() is read-only, not deeply immutable. You can cast it to MutableList and mutate the backing object. The compiler will let you. Don't. Declare mutable if you mean mutable, and read-only if you mean read-only. The distinction is the whole point.
2. mutableMapOf() is LinkedHashMap. This is the Kotlin gotcha that catches the most Java developers. You think you're getting a hash map with no guarantees. You're getting insertion-ordered iteration. It rarely affects correctness, but it will absolutely affect your explanation when the output comes out ordered and you weren't expecting it.
3. Kotlin's ArrayDeque does not allow null elements. If your algorithm needs a nullable slot, use a sentinel value like -1 or switch to MutableList<Int?>. The null restriction is the one reason you'd ever reach for a list over a deque for stack or queue behavior.
4. PriorityQueue iteration order is undefined. Writing a for-loop over a PriorityQueue and expecting sorted output is the interview equivalent of alphabetizing a pile of papers by shaking it. Only poll() and peek() give you the priority-ordered element.
5. sortedMapOf() returns SortedMap, not TreeMap. SortedMap exposes firstKey/lastKey but not floorKey/ceilingKey. You will discover this missing method at the worst possible moment. Construct java.util.TreeMap<K, V>() directly.
The Choice Is Mechanical. The Syntax Is What Trips You.
Once you recognise the pattern in a problem, picking the right structure is straightforward. Unordered lookup goes to HashMap. Ordered iteration goes to TreeMap or TreeSet. Stack and queue both go to ArrayDeque. Repeated min/max extraction goes to PriorityQueue.
What trips people up in a real session is not the choice. It's the syntax under pressure: the comparator for a custom PriorityQueue, the getOrPut call, the floorKey lookup that returns null instead of throwing. Those are worth drilling separately from problem solving. Knowing the structure is right gets you maybe halfway there. Being able to write the idiom from memory without a 30-second detour is the other half.
If you want live, spoken feedback on whether your reasoning sounds clear as well as correct, SpaceComplexity runs voice-based DSA mock interviews with rubric scoring across all four dimensions. The voice part matters: typing LinkedHashMap is one thing, explaining why you chose it while coding is another.
For a broader look at Kotlin's interview-specific idioms and gotchas beyond collections, see the Kotlin for Coding Interviews guide. If you're choosing between languages, Java vs Kotlin for Coding Interviews covers the practical trade-offs. Java's equivalent structures are in Java Built-In Data Structures for Coding Interviews.
Further Reading
- Kotlin Collections Overview, official docs covering the read-only/mutable interface hierarchy
- Kotlin ArrayDeque API, full method reference for the stdlib ArrayDeque
- Java PriorityQueue API, heap semantics and method signatures
- Java TreeMap API, full navigation method reference including floorKey, ceilingKey