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 Comparators

indexOf 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.

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.

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.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.