All guides
02

Foundations

Strings

Arrays of characters with their own quirks: immutability, encoding, and O(n) concatenation traps.

Time: Access O(1), Search O(n), Concatenation O(n) Space: O(n)
Concept Visualization: Strings

How It Works

A string is, conceptually, an ordered sequence of characters, very similar to an array, but with one crucial difference: strings in JavaScript are immutable. You can read any character by index in O(1), but you cannot change a character in place; any "modification" (concatenation, slicing, replacing) creates a brand-new string. That immutability is why naive string-building in a loop can quietly become an O(n²) operation: each += allocates a new string of increasing size and copies the old contents into it.

JavaScript strings are UTF-16 encoded internally, which means .length counts UTF-16 code units, not necessarily "characters" as a human would count them: emoji and many non-Latin characters can be represented by surrogate pairs (two code units), which trips up naive indexing.

Core operations and their complexity:

  • Access by index (str[i], str.charAt(i)): O(1).
  • Search substring (str.includes, indexOf): O(n·m) naively (n = string length, m = pattern length), though engines often use optimized substring search.
  • Concatenation (str + str2): O(n) per operation, since a new string is allocated.
  • Slicing (str.slice(a, b)): O(k) where k is the length of the slice.
  • Building a string from parts in a loop: O(n²) if done with repeated +=; O(n) if done with an array + .join('').

Because strings are immutable and array-like, most array techniques (two pointers, sliding window, hashing character counts) transfer directly.

// Strings behave like read-only arrays of characters
const word = "leetcode";

// O(1) access
console.log(word[3]); // "t"

// Immutable: this does NOT change `word`, it returns a new string
const upper = word.toUpperCase();

// Efficient way to build a string from many pieces: array + join
function buildCsv(items) {
  const parts = [];
  for (const item of items) {
    parts.push(String(item));
  }
  return parts.join(","); // O(n), not O(n^2)
}

// Common pattern: character frequency map for anagram checks
function isAnagram(a, b) {
  if (a.length !== b.length) return false;
  const counts = new Map();
  for (const ch of a) counts.set(ch, (counts.get(ch) || 0) + 1);
  for (const ch of b) {
    if (!counts.has(ch)) return false;
    counts.set(ch, counts.get(ch) - 1);
    if (counts.get(ch) === 0) counts.delete(ch);
  }
  return counts.size === 0;
}

isAnagram("listen", "silent"); // true

Common Mistakes

  • Building strings with += inside a large loop: each concatenation allocates a new string, turning what should be O(n) into O(n²). Use an array and .join() instead.
  • Assuming .length equals visible character count: emoji, combining accents, and many non-Latin scripts use surrogate pairs, so "👍".length === 2, not 1.
  • Trying to mutate a string in place: str[0] = "x" silently fails (or throws in strict mode); strings are immutable, you must build a new one.
  • Forgetting strings are case- and whitespace-sensitive by default: comparing user input without .toLowerCase()/.trim() when it's actually needed.
  • Confusing reference equality intuition from objects: string primitives compare by value with ===, unlike arrays/objects, which trips people up going the other direction.

Pattern Summary

Treat strings as immutable, array-like sequences: index access is free, but every mutation is a new allocation, so batch your string-building through an array and join. The edge in interviews comes from recognizing that classic array techniques (two pointers, sliding window, frequency maps) apply directly to strings once you internalize the immutability cost.

Advertisement