Linear Search: Sequential Search Algorithm

Linear search, also called sequential search, is the most fundamental searching algorithm in computer science. You start at the beginning of a collection and check each element one by one until you...

Key Insights

  • Linear search is the most versatile search algorithm—it works on any data structure, sorted or unsorted, and requires no preprocessing, making it the pragmatic choice for small datasets and complex search criteria.
  • The O(n) time complexity sounds inefficient, but for datasets under ~100 elements, linear search often outperforms binary search due to lower overhead and better cache locality.
  • Optimizations like sentinel search and move-to-front heuristics can significantly improve real-world performance when the same elements are searched repeatedly.

Linear search, also called sequential search, is the most fundamental searching algorithm in computer science. You start at the beginning of a collection and check each element one by one until you find what you’re looking for or run out of elements to check.

It’s the algorithm equivalent of looking for your keys by checking every pocket in order. Not glamorous, but it works every time.

While computer science education often rushes past linear search to get to “more interesting” algorithms like binary search, this dismissiveness is misguided. Linear search remains the workhorse algorithm in production code for good reasons: it’s simple to implement correctly, works on any data structure, and often outperforms fancier alternatives in real-world scenarios.

How Linear Search Works

The algorithm is straightforward:

  1. Start at the first element
  2. Compare the current element with the target
  3. If they match, return the position
  4. If not, move to the next element
  5. Repeat until you find the target or exhaust all elements

Here’s the basic implementation:

def linear_search(arr, target):
    """
    Search for target in arr, return index if found, -1 otherwise.
    """
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

# Usage
numbers = [64, 34, 25, 12, 22, 11, 90]
result = linear_search(numbers, 22)
print(f"Element found at index: {result}")  # Output: 4
function linearSearch(arr, target) {
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] === target) {
            return i;
        }
    }
    return -1;
}

// Usage
const numbers = [64, 34, 25, 12, 22, 11, 90];
const result = linearSearch(numbers, 22);
console.log(`Element found at index: ${result}`); // Output: 4

The logic is identical across languages. This simplicity is a feature, not a bug—there’s almost nothing that can go wrong.

Implementation Variations

Real-world requirements rarely match the textbook “find first occurrence” scenario. Here are practical variations you’ll actually use.

Finding All Occurrences

def find_all_indices(arr, target):
    """Return all indices where target appears."""
    indices = []
    for i, element in enumerate(arr):
        if element == target:
            indices.append(i)
    return indices

# Usage
data = [1, 3, 5, 3, 7, 3, 9]
print(find_all_indices(data, 3))  # Output: [1, 3, 5]

Finding Last Occurrence

def find_last(arr, target):
    """Return index of last occurrence, -1 if not found."""
    last_index = -1
    for i, element in enumerate(arr):
        if element == target:
            last_index = i
    return last_index

# Or search backwards for efficiency
def find_last_optimized(arr, target):
    for i in range(len(arr) - 1, -1, -1):
        if arr[i] == target:
            return i
    return -1

Searching in Linked Lists

Linear search is the only option for linked lists since you can’t jump to arbitrary positions:

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

def search_linked_list(head, target):
    """Search linked list, return node if found, None otherwise."""
    current = head
    position = 0
    
    while current is not None:
        if current.data == target:
            return position
        current = current.next
        position += 1
    
    return -1

# Build a simple list
head = Node(10)
head.next = Node(20)
head.next.next = Node(30)

print(search_linked_list(head, 20))  # Output: 1

Time and Space Complexity Analysis

Linear search has predictable complexity characteristics:

Case Time Complexity When It Occurs
Best O(1) Target is first element
Average O(n/2) → O(n) Target is in middle
Worst O(n) Target is last or absent

Space Complexity: O(1)—you only need a loop counter regardless of input size.

Here’s how linear search compares to alternatives:

Algorithm Time (Average) Space Requires Sorted? Works on Linked Lists?
Linear Search O(n) O(1) No Yes
Binary Search O(log n) O(1) Yes No
Hash Table O(1) O(n) No N/A
Jump Search O(√n) O(1) Yes No

The table reveals linear search’s advantage: it’s the only algorithm that works everywhere with no prerequisites.

Stop reaching for binary search by default. Linear search is the better choice in these scenarios:

Unsorted Data

If your data isn’t sorted and you only need one search, linear search wins. Sorting first (O(n log n)) plus binary search (O(log n)) is slower than a single linear search (O(n)).

