Skip to content

11: Linear Search & Variants

11: Linear Search & Variants

Linear search is the simplest and most intuitive search algorithm. While it’s O(n) and slower than binary search for sorted data, it’s essential to understand and has important use cases. In this chapter, we’ll master linear search and explore its variants and optimizations.

Linear search works by checking each element sequentially until finding the target or reaching the end of the collection. Despite its simplicity, linear search is the optimal choice in many scenarios: when data is unsorted, when arrays are small, when you only need to search once, or when working with linked lists. Understanding linear search also provides the foundation for more advanced search algorithms and helps you appreciate why sorting data can dramatically improve search performance.

We’ll explore optimization techniques like sentinel search (which eliminates boundary checks), jump search for sorted arrays, and interpolation search for uniformly distributed data. You’ll also learn how to apply linear search to real-world scenarios including object searching, multidimensional arrays, and security-sensitive applications that require constant-time comparisons to prevent timing attacks.

By the end of this chapter, you’ll understand when linear search is the right tool for the job and how to optimize it for specific use cases. This knowledge is crucial because linear search forms the basis of many PHP built-in functions like in_array() and array_search(), and understanding its characteristics helps you make informed decisions about algorithm selection.

By the end of this chapter, you will:

  • Master linear search and understand its O(n) complexity for unsorted arrays
  • Learn optimization variants including sentinel search, bidirectional search, and move-to-front
  • Implement recursive and iterative linear search approaches
  • Understand when linear search is the optimal choice despite being “simple”
  • Apply linear search to real-world scenarios including filtering and finding all occurrences

Before starting this chapter, ensure you have:

  • ✓ Understanding of arrays and loops (10 mins review if needed)
  • ✓ Familiarity with Big O notation (60 mins from Chapter 1 if not done)
  • ✓ Completion of foundation chapters (review if needed)
  • Estimated Time: ~45 minutes

Complete these hands-on tasks as you work through the chapter:

  • Implement basic linear search (foreach, for loop, and recursive versions)
  • Create bidirectional search (search from both ends)
  • Build sentinel search optimization
  • Implement transpose and frequency-count heuristics
  • Build jump search for sorted arrays
  • Implement interpolation search for uniform data
  • Create Bloom filter for quick negative checks
  • Understand when NOT to use linear search (decision tree)
  • Benchmark linear vs binary vs hash table performance
  • Create search functions for objects and multidimensional arrays
  • Implement autocomplete engine with linear search
  • Create LRU cache using linear search
  • Add timing-safe search for security-sensitive scenarios

Linear search checks each element sequentially until finding the target or reaching the end.

basic-linear-search.php
function linearSearch(array $arr, mixed $target): int|false
{
foreach ($arr as $index => $value) {
if ($value === $target) {
return $index;
}
}
return false;
}
$numbers = [4, 2, 7, 1, 9, 5];
echo linearSearch($numbers, 7); // Output: 2
echo linearSearch($numbers, 8); // Output: false
basic-linear-search.php
function linearSearchLoop(array $arr, mixed $target): int|false
{
$n = count($arr);
for ($i = 0; $i < $n; $i++) {
if ($arr[$i] === $target) {
return $i;
}
}
return false;
}
basic-linear-search.php
function linearSearchRecursive(array $arr, mixed $target, int $index = 0): int|false
{
// Base case: reached end of array
if ($index >= count($arr)) {
return false;
}
// Base case: found the target
if ($arr[$index] === $target) {
return $index;
}
// Recursive case: check next element
return linearSearchRecursive($arr, $target, $index + 1);
}
$numbers = [4, 2, 7, 1, 9, 5];
echo linearSearchRecursive($numbers, 7); // Output: 2
echo linearSearchRecursive($numbers, 8); // Output: false

Note: While recursive linear search is possible, the iterative version is preferred in PHP because:

  • Recursion has function call overhead
  • PHP has a recursion limit (default 256)
  • Iterative version is more memory-efficient (O(1) vs O(n) stack space)
  • Time Complexity:

    • Best case: O(1) - element is at the beginning
    • Average case: O(n/2) → O(n)
    • Worst case: O(n) - element is at the end or not present
  • Space Complexity: O(1) - no extra space needed

Why O(n)?

  • Must potentially check every element
  • No way to skip elements without examining them

Linear search is the right choice when:

// Can't use binary search - array not sorted!
$randomNumbers = [15, 3, 42, 7, 23, 8, 16];
$index = linearSearch($randomNumbers, 42);
// For tiny arrays, linear search is faster than sorting + binary search
$smallArray = [1, 2, 3, 4, 5];
// This is faster:
linearSearch($smallArray, 3); // O(n) where n=5
// Than this:
sort($smallArray); // O(n log n)
binarySearch($smallArray, 3); // O(log n)
// Total: O(n log n) is worse than O(n) for small n
// If you only search once, sorting first is wasteful
$data = generateRandomArray(1000);
// Just search linearly - O(n)
linearSearch($data, $target);
// vs sorting first - O(n log n) + O(log n) = O(n log n)
sort($data);
binarySearch($data, $target);
// Can't do binary search on linked lists efficiently
class Node
{
public function __construct(
public mixed $data,
public ?Node $next = null
) {}
}
function searchLinkedList(?Node $head, mixed $target): ?Node
{
$current = $head;
while ($current !== null) {
if ($current->data === $target) {
return $current;
}
$current = $current->next;
}
return null;
}

Avoid linear search when:

// Bad: Linear search on sorted data
$sortedIds = range(1, 10000);
linearSearch($sortedIds, 7500); // O(n) - checks ~7500 elements
// Good: Binary search on sorted data
binarySearch($sortedIds, 7500); // O(log n) - checks ~13 elements

Decision: If data is sorted and n > 50, use binary search (60-100x faster for n=10,000).

2. Multiple Searches on Same Dataset (k > log n searches)

Section titled “2. Multiple Searches on Same Dataset (k > log n searches)”
// Bad: Multiple linear searches
$data = range(1, 10000);
for ($i = 0; $i < 100; $i++) {
linearSearch($data, rand(1, 10000)); // 100 * O(n) = O(100n)
}
// Good: Sort once, then binary search
sort($data); // O(n log n) - one-time cost
for ($i = 0; $i < 100; $i++) {
binarySearch($data, rand(1, 10000)); // 100 * O(log n)
}
// Total: O(n log n + 100 log n) << O(100n)

