Day 3: Arrays & Strings
DAY 3 / 30 · WEEK 1
Goal of the day
Section titled “Goal of the day”- 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.
Concept
Section titled “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
Section titled “Diagrams”1. String immutability
Section titled “1. String immutability”2. The mutate-while-iterating trap
Section titled “2. The mutate-while-iterating trap”Interactive visualizations
Section titled “Interactive visualizations”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 arraysstr[0]; // "h" — index access, O(1)str.charAt(0); // "h" — same thing, older APIstr.slice(0, 5); // "hello" — start inclusive, end EXCLUSIVE (same as arrays!)str.indexOf("world"); // 6 — position of first match, -1 if not foundstr.includes("wor"); // true — just "does this exist", no positionstr.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]); // [] — correctLine 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 withstr.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 aclassbody, it throws aTypeErrorinstead.removeEvensBuggy—splice(i, 1)removes the element at indexiand shifts everything after it one slot left. ButforEach’s internal counter still moves toi + 1next — which is now a different element than you’d expect, so the one that shifted into slotinever gets checked.removeEvensFixed—filterbuilds 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.
Complexity
Section titled “Complexity”| 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.
Interview corner
Section titled “Interview corner”-
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 aclassbody — increasingly the default in real codebases — that same line throws aTypeError. 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
forEachor a forwardforloop? 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 forfilterfirst.
- ⚠ 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
-
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.
Practice problems
Section titled “Practice problems”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;}-
In ordinary (non-strict) code, what does this print?
let s = "cat"; s[0] = "b"; console.log(s);- “cat” ✓
- “bat”
- Throws an error
-
What does
"hello".slice(1, 3)return?- “hel”
- “el” ✓
- “ell”
- “llo”
-
What’s the time complexity of
str1 + str2?- O(1)
- O(n) ✓
- O(n²)
-
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
-
What does
"abc".split('')return?- [‘a’, ‘b’, ‘c’] ✓
- “abc”
- Throws an error