Cairn
algorithms · searching · O(n)

back to Searching

Linear Search

Seven of the other ten entries in Searching buy their speed by assuming something about the data up front — sortedness, at minimum, and sometimes a known length or a known distribution too. Feed any of them unsorted data and they don't fail loudly; they just quietly return wrong answers, because the whole mechanism (bisection, an uneven golden-ratio split, striding forward by a fixed block, a rotation-aware bisection around one break point) depends on order that isn't actually there. An eighth, Saddleback Search, needs the same kind of order, just spread across two axes of a matrix instead of one array. Linear search assumes nothing. It checks each element in turn until it finds the target or runs out of elements, correct on shuffled data exactly as it is on sorted data, in exchange for giving up every one of those eight entries' sub-O(n) guarantees. That makes it the right default exactly when sortedness doesn't hold, or isn't worth establishing for a single lookup — and it has one genuinely underrated optimization of its own, checked below with real counted comparisons rather than taken on faith. (The other two, Quickselect and Binary Search on Answer, don't need sortedness either, but each answers a different question entirely — which rank an element holds, and where a monotonic feasibility check switches from "no" to "yes" — not whether a target exists.)

Try it

The array below is deliberately unsorted — 15 elements in an arbitrary order — because that's the one case none of this site's other Searching entries can handle correctly. Pick a preset or edit the array and target directly, then step through. The "assume sorted" checkbox reproduces a real, common bug: applying a sorted-array early-break to data that was never actually sorted.

Press Load, then Step through the search.

Why it works

There's no invariant to prove wrong the way bisection needs one — the loop simply checks every element in the order it appears. By the time it has examined all n of them without a match, it has directly ruled out every possible location, so "not found" is exactly as certain as a match would have been. That's also why it needs no assumption about order: ruling out one element says nothing about any other, unlike binary search's bisection, where a single comparison discards an entire half based on the sorted-order guarantee.

The sentinel optimization

A textbook naive loop pays two comparisons per element examined: i < arr.length to check the loop hasn't run off the end, then arr[i] === target to check the value itself. The sentinel trick removes the first one entirely. Temporarily append the target itself to the end of the array, then loop with no bounds check at all — the loop is now mathematically guaranteed to terminate, because even if the real target isn't anywhere in the original data, it will always find the copy it just planted:

function linearSearchSentinel(arr, target) {
  const n = arr.length;
  arr.push(target);       // temporary sentinel — guarantees the loop below terminates
  let i = 0;
  while (arr[i] !== target) i++;
  arr.pop();               // restore the original array
  return i === n ? -1 : i;  // landing on the sentinel itself means "not found"
}

Every iteration now costs exactly one comparison instead of two. Simulated against a real naive implementation (bounds check counted as a real comparison, not assumed free) across every array size from 1 to 50 and every possible target index, the ratio holds exactly 2:1 whenever the target is present — not "roughly half," genuinely half every single time, because both versions do the same amount of useful work (one equality check per element up to and including the match) and the naive version's only extra cost is the bounds check paid alongside each one. On this page's own 15-element array:

target (index)17 (1)34 (7)5 (13)49 (14, last)100 (absent)
naive comparisons416283031
sentinel comparisons28141516

The absent case is close but not quite exact — 2n+1 for naive against n+1 for sentinel, a ratio that approaches 2 as n grows but is never precisely 2 for any finite n (31 against 16 above, not 31 against 15.5). The exact-half claim is specifically about the found case; worth stating precisely rather than rounding both cases to the same "about half" the way it'd be tempting to.

Reference implementation

function linearSearch(arr, target) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) return i;
  }
  return -1;
}

Pitfalls

Porting a sorted-array early-break onto unsorted data is a silent, verified bug — try the "assume sorted" checkbox above against this page's own array and target 5. A common optimization on sorted data is to stop the scan the moment arr[i] > target, since nothing sorted-and-later could still be a match. On this page's unsorted array, target 5 genuinely sits at index 13 — but index 0 holds 42, which is already greater than 5. The early-break "optimization" stops at the very first element and reports "not found," silently wrong, with no error of any kind — worse even than a slow correct answer, since it never gets anywhere near the real index. The checkbox above reproduces this exact failure live: uncheck it and the correct scan finds index 13 after 14 comparisons; check it and the same array, same target, comes back not-found after just 1.

The sentinel trick needs a mutable array it can temporarily own, not just read. It works by pushing a value onto the real array and popping it back off, invisible to a single-threaded caller — but that's unsafe on an array another reader might observe mid-search (a concurrent reader could see the extra element), and impossible outright on a fixed-length array (a typed array like Int32Array has no push) or a read-only source. Any of those would need a defensive copy first, and that copy alone costs a full linear pass over the array — the same order of work as the entire search, at which point the optimization is competing with the very cost it was trying to avoid, not saving anything net.

Complexity

O(n) worst case and average case (uniformly over target position, or when the target is absent), O(1) best case (target is the first element checked). This is provably the best any comparison-based search can do on unstructured data: an adversary argument shows why — for any algorithm that decides to skip examining some element before checking it, an adversary could have hidden the target exactly there, so nothing sub-O(n) can be made worst-case correct without more structure (sortedness, a hash, an index) than "an unordered list" provides. Every other entry in Searching buys its faster bound by requiring exactly that extra structure. See Choosing a Search Algorithm for how it fits alongside the site's other ten Searching entries.