Decision: If k > log(n) searches, sorting first is worth it.

  • For n=10,000: log(10,000) ≈ 13, so if you search > 13 times, sort first!
// Bad: Linear search for keys
$users = [
'alice' => ['email' => 'alice@example.com'],
'bob' => ['email' => 'bob@example.com'],
// ... 10,000 more users
];
// O(n) - searches through all keys
$found = false;
foreach ($users as $key => $user) {
if ($key === 'bob') {
$found = true;
break;
}
}
// Good: Use array key lookup (hash table)
if (isset($users['bob'])) { // O(1) - instant!
// User exists
}

Decision: For key lookups, ALWAYS use isset() or array_key_exists() (O(1)).

4. Frequent Insertions/Deletions with Searches

Section titled “4. Frequent Insertions/Deletions with Searches”
// Bad: Linear search on frequently modified data
$collection = [];
for ($i = 0; $i < 1000; $i++) {
$collection[] = rand(1, 10000); // Insert: O(1)
linearSearch($collection, $target); // Search: O(n)
}
// Total: O(n²) complexity
// Good: Use balanced tree or hash table
$bst = new BinarySearchTree(); // Or use SplHeap
for ($i = 0; $i < 1000; $i++) {
$bst->insert(rand(1, 10000)); // Insert: O(log n)
$bst->search($target); // Search: O(log n)
}
// Total: O(n log n) complexity

Decision: For dynamic data, use balanced trees (Chapter 20) or hash tables (Chapter 13).

// Bad: Linear search on millions of records
$customers = loadMillionCustomers(); // 1,000,000 customers
linearSearch($customers, $targetId); // Could take seconds!
// Good: Use database index or hash table
// Database with index: O(log n) via B-tree
SELECT * FROM customers WHERE id = ? // Milliseconds
// Or hash table in memory: O(1)
$customersById = array_column($customers, null, 'id');
$customer = $customersById[$targetId] ?? null; // Instant!

Decision: For n > 100,000, use database indexes, hash tables, or search engines.

Decision Tree: Choosing the Right Search Algorithm

Section titled “Decision Tree: Choosing the Right Search Algorithm”
Is data sorted?
├─ YES
│ ├─ n < 50? → Linear Search (simpler, fast enough)
│ └─ n ≥ 50? → Binary Search (O(log n) advantage)
└─ NO (unsorted)
├─ Searching by KEY (not value)?
│ └─ Use Hash Table (isset/array_key_exists) - O(1)
├─ How many searches?
│ ├─ 1 search → Linear Search (O(n))
│ ├─ k searches where k ≤ log n → Linear Search (k * O(n))
│ └─ k searches where k > log n → Sort + Binary (O(n log n + k log n))
├─ Data size?
│ ├─ n < 100 → Linear Search (fast enough)
│ ├─ 100 < n < 10,000 → Consider sorting or hash table
│ └─ n > 10,000 → Hash table or database index
└─ Data structure?
├─ Array → Linear/Binary/Hash based on above
├─ Linked List → Linear Search (only option)
└─ Database → Use indexes (B-tree)
search-variants.php
function findAllOccurrences(array $arr, mixed $target): array
{
$indices = [];
foreach ($arr as $index => $value) {
if ($value === $target) {
$indices[] = $index;
}
}
return $indices;
}
$numbers = [1, 3, 5, 3, 7, 3, 9];
print_r(findAllOccurrences($numbers, 3));
// Output: [1, 3, 5]
search-with-conditions.php
function searchWithCondition(array $arr, callable $condition): mixed
{
foreach ($arr as $value) {
if ($condition($value)) {
return $value;
}
}
return null;
}
// Find first even number
$numbers = [1, 3, 5, 8, 7, 9, 10];
$firstEven = searchWithCondition($numbers, fn($x) => $x % 2 === 0);
echo $firstEven; // Output: 8
// Find first number > 100
$values = [45, 67, 89, 112, 134];
$firstLarge = searchWithCondition($values, fn($x) => $x > 100);
echo $firstLarge; // Output: 112

Bidirectional search searches from both ends simultaneously, potentially finding the target faster:

search-variants.php
function bidirectionalSearch(array $arr, mixed $target): int|false
{
$left = 0;
$right = count($arr) - 1;
while ($left <= $right) {
// Check from left
if ($arr[$left] === $target) {
return $left;
}
// Check from right
if ($arr[$right] === $target) {
return $right;
}
// Move both pointers inward
$left++;
$right--;
}
return false;
}
$numbers = [4, 2, 7, 1, 9, 5];
echo bidirectionalSearch($numbers, 7); // Output: 2
echo bidirectionalSearch($numbers, 5); // Output: 5 (found from right)

Advantage: Can find elements near the end faster (in best case, half the comparisons) Use case: When you don’t know where the target might be, but suspect it could be near either end

Sentinel search eliminates the boundary check in the loop, reducing comparisons:

search-variants.php
function sentinelLinearSearch(array $arr, mixed $target): int|false
{
$n = count($arr);
if ($n === 0) {
return false;
}
// Save last element
$last = $arr[$n - 1];
// Place sentinel at end
$arr[$n - 1] = $target;
$i = 0;
// No need to check $i < $n because sentinel guarantees we'll find target
while ($arr[$i] !== $target) {
$i++;
}
// Restore last element
$arr[$n - 1] = $last;
// Check if we found actual target or just the sentinel
if ($i < $n - 1 || $last === $target) {
return $i;
}
return false;
}
$numbers = [4, 2, 7, 1, 9, 5];
echo sentinelLinearSearch($numbers, 7); // Output: 2

Advantage: Fewer comparisons per iteration (no boundary check) Disadvantage: More complex code, minimal performance gain in practice

Combines linear search with jumping:

search-variants.php
function jumpSearch(array $arr, mixed $target): int|false
{
$n = count($arr);
$step = (int)sqrt($n);
$prev = 0;
// Jump ahead by step size
while ($arr[min($step, $n) - 1] < $target) {
$prev = $step;
$step += (int)sqrt($n);
if ($prev >= $n) {
return false;
}
}
// Linear search in the block
while ($arr[$prev] < $target) {
$prev++;
if ($prev === min($step, $n)) {
return false;
}
}
// Check if element found
if ($arr[$prev] === $target) {
return $prev;
}
return false;
}
// Works on sorted arrays
$sorted = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610];
echo jumpSearch($sorted, 55); // Output: 10