Small Datasets

For arrays under ~100 elements, linear search often beats binary search due to simpler operations and better branch prediction. The overhead of binary search’s index calculations isn’t worth it.

Complex Search Criteria

When searching by computed properties or multiple conditions, linear search with a predicate function is cleaner:

def find_by_predicate(arr, predicate):
    """Find first element matching the predicate function."""
    for i, element in enumerate(arr):
        if predicate(element):
            return i, element
    return -1, None

# Search objects by property
users = [
    {"id": 1, "name": "Alice", "active": False},
    {"id": 2, "name": "Bob", "active": True},
    {"id": 3, "name": "Charlie", "active": True},
]

# Find first active user
index, user = find_by_predicate(users, lambda u: u["active"])
print(f"First active user: {user['name']}")  # Output: Bob

# Find user by complex criteria
index, user = find_by_predicate(
    users, 
    lambda u: u["active"] and u["name"].startswith("C")
)
print(f"Active user starting with C: {user['name']}")  # Output: Charlie

Streaming Data

When processing data streams where you can’t store everything in memory, linear search is your only option:

def search_stream(stream, target):
    """Search through an iterator/generator."""
    for i, element in enumerate(stream):
        if element == target:
            return i
    return -1

Optimizations and Variants

When linear search is the right tool, these optimizations can improve performance.

Eliminate the bounds check on every iteration by placing the target at the end:

def sentinel_search(arr, target):
    """
    Sentinel search eliminates one comparison per iteration.
    Note: Modifies array temporarily.
    """
    n = len(arr)
    if n == 0:
        return -1
    
    # Save last element and place sentinel
    last = arr[n - 1]
    arr[n - 1] = target
    
    i = 0
    while arr[i] != target:
        i += 1
    
    # Restore last element
    arr[n - 1] = last
    
    # Check if we found target or hit sentinel
    if i < n - 1 or arr[n - 1] == target:
        return i
    return -1

This removes one comparison per iteration—the i < n check. For large arrays with many searches, this adds up.

Move-to-Front Heuristic

When the same elements are searched repeatedly, move found elements to the front:

def search_with_mtf(arr, target):
    """
    Move-to-front: recently accessed elements bubble to the front.
    Good when access patterns have temporal locality.
    """
    for i, element in enumerate(arr):
        if element == target:
            if i > 0:
                # Move to front
                arr.pop(i)
                arr.insert(0, element)
            return 0  # Now at front
    return -1

This is particularly effective for caches and recently-used lists.

The choice between linear and binary search isn’t always obvious. Here’s a practical benchmark:

import time
import random

def benchmark_searches(sizes):
    """Compare linear and binary search across different array sizes."""
    
    def linear_search(arr, target):
        for i, val in enumerate(arr):
            if val == target:
                return i
        return -1
    
    def binary_search(arr, target):
        left, right = 0, len(arr) - 1
        while left <= right:
            mid = (left + right) // 2
            if arr[mid] == target:
                return mid
            elif arr[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        return -1
    
    print(f"{'Size':<10} {'Linear (μs)':<15} {'Binary (μs)':<15} {'Winner'}")
    print("-" * 50)
    
    for size in sizes:
        arr = sorted(random.sample(range(size * 10), size))
        target = arr[size // 2]  # Search for middle element
        
        # Benchmark linear
        start = time.perf_counter()
        for _ in range(1000):
            linear_search(arr, target)
        linear_time = (time.perf_counter() - start) * 1000  # ms for 1000 runs
        
        # Benchmark binary
        start = time.perf_counter()
        for _ in range(1000):
            binary_search(arr, target)
        binary_time = (time.perf_counter() - start) * 1000
        
        winner = "Linear" if linear_time < binary_time else "Binary"
        print(f"{size:<10} {linear_time:<15.2f} {binary_time:<15.2f} {winner}")

benchmark_searches([10, 50, 100, 500, 1000, 10000])

Typical results show linear search winning for arrays under 50-100 elements, with binary search pulling ahead as sizes grow.

Decision Framework:

  • Use linear search when: data is unsorted, dataset is small (<100 elements), you’re searching once, you need complex predicates, or you’re working with linked lists.
  • Use binary search when: data is sorted, dataset is large, you’re searching repeatedly, and you can afford the sorted-array constraint.

Linear search isn’t a fallback—it’s a deliberate choice. Know when simplicity wins.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.