Skip to content

Day 3: Arrays & Strings

DAY 3 / 30 · WEEK 1

  • Use the core string operations — slice, indexOf, includes, split, charAt — as fluently as you use array operations.
  • Understand why strings are immutable in JS, and how that changes the way you build new strings.
  • Spot and fix the #1 array mistake: mutating an array while you’re looping over it.

An array is like a whiteboard — you can erase and rewrite any square directly, in place. A string is like a printed page — you can never edit a single letter on it. Every “change” to a string actually prints a brand new page. That’s why str[0] = 'b' does nothing in ordinary JS — no error, no effect, it’s silently ignored, because strings are immutable. (In "use strict" code, ES modules, or inside a class body, that same line throws a TypeError instead — same root cause, stricter enforcement. Either way, it never actually changes the string.) To change a string, you always build a new one: 'b' + str.slice(1).

The second big idea today is a trap, not a feature: modifying an array while you loop over it. When you splice() an item out mid-loop, every element after it shifts down one index — but your loop counter keeps counting up regardless. The result: you silently skip checking the very next element. This bites experienced developers, not just beginners, because the code runs without any error — it just gives a quietly wrong answer.

str = “cat”catstr[0] = ‘b’no effect (or TypeError in strict mode)newStr = ‘b’ + str.slice(1) (a brand new string)batstr itself is still “cat” — completely unchanged. Goal: remove every even number from [2, 4, 6, 8, 10] — expected result: []246810Buggy forEach+splice result:48← left behind by accident!Every splice() shifts later elements left by one, but the loop’sindex keeps climbing — so every other element gets skipped.

Strings are immutable: str[0] = 'b' has no effect. Build a new string instead.

Press Play or Shuffle to begin.

Goal: remove every even number. Buggy forEach+splice result (expected: [])

Press Play or Shuffle to begin.

// --- core string operations ---
const str = "hello world";
str.length; // 11 — same idea as arrays
str[0]; // "h" — index access, O(1)
str.charAt(0); // "h" — same thing, older API
str.slice(0, 5); // "hello" — start inclusive, end EXCLUSIVE (same as arrays!)
str.indexOf("world"); // 6 — position of first match, -1 if not found
str.includes("wor"); // true — just "does this exist", no position
str.split(" "); // ["hello", "world"] — string -> array
["a","b"].join("-"); // "a-b" — array -> string (the reverse)
// --- strings are immutable: this never changes s ---
let s = "cat";
s[0] = "b";
console.log(s); // still "cat" (sloppy mode: silently ignored;
// "use strict" / modules / classes: throws TypeError instead)
// --- to "change" a string, build a new one ---
const newS = "b" + s.slice(1); // "bat" — a brand new string
// --- THE TRAP: mutating an array while looping over it ---
function removeEvensBuggy(arr) {
arr.forEach((val, i) => {
if (val % 2 === 0) arr.splice(i, 1); // shifts everything after i left!
});
return arr;
}
removeEvensBuggy([2, 4, 6, 8, 10]); // [4, 8] — WRONG, expected []
// --- THE FIX: use a non-mutating method instead ---
function removeEvensFixed(arr) {
return arr.filter((val) => val % 2 !== 0); // builds a NEW array, no index shifting
}
removeEvensFixed([2, 4, 6, 8, 10]); // [] — correct

Line by line:

  • str.slice(0, 5) — just like array slicing: the end index is exclusive. This is the #1 off-by-one trap with strings.
  • str.split(" ") / arr.join("-") — your two converters between strings and arrays. Almost every string interview problem starts with str.split('') to turn it into an array you can index and mutate freely.
  • s[0] = "b" — assignment to a string index never changes the string. In ordinary (sloppy-mode) code it’s silently ignored — no error, which is exactly what makes this bug sneaky. In "use strict" code, ES modules, or a class body, it throws a TypeError instead.
  • removeEvensBuggysplice(i, 1) removes the element at index i and shifts everything after it one slot left. But forEach’s internal counter still moves to i + 1 next — which is now a different element than you’d expect, so the one that shifted into slot i never gets checked.
  • removeEvensFixedfilter builds a brand new array by testing every original element exactly once. No indices ever shift out from under you.

Dry run: removeEvensBuggy([2, 4, 6, 8, 10])

Section titled “Dry run: removeEvensBuggy([2, 4, 6, 8, 10])”
i arr before val (arr[i]) action arr after
0 [2,4,6,8,10] 2 even → splice(0,1) [4,6,8,10]
1 [4,6,8,10] 6 even → splice(1,1) [4,8,10]
2 [4,8,10] 10 even → splice(2,1) [4,8]
3 [4,8] undefined (out of bounds) skipped — no element there [4,8]
4 [4,8] undefined (out of bounds) skipped — no element there [4,8]