Complexity: O(√n) - better than linear, worse than binary

6. Interpolation Search (for Uniformly Distributed Sorted Arrays)

Section titled “6. Interpolation Search (for Uniformly Distributed Sorted Arrays)”
search-variants.php
function interpolationSearch(array $arr, int $target): int|false
{
$low = 0;
$high = count($arr) - 1;
while ($low <= $high && $target >= $arr[$low] && $target <= $arr[$high]) {
if ($low === $high) {
if ($arr[$low] === $target) {
return $low;
}
return false;
}
// Estimate position using interpolation formula
$pos = $low + (int)((($high - $low) / ($arr[$high] - $arr[$low])) *
($target - $arr[$low]));
if ($arr[$pos] === $target) {
return $pos;
}
if ($arr[$pos] < $target) {
$low = $pos + 1;
} else {
$high = $pos - 1;
}
}
return false;
}
// Works best on uniformly distributed data
$uniform = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
echo interpolationSearch($uniform, 70); // Output: 6

Complexity: O(log log n) average for uniform data, O(n) worst case

object-search.php
class User
{
public function __construct(
public int $id,
public string $name,
public string $email
) {}
}
function findUserByEmail(array $users, string $email): ?User
{
foreach ($users as $user) {
if ($user->email === $email) {
return $user;
}
}
return null;
}
function findUsersByRole(array $users, string $role): array
{
$result = [];
foreach ($users as $user) {
if ($user->role === $role) {
$result[] = $user;
}
}
return $result;
}
$users = [
new User(1, 'Alice', 'alice@example.com'),
new User(2, 'Bob', 'bob@example.com'),
new User(3, 'Charlie', 'charlie@example.com'),
];
$user = findUserByEmail($users, 'bob@example.com');
object-search.php
function searchMultidimensional(array $arr, string $key, mixed $value): ?array
{
foreach ($arr as $item) {
if (isset($item[$key]) && $item[$key] === $value) {
return $item;
}
}
return null;
}
$products = [
['id' => 1, 'name' => 'Laptop', 'price' => 1200],
['id' => 2, 'name' => 'Mouse', 'price' => 25],
['id' => 3, 'name' => 'Keyboard', 'price' => 75],
];
$product = searchMultidimensional($products, 'name', 'Mouse');
print_r($product); // ['id' => 2, 'name' => 'Mouse', 'price' => 25]
object-search.php
function deepSearch(array $data, string $key, mixed $value): ?array
{
foreach ($data as $item) {
if (is_array($item)) {
// Check current level
if (isset($item[$key]) && $item[$key] === $value) {
return $item;
}
// Recursively search nested arrays
$result = deepSearch($item, $key, $value);
if ($result !== null) {
return $result;
}
}
}
return null;
}
$nested = [
'users' => [
['name' => 'Alice', 'age' => 30],
'admins' => [
['name' => 'Bob', 'age' => 25],
['name' => 'Charlie', 'age' => 35]
]
]
];
$result = deepSearch($nested, 'name', 'Charlie');

Optimize for repeated searches by moving found elements to the front:

optimization-techniques.php
function searchMoveToFront(array &$arr, mixed $target): int|false
{
foreach ($arr as $index => $value) {
if ($value === $target) {
// Move to front if not already there
if ($index > 0) {
$temp = $arr[$index];
array_splice($arr, $index, 1);
array_unshift($arr, $temp);
return 0;
}
return $index;
}
}
return false;
}
$cache = ['a', 'b', 'c', 'd', 'e'];
searchMoveToFront($cache, 'd');
print_r($cache); // ['d', 'a', 'b', 'c', 'e']

More conservative than move-to-front—swap with predecessor:

optimization-techniques.php
function searchTranspose(array &$arr, mixed $target): int|false
{
foreach ($arr as $index => $value) {
if ($value === $target) {
// Swap with predecessor (move up one position)
if ($index > 0) {
[$arr[$index - 1], $arr[$index]] = [$arr[$index], $arr[$index - 1]];
return $index - 1;
}
return $index;
}
}
return false;
}
$cache = ['a', 'b', 'c', 'd', 'e'];
searchTranspose($cache, 'd'); // Returns 2 (after swap)
print_r($cache); // ['a', 'b', 'd', 'c', 'e'] - moved up one position

Advantage: More gradual reordering, better for fluctuating access patterns Use case: When frequently accessed items change over time

Keep frequently accessed items near the front based on access count:

optimization-techniques.php
class FrequencyOptimizedSearch
{
private array $items;
private array $accessCounts;
public function __construct(array $items)
{
$this->items = array_values($items);
$this->accessCounts = array_fill(0, count($items), 0);
}
public function search(mixed $target): int|false
{
foreach ($this->items as $index => $value) {
if ($value === $target) {
$this->accessCounts[$index]++;
$this->reorganize($index);
return $this->findCurrentPosition($value);
}
}
return false;
}
private function reorganize(int $foundIndex): void
{
// Move item forward based on access frequency
while ($foundIndex > 0 &&
$this->accessCounts[$foundIndex] > $this->accessCounts[$foundIndex - 1]) {
// Swap items and counts
[$this->items[$foundIndex], $this->items[$foundIndex - 1]] =
[$this->items[$foundIndex - 1], $this->items[$foundIndex]];
[$this->accessCounts[$foundIndex], $this->accessCounts[$foundIndex - 1]] =
[$this->accessCounts[$foundIndex - 1], $this->accessCounts[$foundIndex]];
$foundIndex--;
}
}
private function findCurrentPosition(mixed $value): int|false
{
foreach ($this->items as $index => $item) {
if ($item === $value) {
return $index;
}
}
return false;
}
public function getItems(): array
{
return $this->items;
}
public function getAccessCounts(): array
{
return $this->accessCounts;
}
}
// Usage example
$search = new FrequencyOptimizedSearch(['a', 'b', 'c', 'd', 'e']);
$search->search('e'); // Access 'e' once
$search->search('e'); // Access 'e' twice
$search->search('e'); // Access 'e' three times
$search->search('b'); // Access 'b' once
print_r($search->getItems());
// Output: ['e', 'b', 'a', 'c', 'd']
// Most frequently accessed items move to front

