Sparround

Strings and immutability

A string is essentially a contiguous array of characters. s[3] is O(1), s.length is O(1), iterating is O(n) — the same logic as arrays.

The key difference: in JS, Kotlin (JVM) and Dart, strings are immutable. Once created they cannot be changed. s.toUpperCase(), s.replace(...), s + "x" — none of these modify s; each returns a new string.

The reasons for immutability are practical:

  • Safe sharing — the same string can be held in many places and nobody can change it underneath you
  • Cacheable hash — the hash is computed once and stored, which makes strings ideal hash-map keys
  • String interning — literals with the same content can share one object
  • Thread safety — no synchronisation needed

Why concatenation in a loop is `O(n²)`. This is the most important practical consequence of immutability.

When you write result += word, the language allocates a new string and copies both sides in full. At step i of the loop the accumulated length is roughly i, so that step costs O(i). In total: 1 + 2 + 3 + ... + n = n(n+1)/2 — that is `O(n²)`.

The fix is the string builder approach: collect the pieces somewhere (an array or a buffer) and join them once at the end. That is O(n).

  • JS: push pieces into an array, then parts.join('')
  • Kotlin: StringBuilder or buildString { }
  • Dart: StringBuffer or parts.join()

Note: modern JS engines (V8) can rescue short += loops with a "rope" optimisation, but you cannot rely on it, and the textbook answer in an interview is O(n²).

ProblemApproachTimeSpace
ReverseTwo pointers (start and end), or convert to a char array and reverseO(n)O(n) — a new buffer is needed since strings are immutable
Palindrome checkTwo pointers moving toward the centre; no copy neededO(n)O(1)
Anagram checkCompare character frequencies (a map or a 26-slot array)O(n)O(k) — alphabet size
Anagram — the simple waySort both and compareO(n log n)O(n)
Character countingAccumulate frequencies in a hash map in one passO(n)O(k)
Longest substring without repeatsSliding window + a `Set`/`Map`O(n)O(k)

Subtleties worth knowing — mentioning these in an interview makes a difference.

  • A character is not a byte and not a code point. In JS and Dart a string is a sequence of UTF-16 code units. Emoji and some symbols take two code units (a surrogate pair), so "👍".length is 2. Azerbaijani letters (ə, ğ, ş, ç, ö, ü, ı) are in the BMP and take one unit, but case conversion is locale-sensitive (the ı/I problem is a classic bug source in Turkish and Azerbaijani).
  • Comparison: in JS === compares content. In Kotlin == (which calls equals) compares content while === compares references — the Java == trap does not exist in Kotlin. In Dart == on strings compares content.
  • Taking a substring (substring, slice) is usually O(k) because it copies — a hidden source of O(n²) when called inside a loop.

Interview tip. "Reverse a string" and "check a palindrome" are the most common warm-up questions. The interviewer is really checking three things: (1) do you know the two-pointer technique, (2) do you state the complexity, (3) do you ask about edge cases.

Ask these before writing code: do spaces and punctuation count? Is case significant? Can there be Unicode/emoji? What should an empty string return? Those questions alone lift you above most candidates.

The most common mistakes: using += in a loop and then claiming O(n); reversing the string and comparing for a palindrome (it works, but costs O(n) extra memory — two pointers is O(1)); reaching for sorting on the anagram question and never considering the O(n) frequency solution.

📚 Sources and documentation

  • Stringofficialdeveloper.mozilla.org