Kotlin String Manipulation for Coding Interviews: The Reference Guide

- String immutability makes
+concatenation O(n²) in loops; always useStringBuilderto avoid silent O(n²) code c.uppercaseChar()returnsChar;c.uppercase()returnsString— mix them up and get a type error mid-interview- Char arithmetic (
c - 'a',c.code) builds bucket arrays faster than hash maps for ASCII-constrained problems toCharArray()+String(arr)is the mutation pattern when you need in-place edits on an immutable stringgroupingBy { it }.eachCount()is the one-liner frequency map for anagram detection and permutation checks==is content equality in Kotlin unlike Java — using===for string comparison will invite a follow-up you don't wantsplit()returnsList<String>, not an array; use.toTypedArray()if you need indexed mutation
Kotlin promises to be the language that finally makes Java bearable. And it mostly delivers. The string API is genuinely cleaner, the syntax is nicer, you can stop typing .equals() like it's 2004. Then you write c.uppercase() expecting a Char back and get a String. Interview over.
The API hides enough surprises to hurt you under time pressure. This guide covers what you actually need: the methods, the complexities, the mutation pattern, and the gotchas that cost candidates points.
Strings Are Immutable, and That Costs You in Loops
String in Kotlin is immutable. Every + concatenation creates a new object. If you concatenate inside a loop, you're writing O(n²) code that looks like O(n), and an interviewer will catch it.
// O(n²), a new string allocated every iteration var result = "" for (c in input) { result += c } // O(n), single allocation, append in place val sb = StringBuilder() for (c in input) { sb.append(c) } val result = sb.toString()
The first version doesn't look slow. It reads fine. It compiles fine. It runs fine on small inputs. Then you hand-trace it in an interview and realize you're copying the entire string on every iteration. A new string of length 1, then 2, then 3, then... n total allocations averaging n/2 characters each. That's n²/2 total work. The interviewer writes "O(n²)" in their notes while you're still staring at it.
This is the first thing you'll get pushed back on if you miss it. Know it cold before your screen.
For the full picture on why immutable strings make concatenation expensive, see What Is a StringBuilder?.
The Properties and Methods You'll Actually Use
One Kotlin quirk upfront: length is a property, not a method. No parentheses. Your muscle memory from Java will betray you exactly once during a screen.
val s = "hello" s.length // 5, not s.length() s[0] // 'h', same as s.get(0), O(1) s.substring(1, 4) // "ell", end index is exclusive s.indexOf('l') // 2, first occurrence, -1 if missing s.lastIndexOf('l') // 3 s.contains("ell") // true s.startsWith("he") // true s.endsWith("lo") // true
The core transformation methods:
s.lowercase() // "hello", current API (not toLowerCase()) s.uppercase() // "HELLO" s.reversed() // "olleh", returns a new String, O(n) s.trim() // strips leading and trailing whitespace s.replace('l', 'r') // "herro" s.repeat(3) // "hellohellohello"
Splitting gives you a List<String>, not an array. This matters when you try to assign back into it.
"a,b,c".split(",") // ["a", "b", "c"] "one two three".split("\\s+".toRegex()) // ["one", "two", "three"]
Kotlin's functional string operations work directly on the string, no conversion needed:
s.filter { it.isLetter() } // remove non-letter characters s.count { it == 'a' } // count 'a' occurrences s.all { it.isDigit() } // true only if every char is a digit s.any { it.isUpperCase() } // true if any char is uppercase s.none { it == ' ' } // true if no spaces
groupingBy { it }.eachCount() is the one-liner that builds a frequency map in a single pass.
val freq: Map<Char, Int> = s.groupingBy { it }.eachCount()
That covers most frequency-based problems (anagram detection, character counting, permutation checks) with almost no boilerplate. One line instead of a four-line loop. Use it.
Char Arithmetic Beats Hash Maps for ASCII Input
Kotlin's Char type supports arithmetic directly. This is how you build bucket arrays and compute character offsets, which comes up constantly in sliding window and frequency problems.
'b' - 'a' // 1 (Int), offset from 'a' 'a' + 3 // 'd' (Char), advance by 3 'A'.code // 65, ASCII value as Int 'a'.code // 97 c - 'a' // offset from 'a', valid for lowercase ASCII input c.isLetter() // true/false c.isDigit() // true/false c.isWhitespace() // true/false
Use c.code instead of c.toInt(). Both work, but .code is explicit and modern. The interviewer has seen enough Java refugees using deprecated APIs.
The bucket array pattern is faster than a hash map when the input is known to be ASCII:
val freq = IntArray(26) for (c in s) freq[c - 'a']++
O(1) space relative to the alphabet, O(n) time. Cleaner than groupingBy when you need repeated lookups during a sliding window. If your problem says "lowercase letters only," this is the move.
Need to Mutate? Convert to CharArray First
When you need to mutate individual characters, convert to CharArray first. Strings are immutable. CharArrays are not.
val arr: CharArray = s.toCharArray() arr[0] = 'H' val modified = String(arr) // back to String
CharArray is backed by a primitive char[] on the JVM. Array<Char> uses boxed Character objects. Always use CharArray in interviews. The difference matters at scale and signals that you know what's actually happening at the JVM level.
The two-pointer reversal pattern becomes three lines:
fun reverse(s: String): String { val arr = s.toCharArray() var left = 0; var right = arr.size - 1 while (left < right) { arr[left] = arr[right].also { arr[right] = arr[left] } left++; right-- } return String(arr) }
This is the building block for reversing words in a sentence, rotating strings, and similar problems. Learn it once. Use it everywhere.
StringBuilder: When and How
Use StringBuilder whenever you're building a result character by character, reversing output, or doing repeated inserts and deletes. It's the right tool for about 60% of string problems.
val sb = StringBuilder() sb.append('a') // append a char sb.append("world") // append a string sb.insert(0, "hello ") // insert at index, O(n) sb.deleteCharAt(0) // remove char at index, O(n) sb.delete(0, 3) // remove range [start, end), O(n) sb.reverse() // reverse in place, O(n) sb.setCharAt(0, 'H') // mutate at index, O(1) sb[0] // read at index, O(1) sb.length // length, O(1) sb.toString() // convert to String
sb.reverse() is the cleanest way to check palindromes without a two-pointer loop:
fun isPalindrome(s: String): Boolean { val clean = s.filter { it.isLetterOrDigit() }.lowercase() return clean == StringBuilder(clean).reverse().toString() }
That handles the "Valid Palindrome" variant (alphanumeric only, case-insensitive) in four lines. Yes, it allocates. No, the interviewer does not care about that allocation.
What These Patterns Look Like in Kotlin
See String Internals, Immutability, and Interview Patterns for the full treatment. Here's how they translate.
Anagram Check
fun isAnagram(s: String, t: String): Boolean { if (s.length != t.length) return false val freq = IntArray(26) for (c in s) freq[c - 'a']++ for (c in t) freq[c - 'a']-- return freq.all { it == 0 } }
The freq.all { it == 0 } call is cleaner than a manual loop. Both are O(n).
Sliding Window on Strings
The window state usually lives in a frequency array or map. Expand right, shrink left, track the invariant.
fun lengthOfLongestSubstring(s: String): Int { val last = mutableMapOf<Char, Int>() var left = 0 var max = 0 for (right in s.indices) { val c = s[right] if (c in last && last[c]!! >= left) { left = last[c]!! + 1 } last[c] = right max = maxOf(max, right - left + 1) } return max }
Parsing and Conversion
"42".toInt() // 42 "3.14".toDouble() // 3.14 42.toString() // "42" "abc".toIntOrNull() // null, safe version, doesn't throw
toIntOrNull() is useful when the input might not be a valid number. It's safer than wrapping toInt() in a try-catch during an interview. A try-catch in a 45-minute session reads as "I'm not sure what this does."
Joining Collections Back to Strings
listOf("a", "b", "c").joinToString("") // "abc" listOf("hello", "world").joinToString(" ") // "hello world" listOf(1, 2, 3).joinToString(", ") // "1, 2, 3" charArrayOf('a', 'b').joinToString("") // "ab"
joinToString is cleaner than a StringBuilder loop when you already have a list. Don't build a StringBuilder just to join a list you already have.
The Gotchas That Actually Cost You Points
For the full list, see Kotlin Coding Interview Gotchas. These hit hardest in string problems, usually at the worst possible moment.
c.uppercase() returns String, not Char. This changed in Kotlin 1.5 and it breaks code silently. You'll write something that looks completely reasonable, assign the result to a Char, and get a type error that takes thirty seconds to diagnose. Thirty seconds you don't have.
val upper: String = 'a'.uppercase() // "A", String, not Char val upper: Char = 'a'.uppercaseChar() // 'A', what you want for char ops
Use uppercaseChar() and lowercaseChar() when you need a Char back. The method exists. Just use it.
== is content equality in Kotlin, not reference equality. This is the opposite of Java.
val a = "hello" val b = "hello" a == b // true, compares content (calls .equals()) a === b // true on JVM due to string pooling, but don't rely on this
If you come from Java, you might reach for .equals() out of habit. In Kotlin, == is correct. Using === for string comparison in an interview will get a follow-up question you don't want. The follow-up is "what does === actually do here?" and the answer will take longer than the original problem.
toLowerCase() and toUpperCase() are deprecated. They still compile against the JVM target, but modern Kotlin uses lowercase() and uppercase(). Using the deprecated versions signals you're working from outdated notes or a tutorial from 2019. Neither is a great look.
split() returns List<String>, not Array<String>. You can't reassign elements. If you need mutation, use .toTypedArray() or work on the list directly.
substring() throws StringIndexOutOfBoundsException on bad bounds. There's no silent truncation. Check your indices, especially when computing them from the input length.
Null safety bites string return types. Methods like find() return Char?. The Elvis operator handles this cleanly, and forgetting it is a one-way ticket to a NullPointerException in a language designed to prevent them.
val firstDigit = s.find { it.isDigit() } ?: return -1
Kotlin String Manipulation Cheat Sheet
| Operation | Syntax | Time |
|---|---|---|
| Access char | s[i] | O(1) |
| Length | s.length | O(1) |
| Substring | s.substring(i, j) | O(j-i) |
| Index of char | s.indexOf(c) | O(n) |
| Contains | s.contains(sub) | O(n·m) |
| Split | s.split(delim) | O(n) |
| Reverse | s.reversed() | O(n) |
| Frequency map | s.groupingBy { it }.eachCount() | O(n) |
| Filter chars | s.filter { ... } | O(n) |
| Char to int | c - 'a' or c.code | O(1) |
| To CharArray | s.toCharArray() | O(n) |
| CharArray to String | String(arr) | O(n) |
| StringBuilder append | sb.append(c) | O(1) amortized |
| StringBuilder reverse | sb.reverse() | O(n) |
| Join list | list.joinToString("") | O(n) |
Know These Cold
Stringis immutable. UseStringBuilderin any loop that builds a string.lengthis a property. No parentheses.s[i]givesChar.s[i].codegives the ASCIIInt.s[i] - 'a'gives the offset.c.uppercaseChar()returnsChar.c.uppercase()returnsString. Know which you need.==is content equality.===is reference equality. You almost never want===on strings.groupingBy { it }.eachCount()is the one-liner frequency map.toCharArray()plusString(arr)is the mutation pattern for in-place edits.split()returnsList.find()returns nullable. Handle both.
Knowing these methods is half the job. Using them cleanly under pressure is the other half, and that only comes from spoken practice. SpaceComplexity runs voice-based DSA mock interviews with rubric-based feedback on approach, communication, and edge-case handling. Try a session before your next screen.
Further Reading
- Kotlin Strings Documentation, official guide covering string templates, multiline strings, and the full API
- Kotlin String API Reference, complete method signatures from the stdlib
- Kotlin StringBuilder Reference, full StringBuilder method list
- String Data Structure, GeeksforGeeks, common interview patterns with worked examples
- String (Computer Science), Wikipedia, background on immutability, encoding, and why string design decisions were made this way