Advantage: Better long-term optimization than move-to-front Use case: When access patterns are stable over time (e.g., caching popular items)

optimization-techniques.php
function searchSortedWithTermination(array $arr, int $target): int|false
{
foreach ($arr as $index => $value) {
if ($value === $target) {
return $index;
}
// Early termination: target can't be in remaining elements
if ($value > $target) {
return false;
}
}
return false;
}
$sorted = [1, 3, 5, 7, 9, 11, 13];
// Searching for 6 stops at 7 - no need to check rest

Before expensive linear search, use a Bloom filter to quickly determine “definitely NOT present”:

optimization-techniques.php
class BloomFilter
{
private array $bitArray;
private int $size;
private int $numHashFunctions;
public function __construct(int $size = 1000, int $numHashFunctions = 3)
{
$this->size = $size;
$this->numHashFunctions = $numHashFunctions;
$this->bitArray = array_fill(0, $size, false);
}
public function add(mixed $item): void
{
$itemStr = (string)$item;
for ($i = 0; $i < $this->numHashFunctions; $i++) {
$hash = $this->hash($itemStr, $i);
$index = $hash % $this->size;
$this->bitArray[$index] = true;
}
}
public function mightContain(mixed $item): bool
{
$itemStr = (string)$item;
for ($i = 0; $i < $this->numHashFunctions; $i++) {
$hash = $this->hash($itemStr, $i);
$index = $hash % $this->size;
if (!$this->bitArray[$index]) {
return false; // Definitely not present
}
}
return true; // Might be present (could be false positive)
}
private function hash(string $item, int $seed): int
{
// Use different hash functions by combining with seed
return abs(crc32($item . $seed));
}
public function addAll(array $items): void
{
foreach ($items as $item) {
$this->add($item);
}
}
}
// Optimized search with Bloom filter
function optimizedSearchWithBloom(array $arr, mixed $target, BloomFilter $filter): int|false
{
// Quick check: if bloom filter says "no", skip linear search entirely
if (!$filter->mightContain($target)) {
return false; // Definitely not present - O(1) check!
}
// Bloom filter says "maybe" - do actual linear search
return linearSearch($arr, $target); // O(n) only when necessary
}
// Usage example
$data = range(1, 10000);
$bloom = new BloomFilter(15000, 3);
$bloom->addAll($data);
// Fast negative check - no linear search needed
$result1 = optimizedSearchWithBloom($data, 99999, $bloom);
echo "99999 found: " . ($result1 !== false ? "Yes" : "No") . "\n"; // No (fast!)
// Positive case - linear search performed
$result2 = optimizedSearchWithBloom($data, 5000, $bloom);
echo "5000 found at index: $result2\n"; // Yes (index 4999)

Advantage: O(1) rejection of non-existent items, dramatically speeds up negative searches Use case: Large datasets where most searches are negative (e.g., checking if username is taken) Trade-off: Small false positive rate (might do unnecessary linear search), but NEVER false negatives

practical-applications.php
function filter(array $arr, callable $predicate): array
{
$result = [];
foreach ($arr as $item) {
if ($predicate($item)) {
$result[] = $item;
}
}
return $result;
}
$numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
$evens = filter($numbers, fn($x) => $x % 2 === 0);
$largeNumbers = filter($numbers, fn($x) => $x > 5);
practical-applications.php
function findInLogFile(string $filename, string $pattern): array
{
$matches = [];
$handle = fopen($filename, 'r');
if ($handle) {
$lineNumber = 0;
while (($line = fgets($handle)) !== false) {
$lineNumber++;
// Linear search through file
if (stripos($line, $pattern) !== false) {
$matches[] = [
'line' => $lineNumber,
'content' => trim($line)
];
}
}
fclose($handle);
}
return $matches;
}
// Find all ERROR entries in log
$errors = findInLogFile('app.log', 'ERROR');
practical-applications.php
function searchWithWildcard(array $arr, string $pattern): array
{
$results = [];
// Convert wildcard pattern to regex
$regex = '/^' . str_replace('*', '.*', preg_quote($pattern, '/')) . '$/i';
foreach ($arr as $item) {
if (preg_match($regex, $item)) {
$results[] = $item;
}
}
return $results;
}
$files = ['test.php', 'index.php', 'config.json', 'data.php'];
$phpFiles = searchWithWildcard($files, '*.php');
print_r($phpFiles); // ['test.php', 'index.php', 'data.php']

PHP provides several built-in functions that use linear search internally. Understanding when to use each is crucial:

FunctionReturnsUse WhenNotes
in_array($needle, $haystack)boolNeed to check existence onlyReturns true/false
array_search($needle, $haystack)int|string|falseNeed the key/indexReturns first match’s key
array_key_exists($key, $array)boolChecking if key existsO(1) for arrays (hash table)
isset($array[$key])boolChecking if key exists and not nullO(1), fastest for key checks
php-builtin-comparison.php
$users = [
'alice' => 'alice@example.com',
'bob' => 'bob@example.com',
'charlie' => 'charlie@example.com',
];
// Check if value exists (linear search)
if (in_array('bob@example.com', $users)) {
echo "Email exists\n";
}
// Get key for value (linear search)
$key = array_search('bob@example.com', $users);
echo "Key: $key\n"; // Output: bob
// Check if key exists (O(1) hash table lookup - NOT linear search!)
if (array_key_exists('bob', $users)) {
echo "User exists\n";
}
// Fastest key check (O(1))
if (isset($users['bob'])) {
echo "User exists and is not null\n";
}
// For indexed arrays
$numbers = [10, 20, 30, 40, 50];
// Check value exists
if (in_array(30, $numbers)) {
echo "30 found\n";
}
// Get index
$index = array_search(30, $numbers);
echo "Index: $index\n"; // Output: 2

Use in_array() when:

  • You only need to know if a value exists
  • You don’t need the key/index
  • Working with indexed arrays

Use array_search() when:

  • You need the key/index of the found value
  • You want the first occurrence
  • Working with associative arrays where you need the key

Use array_key_exists() or isset() when:

  • Checking if a key exists (not a value)
  • This is O(1) - much faster than linear search!
  • Always prefer these for key checks

