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:
StringBuilderorbuildString { } - Dart:
StringBufferorparts.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²).
| Problem | Approach | Time | Space |
|---|---|---|---|
| Reverse | Two pointers (start and end), or convert to a char array and reverse | O(n) | O(n) — a new buffer is needed since strings are immutable |
| Palindrome check | Two pointers moving toward the centre; no copy needed | O(n) | O(1) |
| Anagram check | Compare character frequencies (a map or a 26-slot array) | O(n) | O(k) — alphabet size |
| Anagram — the simple way | Sort both and compare | O(n log n) | O(n) |
| Character counting | Accumulate frequencies in a hash map in one pass | O(n) | O(k) |
| Longest substring without repeats | Sliding 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
"👍".lengthis 2. Azerbaijani letters (ə, ğ, ş, ç, ö, ü, ı) are in the BMP and take one unit, but case conversion is locale-sensitive (theı/Iproblem is a classic bug source in Turkish and Azerbaijani). - Comparison: in JS
===compares content. In Kotlin==(which callsequals) 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 usuallyO(k)because it copies — a hidden source ofO(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