DAY 3 / 30 · WEEK 1

Arrays & Strings

Goal of the day

Concept

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.

Diagrams

1. String immutability

str = "cat" c a t str[0] = 'b' no effect (or TypeError in strict mode) newStr = 'b' + str.slice(1) (a brand new string) b a t str itself is still "cat" — completely unchanged.

2. The mutate-while-iterating trap

Goal: remove every even number from [2, 4, 6, 8, 10] — expected result: [] 2 4 6 8 10 Buggy forEach+splice result: 4 8 ← left behind by accident! Every splice() shifts later elements left by one, but the loop's index keeps climbing — so every other element gets skipped.

Interactive visualizations

current index being removed / has no effect new / correct result untouched

1. String immutability, step by step

Press Play or Step to begin.

2. The mutate-while-iterating bug, live

Press Play or Step to begin.

The code

// --- 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:

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

iarr beforeval (arr[i])actionarr after
0[2,4,6,8,10]2even → splice(0,1)[4,6,8,10]
1[4,6,8,10]6even → splice(1,1)[4,8,10]
2[4,8,10]10even → 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.

Complexity

OperationTimeSpaceWhy
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 / includesO(n)O(1)Worst case, scans the whole string looking for a match.
str1 + str2O(n)O(n)n = combined length — building the new string touches every character.
str += x inside an n-iteration loopO(n²) worst caseO(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.

Interview corner

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.

Practice problems

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

Reverse an array of characters s in place.

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.

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.

Quiz

1. In ordinary (non-strict) code, what does this print? let s = "cat"; s[0] = "b"; console.log(s);

2. What does "hello".slice(1, 3) return?

3. What's the time complexity of str1 + str2?

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

5. What does "abc".split('') return?