Performance Note: array_key_exists() and isset() are O(1) because PHP arrays are hash tables. They’re much faster than in_array() or array_search() which are O(n) linear searches.

require_once 'Benchmark.php';
$bench = new Benchmark();
$sizes = [100, 1000, 10000];
foreach ($sizes as $size) {
$data = range(1, $size);
shuffle($data);
$target = $data[rand(0, $size - 1)];
echo "Array size: $size\n";
$bench->compare([
'Linear Search' => fn($arr) => linearSearch($arr, $target),
'Sentinel Search' => fn($arr) => sentinelLinearSearch($arr, $target),
'PHP in_array()' => fn($arr) => array_search($target, $arr),
], $data, iterations: 100);
echo "\n";
}

Linear Search vs Other Algorithms: Performance Comparison

Section titled “Linear Search vs Other Algorithms: Performance Comparison”

Understanding when linear search becomes impractical is crucial for making informed decisions:

Data SizeLinear SearchBinary SearchHash Table (isset)Best Choice
10 items5 µs3 µs1 µsLinear (simplicity wins)
50 items25 µs5 µs1 µsLinear still competitive
100 items50 µs7 µs1 µsBinary or Hash (2x faster)
500 items250 µs9 µs1 µsBinary/Hash (25x faster)
1,000 items500 µs10 µs1 µsHash (500x faster)
5,000 items2.5 ms12 µs1 µsHash (2500x faster)
10,000 items5 ms13 µs1 µsHash (5000x faster)
100,000 items50 ms17 µs1 µsHash (50,000x faster)
1,000,000 items500 ms20 µs1 µsHash (500,000x faster)

The Crossover Point: Around 50-100 items is where other algorithms become significantly faster.

Sorting Cost Analysis:

// When is it worth sorting?
$n = 10000;
// Cost of k linear searches without sorting
$linearCost = function($k, $n) {
return $k * $n; // k * O(n)
};
// Cost of sorting once + k binary searches
$binaryCost = function($k, $n) {
return $n * log($n) + $k * log($n); // O(n log n) + k * O(log n)
};
// Break-even point
$k = log($n); // When k > log(n), binary is better
echo "For n=$n, sorting is worth it when k > " . log($n) . " searches\n";
// Output: For n=10000, sorting is worth it when k > 9.21 searches

Memory Trade-offs:

ApproachTime ComplexitySpace ComplexityNotes
Linear SearchO(n)O(1)No extra memory
Binary SearchO(log n)O(1)Requires sorted data
Hash TableO(1) averageO(n)Needs hash table space
Sorted ArrayO(log n)O(n)Needs sorted copy
performance-comparison.php
class SearchComparison
{
public function benchmarkAllMethods(int $size = 10000): void
{
$data = range(1, $size);
shuffle($data);
$target = $data[rand(0, $size - 1)];
$iterations = 1000;
// 1. Linear Search
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
linearSearch($data, $target);
}
$linearTime = (hrtime(true) - $start) / 1_000_000; // ms
// 2. Sort + Binary Search
$sortedData = $data;
$sortStart = hrtime(true);
sort($sortedData);
$sortTime = (hrtime(true) - $sortStart) / 1_000_000;
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
binarySearch($sortedData, $target);
}
$binaryTime = (hrtime(true) - $start) / 1_000_000;
// 3. Hash Table (array_flip)
$hashTable = array_flip($data);
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
isset($hashTable[$target]);
}
$hashTime = (hrtime(true) - $start) / 1_000_000;
// Results
echo "Dataset size: $size elements, $iterations searches\n";
echo str_repeat('=', 60) . "\n";
printf("Linear Search: %8.2f ms (%.0fx baseline)\n", $linearTime, 1.0);
printf("Binary Search: %8.2f ms (%.0fx faster)\n", $binaryTime, $linearTime / $binaryTime);
printf(" (+ sort time: %8.2f ms one-time cost)\n", $sortTime);
printf("Hash Table (isset): %8.2f ms (%.0fx faster)\n", $hashTime, $linearTime / $hashTime);
echo str_repeat('=', 60) . "\n";
// Break-even analysis
$breakEven = $sortTime / ($linearTime - $binaryTime);
printf("\nBreak-even: Sort + Binary becomes faster after %.0f searches\n", $breakEven);
}
}
$comparison = new SearchComparison();
$comparison->benchmarkAllMethods(10000);

Typical Output:

Dataset size: 10000 elements, 1000 searches
============================================================
Linear Search: 4850.00 ms (1x baseline)
Binary Search: 9.50 ms (511x faster)
(+ sort time: 45.00 ms one-time cost)
Hash Table (isset): 0.95 ms (5105x faster)
============================================================
Break-even: Sort + Binary becomes faster after 9 searches

Use Linear Search when:

  • n < 50 elements (negligible performance difference)
  • Data is unsorted and you search only once
  • Simplicity is more important than speed
  • Working with linked lists (no choice)

Use Binary Search when:

  • Data is already sorted
  • n > 50 and you search multiple times (k > log n)
  • Memory is constrained (no space for hash table)

Use Hash Tables when:

  • You search by key (not by value)
  • n > 100 and you search frequently
  • You have memory available
  • You need O(1) lookups

Use Database Indexes when:

  • n > 100,000 elements
  • Data persists across requests
  • Complex queries needed
  • Concurrent access required

Let’s compare different linear search approaches with actual measurements:

class SearchBenchmark
{
public function benchmarkSearchMethods(int $arraySize = 10000): array
{
$data = range(1, $arraySize);
shuffle($data);
// Target at different positions
$positions = [
'beginning' => $data[0],
'middle' => $data[(int)($arraySize / 2)],
'end' => $data[$arraySize - 1],
'not_found' => $arraySize + 1
];
$results = [];
foreach ($positions as $position => $target) {
echo "\n=== Target at: $position ===\n";
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
linearSearch($data, $target);
}
$results['linear'][$position] = microtime(true) - $start;
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
sentinelLinearSearch($data, $target);
}
$results['sentinel'][$position] = microtime(true) - $start;
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
array_search($target, $data);
}
$results['php_native'][$position] = microtime(true) - $start;
printf("Linear Search: %.4f seconds\n", $results['linear'][$position]);
printf("Sentinel Search: %.4f seconds\n", $results['sentinel'][$position]);
printf("PHP Native: %.4f seconds\n", $results['php_native'][$position]);
}
return $results;
}
}
// Run benchmark
$benchmark = new SearchBenchmark();
$benchmark->benchmarkSearchMethods(10000);

