Searching & Sorting Interview Questions & Answers
31 questions Updated 2026-06-18
JavaScript array searching and sorting interview questions — indexOf, includes, find, the sort() coercion gotcha, numeric comparators, stable sort, toSorted and sorting objects.
Read the in-depth guideJavaScript Array Searching & Sorting — indexOf, find, the sort() Gotcha and ComparatorsindexOf returns the index of the first element strictly equal
(===) to the search value, or -1 if it is not present.
const a = ['x', 'y', 'z']
a.indexOf('y') // 1
a.indexOf('q') // -1 the "not found" sentinel
The classic pitfall is treating -1 as falsy: if (a.indexOf(x)) is
wrong because index 0 is also falsy. Compare explicitly with
!== -1, or prefer includes when you only need a boolean.
indexOf searches front-to-back and returns the first match;
lastIndexOf searches back-to-front and returns the last match.
Both use === and return -1 when absent.
const a = [1, 2, 3, 2, 1]
a.indexOf(2) // 1
a.lastIndexOf(2) // 3 last occurrence
Each also takes an optional start index. A subtle point: for
lastIndexOf the second argument is where to begin searching
backwards, so a.lastIndexOf(2, 2) searches indices 2->0.
Use includes when you only care whether a value exists — it
returns a boolean and reads clearly. Use indexOf when you need the
position.
if (tags.includes('urgent')) { /* ... */ } // clear intent
const at = tags.indexOf('urgent') // when you need where
The other reason to prefer includes: it finds NaN, which indexOf
cannot (see the next question).
indexOf compares with ===, and NaN === NaN is false — so it can
never match. includes uses the SameValueZero algorithm, which
treats NaN as equal to NaN.
[NaN].indexOf(NaN) // -1 can't find it
[NaN].includes(NaN) // true
SameValueZero is also why includes(0) matches both +0 and -0. If
you must locate a NaN index, use findIndex(Number.isNaN).
find returns the first element that satisfies the predicate (or
undefined); filter returns a new array of all matches.
users.find(u => u.id === 7) // the one user, or undefined
users.filter(u => u.active) // array of every active user
Use find for "give me the one I want" — it also short-circuits on the
first match, so it's cheaper than filter()[0] which scans the whole
array.
indexOf searches by value equality; findIndex searches by a
predicate function, so it works for objects and complex conditions.
const i = users.indexOf(someUser) // needs the exact reference
const j = users.findIndex(u => u.id === 7) // search by a property
Both return -1 when nothing matches. Reach for findIndex whenever the
match is "the element where some condition holds" rather than "this exact
value."
They are the back-to-front counterparts of find/findIndex: they walk
the array from the end and return the first matching element / index.
const nums = [1, 8, 3, 9, 2]
nums.findLast(n => n > 4) // 9 last match, not first
nums.findLastIndex(n => n > 4) // 3
Before these (ES2023) you'd reverse a copy or loop manually. They're handy for "most recent" lookups in append-ordered data like logs.
some answers "does any element match?" and every answers
"do all elements match?" — both return a boolean and short-circuit.
nums.some(n => n < 0) // true if there's at least one negative
nums.every(n => n > 0) // true only if all are positive
some stops at the first true; every stops at the first false. A
gotcha: every on an empty array is true (vacuous truth) and some
on an empty array is false.
Without a comparator, sort converts elements to strings and compares
them by UTF-16 code unit — so numbers sort lexicographically.
[10, 1, 2, 20].sort() // [1, 10, 2, 20] "10" < "2"
[10, 1, 2, 20].sort((a,b)=>a-b) // [1, 2, 10, 20]
Always pass a comparator for numbers. This string-coercion default trips up nearly everyone at least once.
A comparator returns a negative number if a should come first, a
positive number if b should, and 0 to leave order unchanged.
arr.sort((a, b) => a - b) // ascending
arr.sort((a, b) => b - a) // descending
The a - b trick works only for numbers. Don't return a boolean
(a > b) — coerced to 0/1 it never yields a negative, so the sort is
broken.
Yes — sort sorts in place and returns the same array reference.
That can surprise callers sharing the array.
const a = [3, 1, 2]
const b = a.sort((x,y)=>x-y)
b === a // true original mutated
const sorted = [...a].sort((x,y)=>x-y) // copy first
const safe = a.toSorted((x,y)=>x-y) // ES2023, returns new array
Prefer toSorted (or spread-then-sort) in code that values immutability,
e.g. React state.
Since ES2019 sort is guaranteed stable: elements the comparator
treats as equal keep their original relative order. This makes
multi-pass sorting reliable.
// sort by name, then by age — age becomes the primary key,
// ties broken by the earlier name order
people.sort((a,b)=>a.name.localeCompare(b.name))
.sort((a,b)=>a.age - b.age) // stable keeps name order within an age
Before ES2019, engines like V8 used an unstable sort for large arrays, so this pattern wasn't portable.
Compare the property inside the comparator. Subtract for numbers, use
localeCompare for strings.
users.sort((a, b) => a.age - b.age) // numeric field
users.sort((a, b) => a.name.localeCompare(b.name)) // string field
Don't use a.name > b.name directly — it returns a boolean and breaks the
comparator contract. Remember it mutates, so copy first if needed.
Evaluate keys in priority order and return the first non-zero comparison.
people.sort((a, b) =>
a.lastName.localeCompare(b.lastName) || // primary
a.firstName.localeCompare(b.firstName) || // tiebreaker
a.age - b.age // final tiebreaker
)
The || chain works because a 0 (equal) is falsy, so it falls through
to the next key. Clean and avoids nested ifs.
Comparing strings with </> uses raw UTF-16 code units, which mishandles
accents and locale rules. localeCompare sorts the way humans expect
for a given language.
['é', 'a', 'z'].sort() // ['a', 'z', 'é']
['é', 'a', 'z'].sort((a,b)=>a.localeCompare(b)) // ['a', 'é', 'z']
For large arrays, build a reusable Intl.Collator and pass its compare
method — it's much faster than calling localeCompare repeatedly.
Either lowercase both sides, or use localeCompare with the
sensitivity: 'base' option (which also ignores accents).
arr.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
// or
arr.sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: 'base' })) //
The Intl option is preferable: lowercasing can be wrong for some
languages (e.g. Turkish dotless İ).
Yes — reverse reverses in place and returns the same array. The
ES2023 immutable version is toReversed.
const a = [1, 2, 3]
a.reverse() // a is now [3, 2, 1] mutated
const b = a.toReversed() // new array, a untouched
To reverse-sort numbers, you don't need reverse at all — just flip the
comparator: sort((x,y)=>y-x).
It's the start index. Searching begins there and continues to the end. Negative values count from the end.
const a = ['a', 'b', 'a', 'b']
a.indexOf('a', 1) // 2 (skips index 0)
a.includes('a', -1) // false (only looks at last element)
Useful for finding all occurrences in a loop: keep calling
indexOf(x, lastFound + 1) until it returns -1.
There's no built-in, but reduce collects them in one pass, or map +
filter.
const evens = nums.reduce((acc, n, i) => {
if (n % 2 === 0) acc.push(i)
return acc
}, []) // array of indices
Avoid the naive nums.map((n,i)=> n%2===0 ? i : -1).filter(i=>i!==-1) —
it's two passes and the -1 sentinel is fragile if 0 is a valid index.
includes/indexOf are O(n) linear scans. If the array is already
sorted and you search it many times, a binary search is O(log n).
function bsearch(arr, target) {
let lo = 0, hi = arr.length - 1
while (lo <= hi) {
const mid = (lo + hi) >> 1
if (arr[mid] === target) return mid
arr[mid] < target ? (lo = mid + 1) : (hi = mid - 1)
}
return -1
}
The pitfall: the array must stay sorted, and (lo+hi)>>1 assumes
indices small enough to avoid overflow (fine in practice for arrays).
The default sort stringifies everything, giving surprising order; a numeric
comparator on mixed types produces NaN comparisons that leave order
effectively undefined.
[3, '1', 2].sort() // ['1', 2, 3] — all compared as strings
[3, 'x', 2].sort((a,b)=>a-b) // 'x'-num is NaN, order unreliable
Normalize types before sorting (e.g. Number(x)), or filter out invalid
entries first. Mixed-type arrays are usually a data-modeling smell.
sort moves all undefined values to the end without calling the
comparator on them, and holes in sparse arrays go after even those.
[3, undefined, 1].sort((a,b)=>a-b) // [1, 3, undefined]
So you can't reliably place undefined via a comparator — the engine
handles them specially. Strip or default them beforehand if you need
specific placement.
includes uses identity (SameValueZero), so two distinct objects with the
same contents are not equal.
const arr = [{ id: 1 }]
arr.includes({ id: 1 }) // false different reference
arr.some(o => o.id === 1) // true compare by value
For "contains an object like this," use some/find with a predicate, not
includes/indexOf.
They are ES2023 non-mutating versions of sort, reverse, splice,
and bracket assignment — each returns a new array.
a.toSorted((x,y)=>x-y) // sorted copy
a.toReversed() // reversed copy
a.toSpliced(1, 2) // copy with items removed/inserted
a.with(0, 'new') // copy with index 0 replaced
They make immutable updates concise — especially with, which replaces the
old [...a.slice(0,i), val, ...a.slice(i+1)] dance.
Repeated includes/indexOf lookups are O(n) each — O(n·m) overall.
A Set gives O(1) membership tests.
const seen = new Set(bigArray)
queries.filter(q => seen.has(q)) // O(1) per lookup
Rule of thumb: if you search the same collection more than a handful of
times, build a Set/Map index once and query that instead.
A Set dedupes and preserves first-seen insertion order in one line.
const unique = [...new Set(arr)] // order preserved
For objects (reference equality won't help), dedupe by a key with a Map:
const byId = [...new Map(users.map(u => [u.id, u])).values()]
The comparator runs O(n log n) times, so any expensive computation inside it is repeated constantly. Precompute keys once with a Schwartzian transform.
const sorted = items
.map(x => ({ x, key: expensiveKey(x) })) // compute once
.sort((a, b) => a.key - b.key)
.map(o => o.x) // unwrap
This trades a little memory for far fewer key computations than calling
expensiveKey inside sort.
It returns undefined. Destructuring or accessing properties on the
result without a guard then throws.
const u = users.find(u => u.id === 99)
u.name // TypeError if not found
u?.name // optional chaining
const { name } = u ?? {} // safe default
Always treat find's result as possibly undefined.
They share a name and a -1-on-miss convention, but operate on different
things. Array indexOf matches an element by ===; string
indexOf finds a substring.
['ab', 'cd'].indexOf('b') // -1 (no element equals 'b')
'abcd'.indexOf('b') // 1 (substring position)
Don't expect array indexOf to do partial/substring matching — use
some(s => s.includes('b')) for that.
Default and localeCompare without options give file10 < file2
(lexicographic). Pass { numeric: true } for natural sort.
['file10','file2'].sort() // ['file10','file2']
['file10','file2'].sort((a,b)=>
a.localeCompare(b, undefined, { numeric: true }))
// ['file2','file10']
Combine with sensitivity: 'base' for case/accent-insensitive natural
ordering of filenames and version strings.
Consistent, safe defaults: indexOf/findIndex return -1,
find returns undefined, includes/some return false, and
every returns true.
[].indexOf('x') // -1
[].find(Boolean) // undefined
[].some(Boolean) // false
[].every(Boolean) // true vacuously
The every === true on empty is the one that surprises people in
validation logic — guard for emptiness if "all valid" shouldn't pass on no
data.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.