C# Searching

Searching means finding whether a value exists in a collection, and often finding where it is. It is one of the most common operations in programming: checking a username, finding an order, locating a number, or matching an object by key. In C#, good searching code depends on the data structure, whether the data is sorted, and what equality or ordering means for the type being searched.

Overview: How Searching Works

A search algorithm answers a question such as: does this array contain 42, what index stores "Ada", or which customer has ID 1007? The simplest approach is linear search: inspect each item from beginning to end until a match is found or the collection is exhausted. Linear search works on almost any sequence, including arrays, lists, linked lists, and streamed data, because it makes no assumptions about order.

The tradeoff is cost. If a collection has n items, linear search is O(n) because it may need to inspect all n items. If the target is near the front, it finishes quickly; if the target is missing, it must check everything. For a small list this is usually fine. For a million records searched repeatedly, it can become the slowest part of the program.

Binary search is faster, but it has a strict requirement: the data must already be sorted according to the same ordering used by the search. Binary search compares the target with the middle item. If the target is smaller, the right half cannot contain it; if the target is larger, the left half cannot contain it. Each comparison discards about half the remaining search space, so the complexity is O(log n). A million sorted items take around twenty comparisons, not a million.

C# also gives you hash-based search through Dictionary<TKey,TValue> and HashSet<T>. These collections compute a hash code for the key, jump to a bucket, then check candidates in that bucket using equality. Average lookup is O(1), which is why dictionaries are the normal choice for repeated key-based searches. The important detail is that hash lookup depends on stable, correct Equals and GetHashCode behavior.

Under the CLR, arrays store values contiguously, so looping by index has excellent locality. A List<T> wraps an internal array, so searching it is similar. A LinkedList<T> must follow references from node to node, which is still O(n) but usually less cache-friendly. Search performance is not only about big-O notation; memory layout, comparisons, allocations, and repeated work matter too.

Syntax

// Linear search over any sequence
int index = Array.IndexOf(numbers, target);
bool exists = names.Contains("Mina");

// Binary search requires sorted data
Array.Sort(numbers);
int position = Array.BinarySearch(numbers, target);

// Hash-based lookup by key
Dictionary<int, string> usersById = new Dictionary<int, string>();
bool found = usersById.TryGetValue(42, out string? userName);
Approach Requires Sorted Data? Typical Complexity Use When
Linear search No O(n) The collection is small, unsorted, or searched once.
Binary search Yes O(log n) The data is sorted and searched many times.
Dictionary<TKey,TValue> No Average O(1) You search repeatedly by a unique key.
HashSet<T> No Average O(1) You only need membership: present or absent.

Examples

Example 1: Linear Search with a Custom Condition

using System;

class Program
{
    static void Main()
    {
        string[] products = { "Keyboard", "Mouse", "Monitor", "Dock" };
        string target = "Monitor";

        int index = FindProduct(products, target);
        Console.WriteLine($"Index of {target}: {index}");
        Console.WriteLine($"Contains Tablet: {FindProduct(products, "Tablet") != -1}");
    }

    static int FindProduct(string[] products, string target)
    {
        for (int i = 0; i < products.Length; i++)
        {
            if (products[i] == target)
            {
                return i;
            }
        }

        return -1;
    }
}

Output:

Index of Monitor: 2
Contains Tablet: False

This is the classic linear search pattern. The loop starts at index 0, checks each element, and returns as soon as it finds a match. Returning -1 is a common convention for not found because it cannot be a valid array index. The missing product forces the loop to inspect every item.

Example 2: Binary Search on a Sorted Array

using System;

class Program
{
    static void Main()
    {
        int[] scores = { 12, 18, 25, 37, 41, 56, 63, 79, 88 };

        PrintSearch(scores, 41);
        PrintSearch(scores, 50);
    }

    static void PrintSearch(int[] sortedNumbers, int target)
    {
        int index = BinarySearch(sortedNumbers, target);
        string result = index >= 0 ? $"found at index {index}" : "not found";
        Console.WriteLine($"{target}: {result}");
    }

    static int BinarySearch(int[] sortedNumbers, int target)
    {
        int left = 0;
        int right = sortedNumbers.Length - 1;

        while (left <= right)
        {
            int middle = left + (right - left) / 2;

            if (sortedNumbers[middle] == target)
            {
                return middle;
            }

            if (target < sortedNumbers[middle])
            {
                right = middle - 1;
            }
            else
            {
                left = middle + 1;
            }
        }

        return -1;
    }
}

Output:

41: found at index 4
50: not found

The array is sorted in ascending order, so binary search can eliminate half the remaining elements after each comparison. The midpoint formula left + (right - left) / 2 is preferred over (left + right) / 2 because it avoids integer overflow for very large indexes. When left moves beyond right, there is no possible range left to search.