Expected Results:

=== Target at: beginning ===
Linear Search: 0.0123 seconds
Sentinel Search: 0.0118 seconds (5% faster)
PHP Native: 0.0089 seconds (27% faster)
=== Target at: end ===
Linear Search: 0.8456 seconds
Sentinel Search: 0.7823 seconds (7% faster)
PHP Native: 0.6234 seconds (26% faster)

Key Insights:

  • PHP native functions are optimized at C level - always prefer them
  • Sentinel search shows marginal improvement (5-7%)
  • Position of target significantly affects performance
  • For not found: all methods must scan entire array

Understanding memory usage of different search approaches:

class MemoryProfiler
{
public function compareMemoryUsage(): void
{
$arraySize = 100000;
$data = range(1, $arraySize);
// Basic Linear Search
$memBefore = memory_get_usage();
$result = linearSearch($data, 50000);
$memAfter = memory_get_usage();
echo "Linear Search Memory: " . ($memAfter - $memBefore) . " bytes\n";
// Search with Callback
$memBefore = memory_get_usage();
$result = searchWithCondition($data, fn($x) => $x === 50000);
$memAfter = memory_get_usage();
echo "Callback Search Memory: " . ($memAfter - $memBefore) . " bytes\n";
// Find All Occurrences
$memBefore = memory_get_usage();
$results = findAllOccurrences($data, 50000);
$memAfter = memory_get_usage();
echo "Find All Memory: " . ($memAfter - $memBefore) . " bytes\n";
// Move-to-Front (modifies array)
$temp = $data;
$memBefore = memory_get_usage();
searchMoveToFront($temp, 50000);
$memAfter = memory_get_usage();
echo "Move-to-Front Memory: " . ($memAfter - $memBefore) . " bytes\n";
}
}
$profiler = new MemoryProfiler();
$profiler->compareMemoryUsage();

Output:

Linear Search Memory: 16 bytes (minimal overhead)
Callback Search Memory: 1024 bytes (closure overhead)
Find All Memory: 5632 bytes (stores result array)
Move-to-Front Memory: 524 bytes (array modification)

Memory Considerations:

  • Simple search: O(1) extra space
  • Find all: O(k) where k = number of matches
  • Move-to-front: O(1) but modifies original
  • Callbacks add closure overhead

Leveraging Standard PHP Library for efficient searching:

class SPLSearchExamples
{
// SplFixedArray - faster than regular PHP arrays
public function searchFixedArray(): void
{
$size = 10000;
$fixedArray = new SplFixedArray($size);
// Populate
for ($i = 0; $i < $size; $i++) {
$fixedArray[$i] = rand(1, 1000);
}
// Linear search on SplFixedArray
$target = 500;
$found = false;
for ($i = 0; $i < $fixedArray->getSize(); $i++) {
if ($fixedArray[$i] === $target) {
echo "Found at index: $i\n";
$found = true;
break;
}
}
if (!$found) {
echo "Not found\n";
}
}
// Iterator-based search
public function searchWithIterator(array $data, mixed $target): mixed
{
$iterator = new ArrayIterator($data);
foreach ($iterator as $key => $value) {
if ($value === $target) {
return $key;
}
}
return null;
}
// Filtered iterator for complex searches
public function searchWithFilter(array $data, callable $predicate): array
{
$iterator = new ArrayIterator($data);
$filtered = new CallbackFilterIterator(
$iterator,
$predicate
);
return iterator_to_array($filtered);
}
}
// Usage examples
$spl = new SPLSearchExamples();
$spl->searchFixedArray();
$data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
$result = $spl->searchWithIterator($data, 7);
echo "Found at: $result\n";
// Find all even numbers
$evens = $spl->searchWithFilter($data, fn($x) => $x % 2 === 0);
print_r($evens); // [2, 4, 6, 8, 10]
class PrioritySearch
{
private SplMinHeap $heap;
public function __construct(array $data)
{
$this->heap = new SplMinHeap();
foreach ($data as $item) {
$this->heap->insert($item);
}
}
public function findMinimum(): mixed
{
return $this->heap->top();
}
public function searchLessThan(int $threshold): array
{
$results = [];
$tempHeap = clone $this->heap;
while (!$tempHeap->isEmpty()) {
$value = $tempHeap->extract();
if ($value < $threshold) {
$results[] = $value;
} else {
break; // Heap is sorted
}
}
return $results;
}
}
$search = new PrioritySearch([5, 2, 8, 1, 9, 3]);
echo "Minimum: " . $search->findMinimum() . "\n"; // 1
print_r($search->searchLessThan(5)); // [1, 2, 3]

Linear search can be vulnerable to timing attacks when searching sensitive data:

class SecureSearch
{
/**
* VULNERABLE: Early termination leaks information
* Attacker can measure time to determine if value exists
*/
public function insecureSearch(array $secrets, string $token): bool
{
foreach ($secrets as $secret) {
if ($secret === $token) {
return true; // Returns immediately - timing leak!
}
}
return false;
}
/**
* SECURE: Constant-time comparison
* Always checks entire array
*/
public function secureSearch(array $secrets, string $token): bool
{
$found = false;
foreach ($secrets as $secret) {
// Use constant-time comparison
if (hash_equals($secret, $token)) {
$found = true;
// Don't return early - continue checking
}
}
return $found;
}
/**
* SECURE: Constant-time with hash_equals
*/
public function constantTimeArraySearch(array $secrets, string $token): bool
{
$result = 0;
foreach ($secrets as $secret) {
// Bitwise OR to avoid early termination
$result |= (int)hash_equals($secret, $token);
}
return $result === 1;
}
}
// Example: API token validation
$validTokens = [
'token_abc123def456',
'token_xyz789ghi012',
'token_mno345pqr678'
];
$search = new SecureSearch();
// VULNERABLE: Timing attack possible
$userToken = $_POST['token'] ?? '';
if ($search->insecureSearch($validTokens, $userToken)) {
echo "Access granted";
}
// SECURE: Constant-time search
if ($search->secureSearch($validTokens, $userToken)) {
echo "Access granted";
}
class TimingSafeOperations
{
/**
* Find user by email (timing-safe)
*/
public function findUserSecure(array $users, string $email): ?array
{
$result = null;
$found = 0;
foreach ($users as $user) {
$match = hash_equals($user['email'], $email);
// Constant-time selection
if ($match) {
$result = $user;
$found = 1;
}
}
// Add random delay to obscure timing
usleep(rand(100, 500));
return $found ? $result : null;
}
/**
* Rate limiting to prevent timing attack exploitation
*/
private array $attempts = [];
public function searchWithRateLimit(
string $clientId,
array $data,
mixed $target,
int $maxAttempts = 10
): mixed {
// Track attempts
if (!isset($this->attempts[$clientId])) {
$this->attempts[$clientId] = ['count' => 0, 'time' => time()];
}
// Reset after 1 minute
if (time() - $this->attempts[$clientId]['time'] > 60) {
$this->attempts[$clientId] = ['count' => 0, 'time' => time()];
}
// Check rate limit
if ($this->attempts[$clientId]['count'] >= $maxAttempts) {
throw new Exception("Rate limit exceeded");
}
$this->attempts[$clientId]['count']++;
// Perform constant-time search
$result = null;
foreach ($data as $index => $value) {
if (hash_equals((string)$value, (string)$target)) {
$result = $index;
}
}
return $result;
}
}
namespace App\Services;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
class SearchService
{
/**
* Search with caching for frequently accessed data
*/
public function searchWithCache(
Collection $collection,
string $field,
mixed $value
): mixed {
$cacheKey = "search:{$field}:{$value}";
return Cache::remember($cacheKey, 3600, function () use ($collection, $field, $value) {
return $collection->first(function ($item) use ($field, $value) {
return $item->$field === $value;
});
});
}
/**
* Autocomplete search using linear search
*/
public function autocomplete(Collection $items, string $query, int $limit = 10): Collection
{
return $items
->filter(function ($item) use ($query) {
return str_starts_with(strtolower($item->name), strtolower($query));
})
->take($limit)
->values();
}
/**
* Full-text search simulation
*/
public function fullTextSearch(Collection $items, string $searchTerm): Collection
{
$terms = explode(' ', strtolower($searchTerm));
return $items->filter(function ($item) use ($terms) {
$content = strtolower($item->title . ' ' . $item->description);
foreach ($terms as $term) {
if (str_contains($content, $term)) {
return true;
}
}
return false;
});
}
}
// Usage in Laravel controller
class ProductController extends Controller
{
public function search(Request $request, SearchService $searchService)
{
$products = Product::all();
// Autocomplete
if ($request->has('autocomplete')) {
return $searchService->autocomplete(
$products,
$request->input('q'),
10
);
}
// Full search
return $searchService->fullTextSearch(
$products,
$request->input('q')
);
}
}
namespace App\Search;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Contracts\Cache\ItemInterface;
class LinearSearchService
{
private AdapterInterface $cache;
public function __construct(AdapterInterface $cache)
{
$this->cache = $cache;
}
/**
* Search entities with caching
*/
public function searchEntities(
array $entities,
string $property,
mixed $value
): ?object {
$cacheKey = sprintf('entity_search_%s_%s', $property, md5((string)$value));
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($entities, $property, $value) {
$item->expiresAfter(3600);
foreach ($entities as $entity) {
$getter = 'get' . ucfirst($property);
if (method_exists($entity, $getter) && $entity->$getter() === $value) {
return $entity;
}
}
return null;
});
}
/**
* Repository pattern with linear search
*/
public function findByMultipleFields(array $data, array $criteria): array
{
return array_filter($data, function ($item) use ($criteria) {
foreach ($criteria as $field => $value) {
if (!isset($item[$field]) || $item[$field] !== $value) {
return false;
}
}
return true;
});
}
}
// Usage in Symfony controller
class SearchController extends AbstractController
{
#[Route('/search', name: 'app_search')]
public function search(
Request $request,
LinearSearchService $searchService
): Response {
$users = $this->getDoctrine()
->getRepository(User::class)
->findAll();
$result = $searchService->searchEntities(
$users,
'email',
$request->query->get('email')
);
return $this->json(['user' => $result]);
}
}
class AutocompleteEngine
{
private array $dictionary;
private array $frequencyMap = [];
public function __construct(array $words)
{
sort($words);
$this->dictionary = $words;
// Build frequency map
foreach ($words as $word) {
$this->frequencyMap[$word] = 0;
}
}
/**
* Optimized autocomplete with prefix search
*/
public function suggest(string $prefix, int $maxResults = 5): array
{
$prefix = strtolower($prefix);
$results = [];
// Binary search for first match (optimization)
$start = $this->findFirstMatch($prefix);
if ($start === -1) {
return [];
}
// Linear search from first match
for ($i = $start; $i < count($this->dictionary); $i++) {
$word = $this->dictionary[$i];
if (!str_starts_with(strtolower($word), $prefix)) {
break; // No more matches
}
$results[] = [
'word' => $word,
'frequency' => $this->frequencyMap[$word]
];
if (count($results) >= $maxResults) {
break;
}
}
// Sort by frequency
usort($results, fn($a, $b) => $b['frequency'] <=> $a['frequency']);
return array_column($results, 'word');
}
private function findFirstMatch(string $prefix): int
{
$left = 0;
$right = count($this->dictionary) - 1;
$result = -1;
while ($left <= $right) {
$mid = (int)(($left + $right) / 2);
$comparison = strncasecmp($this->dictionary[$mid], $prefix, strlen($prefix));
if ($comparison >= 0) {
if ($comparison === 0) {
$result = $mid;
}
$right = $mid - 1;
} else {
$left = $mid + 1;
}
}
return $result;
}
public function recordUsage(string $word): void
{
if (isset($this->frequencyMap[$word])) {
$this->frequencyMap[$word]++;
}
}
}
// Usage
$words = ['apple', 'application', 'apply', 'banana', 'band', 'bandana'];
$autocomplete = new AutocompleteEngine($words);
print_r($autocomplete->suggest('app')); // ['apple', 'application', 'apply']
print_r($autocomplete->suggest('ban')); // ['banana', 'band', 'bandana']
$autocomplete->recordUsage('application');
$autocomplete->recordUsage('application');
print_r($autocomplete->suggest('app')); // ['application', 'apple', 'apply'] - sorted by frequency
class LRUCache
{
private array $cache = [];
private array $usage = [];
private int $capacity;
public function __construct(int $capacity)
{
$this->capacity = $capacity;
}
public function get(string $key): mixed
{
// Linear search in cache
if (!isset($this->cache[$key])) {
return null;
}
// Update usage order
$this->updateUsage($key);
return $this->cache[$key];
}
public function put(string $key, mixed $value): void
{
if (isset($this->cache[$key])) {
$this->cache[$key] = $value;
$this->updateUsage($key);
return;
}
// Evict least recently used if at capacity
if (count($this->cache) >= $this->capacity) {
$lruKey = $this->findLRU();
unset($this->cache[$lruKey]);
unset($this->usage[$lruKey]);
}
$this->cache[$key] = $value;
$this->updateUsage($key);
}
private function updateUsage(string $key): void
{
$this->usage[$key] = microtime(true);
}
private function findLRU(): string
{
$minTime = PHP_FLOAT_MAX;
$lruKey = null;
// Linear search for least recently used
foreach ($this->usage as $key => $time) {
if ($time < $minTime) {
$minTime = $time;
$lruKey = $key;
}
}
return $lruKey;
}
public function getStats(): array
{
return [
'size' => count($this->cache),
'capacity' => $this->capacity,
'items' => array_keys($this->cache)
];
}
}
// Usage
$cache = new LRUCache(3);
$cache->put('a', 1);
$cache->put('b', 2);
$cache->put('c', 3);
print_r($cache->getStats()); // size: 3, items: [a, b, c]
$cache->put('d', 4); // Evicts 'a' (least recently used)
print_r($cache->getStats()); // size: 3, items: [b, c, d]
$cache->get('b'); // Access 'b', making it most recently used
$cache->put('e', 5); // Evicts 'c' (now least recently used)
print_r($cache->getStats()); // size: 3, items: [b, d, e]