4 and 8 were never re-checked after the array shifted under the loop — they survive by accident, not because they’re odd.

Operation Time Space Why
str[i] / str.charAt(i) O(1) O(1) Same direct-index trick as arrays.
str.slice(a, b) O(b - a) O(b - a) Has to copy every character in the range into a new string.
str.indexOf / includes O(n) O(1) Worst case, scans the whole string looking for a match.
str1 + str2 O(n) O(n) n = combined length — building the new string touches every character.
str += x inside an n-iteration loop O(n²) worst case O(n) Naively, each concatenation copies the whole string built so far. Some engines optimize repeated concatenation internally — but O(n²) is still the safe assumption to state in an interview.
arr.splice(i, 1) O(n) O(1) Same reason as shift() — everything after index i shifts left.

The str += x row is why, when you’re building a huge string piece by piece in a loop, it’s often faster to push pieces into an array and .join('') them once at the end.

  • What happens when you run str[0] = 'x'? It never changes the string either way — but whether it errors depends on mode. In ordinary (sloppy-mode) script code it’s a silent no-op: JS treats it like trying to write to a read-only property and just ignores it. In "use strict" code, ES modules, or a class body — increasingly the default in real codebases — that same line throws a TypeError. Know both, and say “it depends on strict mode” rather than asserting either one universally.

  • How would you reverse a string in JS? str.split('').reverse().join('') is the idiomatic one-liner — split into an array (which IS mutable), reverse it in place, join back into a string.

    • Common follow-up: “Can you do it without the built-ins?” — two pointers from both ends swapping into a new array/char list, O(n) time, O(n) space (you can’t avoid the space since you can’t mutate the original string). We formalize the two-pointer pattern itself on Day 4.
  • What’s wrong with removing items from an array inside a forEach or a forward for loop? Explained in today’s Concept and Diagram — indices shift under you and you silently skip elements.

    • Trap: “just loop backwards instead” is a real fix (shifting elements you’ve already passed doesn’t affect indices you haven’t reached yet), but filter() is almost always clearer and safer — mention both, but reach for filter first.
  • What’s the time complexity of str1 + str2? O(n), where n is the combined length — every character has to be copied into the new string.

How to talk through it out loud: when you catch yourself writing arr.splice() or arr.push() inside a loop that’s also iterating over that same array, say so out loud: “I need to be careful here — I’m mutating the array I’m looping over, so I’ll either loop backwards or switch to filter/map to avoid shifting indices.” Naming the risk before it becomes a bug is exactly what interviewers are listening for.

1. Reverse String (Easy — LeetCode: Reverse String)

Reverse an array of characters s in place.

Show hint

Swap the first and last characters, then move one step inward from each end, and repeat until the two pointers meet.

Show solution
function reverseString(s) {
let left = 0, right = s.length - 1;
while (left < right) {
[s[left], s[right]] = [s[right], s[left]]; // swap via destructuring
left++;
right--;
}
}

2. Valid Palindrome (Easy — LeetCode: Valid Palindrome)

Given a string, return true if it reads the same forwards and backwards, ignoring non-alphanumeric characters and case.

Show hint

Clean the string first (lowercase, strip non-alphanumeric characters with a regex), then compare it to its own reverse.

Show solution
function isPalindrome(s) {
const clean = s.toLowerCase().replace(/[^a-z0-9]/g, '');
return clean === clean.split('').reverse().join('');
}

3. String Compression (Medium — LeetCode: String Compression)

Compress an array of characters in place: consecutive repeated characters become the character followed by the count (a group of 1 is written with no count). Return the new length.

Show hint

Use two pointers: one (read) scans through looking for where a run of repeats ends, the other (write) writes the compressed result back into the same array.

Show solution
function compress(chars) {
let write = 0, read = 0;
while (read < chars.length) {
const char = chars[read];
let count = 0;
while (read < chars.length && chars[read] === char) { read++; count++; }
chars[write++] = char;
if (count > 1) {
for (const digit of String(count)) chars[write++] = digit;
}
}
return write;
}
  1. In ordinary (non-strict) code, what does this print? let s = "cat"; s[0] = "b"; console.log(s);

    • “cat” ✓
    • “bat”
    • Throws an error
  2. What does "hello".slice(1, 3) return?

    • “hel”
    • “el” ✓
    • “ell”
    • “llo”
  3. What’s the time complexity of str1 + str2?

    • O(1)
    • O(n) ✓
    • O(n²)
  4. What goes wrong with arr.forEach((v, i) => { if (cond) arr.splice(i, 1); })?

    • It throws an error
    • It silently skips checking some elements ✓
    • It loops forever
  5. What does "abc".split('') return?

    • [‘a’, ‘b’, ‘c’] ✓
    • “abc”
    • Throws an error