Kotlin String Manipulation for Coding Interviews: The Reference Guide

June 19, 202611 min read
dsaalgorithmsinterview-prepkotlin
Kotlin String Manipulation for Coding Interviews: The Reference Guide
TL;DR
  • String immutability makes + concatenation O(n²) in loops; always use StringBuilder to avoid silent O(n²) code
  • c.uppercaseChar() returns Char; c.uppercase() returns String — 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 string
  • groupingBy { 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 want
  • split() returns List<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

OperationSyntaxTime
Access chars[i]O(1)
Lengths.lengthO(1)
Substrings.substring(i, j)O(j-i)
Index of chars.indexOf(c)O(n)
Containss.contains(sub)O(n·m)
Splits.split(delim)O(n)
Reverses.reversed()O(n)
Frequency maps.groupingBy { it }.eachCount()O(n)
Filter charss.filter { ... }O(n)
Char to intc - 'a' or c.codeO(1)
To CharArrays.toCharArray()O(n)
CharArray to StringString(arr)O(n)
StringBuilder appendsb.append(c)O(1) amortized
StringBuilder reversesb.reverse()O(n)
Join listlist.joinToString("")O(n)

Know These Cold

  • String is immutable. Use StringBuilder in any loop that builds a string.
  • length is a property. No parentheses.
  • s[i] gives Char. s[i].code gives the ASCII Int. s[i] - 'a' gives the offset.
  • c.uppercaseChar() returns Char. c.uppercase() returns String. 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() plus String(arr) is the mutation pattern for in-place edits.
  • split() returns List. 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