Example 3: Fast Lookup with a Dictionary

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Dictionary<int, string> employeesById = new Dictionary<int, string>
        {
            [101] = "Ava",
            [205] = "Ben",
            [309] = "Chen"
        };

        PrintEmployee(employeesById, 205);
        PrintEmployee(employeesById, 400);
    }

    static void PrintEmployee(Dictionary<int, string> employeesById, int id)
    {
        if (employeesById.TryGetValue(id, out string? name))
        {
            Console.WriteLine($"ID {id}: {name}");
        }
        else
        {
            Console.WriteLine($"ID {id}: not found");
        }
    }
}

Output:

ID 205: Ben
ID 400: not found

A dictionary is usually better than repeatedly scanning a list when you search by an ID, username, SKU, or other unique key. TryGetValue performs one lookup and tells you both whether the key exists and what value belongs to it. This avoids the common inefficient pattern of calling ContainsKey first and then indexing into the dictionary, which repeats the lookup work.

How It Works Step by Step

  1. In linear search, the CLR executes the loop body for each item until the condition succeeds or the loop ends.
  2. Each comparison uses the type’s equality behavior. For strings, == compares text values, not object references.
  3. In binary search, left and right describe the only indexes that may still contain the target.
  4. The middle element is compared with the target. A smaller target moves right leftward; a larger target moves left rightward.
  5. For dictionary lookup, the key’s hash code selects a bucket inside the dictionary’s internal storage.
  6. If more than one key lands in the same bucket, the dictionary compares candidate keys with equality until it finds the right key or proves it is absent.
  7. As a dictionary grows, it resizes its internal storage to keep lookups efficient; this costs time at resize moments but keeps average lookup fast.

Built-in methods follow these same ideas. Array.IndexOf, List<T>.IndexOf, and LINQ FirstOrDefault are linear searches. Array.BinarySearch and List<T>.BinarySearch perform binary search, but only give meaningful results when the collection is sorted with a compatible comparer. Dictionary<TKey,TValue> and HashSet<T> use hashing instead of scanning.

Common Mistakes

Mistake 1: Using Binary Search on Unsorted Data

int[] values = { 40, 10, 30, 20 };
int index = Array.BinarySearch(values, 20); // Wrong: values is not sorted.

Binary search is not a magic faster version of search. It depends on the sorted-order rule to discard half the data safely. If the input is unsorted, the result is undefined for your intent: it may fail to find an existing value or return a misleading position. Sort first, or use linear search.

using System;

class Program
{
    static void Main()
    {
        int[] values = { 40, 10, 30, 20 };
        Array.Sort(values);

        int index = Array.BinarySearch(values, 20);
        Console.WriteLine($"Sorted values: {string.Join(", ", values)}");
        Console.WriteLine($"20 found at index {index}");
    }
}

Output:

Sorted values: 10, 20, 30, 40
20 found at index 1

Mistake 2: Getting the Loop Bound Wrong

for (int i = 0; i <= items.Length; i++)
{
    if (items[i] == target)
    {
        return i;
    }
}

The final valid array index is Length - 1. A loop condition of i <= items.Length eventually tries to read items[items.Length], which throws IndexOutOfRangeException. The correct condition is i < items.Length.

using System;

class Program
{
    static void Main()
    {
        int[] items = { 4, 8, 15, 16 };
        Console.WriteLine(Find(items, 15));
        Console.WriteLine(Find(items, 23));
    }

    static int Find(int[] items, int target)
    {
        for (int i = 0; i < items.Length; i++)
        {
            if (items[i] == target)
            {
                return i;
            }
        }

        return -1;
    }
}

Output:

2
-1

Best Practices

  • Use linear search for small collections, one-time searches, or data that is naturally unsorted.
  • Use binary search only when the collection is sorted with the same comparer used by the search.
  • For repeated lookups by ID or key, build a Dictionary<TKey,TValue> once instead of scanning a list again and again.
  • Use HashSet<T> when you only care whether values are present and do not need associated data.
  • Prefer TryGetValue over ContainsKey followed by index access.
  • Be explicit about string comparison rules. Use StringComparer.OrdinalIgnoreCase for case-insensitive dictionaries when appropriate.
  • Return a clear not-found value such as -1, null, or a bool plus an out value; document which convention your method uses.
  • Measure before replacing simple search code. For tiny collections, readability can matter more than theoretical complexity.

Practice Exercises

  1. Write a method that searches an array of temperatures and returns the first index whose value is below freezing. Return -1 if no value qualifies.
  2. Create a sorted array of names and use Array.BinarySearch to find a name. Then test what happens when the name is missing.
  3. Build a Dictionary<string, decimal> of product prices keyed by SKU. Use TryGetValue to print a price or a not-found message.

Summary

  • Searching finds whether data exists, and often where it exists.
  • Linear search works on unsorted data but may inspect every item.
  • Binary search is much faster on large data, but only when the data is sorted correctly.
  • Dictionaries and hash sets provide average constant-time lookup by using hash codes and equality.
  • The right search technique depends on collection size, ordering, lookup frequency, and the meaning of equality.
  • Most search bugs come from wrong assumptions: unsorted input, off-by-one indexes, or mismatched comparison rules.