Write a function that finds both min and max in a single pass:

function findMinMax(array $arr): array
{
// Your code here
// Return ['min' => $min, 'max' => $max]
}
echo findMinMax([3, 1, 4, 1, 5, 9, 2, 6]);
// Should output: ['min' => 1, 'max' => 9]
Solution
function findMinMax(array $arr): array
{
if (empty($arr)) {
throw new InvalidArgumentException('Array cannot be empty');
}
$min = $max = $arr[0];
foreach ($arr as $value) {
if ($value < $min) {
$min = $value;
}
if ($value > $max) {
$max = $value;
}
}
return ['min' => $min, 'max' => $max];
}

Find the missing number in array [1..n]:

function findMissing(array $arr): int
{
// Array contains 1 to n with one number missing
// Your code here
}
echo findMissing([1, 2, 4, 5, 6]); // Should output: 3
Hint Use the formula: sum of 1 to n = n(n+1)/2
Solution
function findMissing(array $arr): int
{
$n = count($arr) + 1; // n is one more than array length
$expectedSum = $n * ($n + 1) / 2; // Sum of 1 to n
$actualSum = array_sum($arr);
return $expectedSum - $actualSum;
}
echo findMissing([1, 2, 4, 5, 6]); // Output: 3

Why it works: The sum of numbers from 1 to n is n(n+1)/2. If one number is missing, the difference between the expected sum and actual sum is the missing number.

Find two numbers that add up to a target:

function twoSum(array $nums, int $target): ?array
{
// Return indices of two numbers that add up to target
// Your code here
}
print_r(twoSum([2, 7, 11, 15], 9)); // Should output: [0, 1]
Solution
function twoSum(array $nums, int $target): ?array
{
$n = count($nums);
// Linear search: check all pairs
for ($i = 0; $i < $n; $i++) {
for ($j = $i + 1; $j < $n; $j++) {
if ($nums[$i] + $nums[$j] === $target) {
return [$i, $j];
}
}
}
return null;
}
print_r(twoSum([2, 7, 11, 15], 9)); // Output: [0, 1]

Why it works: We use nested loops to check all pairs of numbers. The outer loop starts at index 0, and the inner loop starts at the next index to avoid checking the same pair twice. When we find a pair that sums to the target, we return their indices.

Note: This is O(n²) time complexity. For better performance with large arrays, consider using a hash table for O(n) time complexity (covered in Chapter 13).

  • Linear search is O(n) - simple but slow for large datasets (n > 100)
  • Use linear search for: unsorted data, small arrays (n < 50), single searches, or linked lists
  • Avoid linear search when: data is sorted (use binary), multiple searches (k > log n), key lookups (use hash tables), or n > 100,000
  • The crossover point is around 50-100 items where other algorithms become significantly faster
  • Optimization techniques: move-to-front, transpose, frequency-count, Bloom filters for negative checks
  • Sentinel search eliminates boundary checks (minimal gain in practice)
  • Jump search is O(√n) for sorted arrays (better than linear, worse than binary)
  • Interpolation search is O(log log n) for uniformly distributed sorted data
  • PHP’s in_array() and array_search() use linear search internally - O(n) complexity
  • Decision rule: For n=10,000, if you search more than ~10 times, sort first and use binary search
  • Hash tables (isset/array_key_exists) provide O(1) lookups - dramatically faster for key-based searches
  • Linear search is the only option for linked lists (can’t binary search them efficiently)

In the next chapter, we’ll explore Binary Search, a much faster O(log n) algorithm that works on sorted data. You’ll learn how sorting your data first can dramatically improve search performance, and when the overhead of sorting is worth it.

All code examples from this chapter are available in the GitHub repository:

View Chapter 11 Code Samples

Files included:

  • 01-basic-linear-search.php - Basic linear search implementation with multiple implementations and use cases
  • 02-search-variants.php - Advanced search variants including sentinel search, jump search, and interpolation search
  • 03-search-with-conditions.php - Search with conditions and callbacks including findIndex(), findAll(), and predicates
  • 04-object-search.php - Searching in objects, associative arrays, and nested structures
  • 05-practical-applications.php - Practical real-world applications including grep, autocomplete, and validation
  • README.md - Complete documentation and usage guide

Clone the repository to run the examples locally:

Terminal window
git clone https://github.com/dalehurley/codewithphp.git
cd codewithphp/code/php-algorithms/chapter-11
php 01-basic-linear-search.php

Continue to Chapter 12: Binary Search.