Skip to Content
Course content

156: Implementing Sorting and Searching Algorithms in C#

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time reviewing code from developers who are just starting to tackle algorithmic thinking, and there is one mistake that pops up constantly: the belief that "if the code produces the right output, the algorithm is correct."

The "It Works on My Machine" Complexity Trap

Let's say you're building a system to sort a list of 10 user profiles by their join date. You write a simple Bubble Sort because it's intuitive—you just swap adjacent elements until the list is sorted. You run it with your 10 test users, it finishes in a fraction of a millisecond, and you check it into the codebase. You think you've solved the problem.

// The "naive" approach that feels fine during testing
public void BubbleSort(List<User> users)
{
    int n = users.Count;
    for (int i = 0; i < n - 1; i++)
    {
        for (int j = 0; j < n - i - 1; j++)
        {
            if (users[j].JoinDate > users[j + 1].JoinDate)
            {
                var temp = users[j];
                users[j] = users[j + 1];
                users[j + 1] = temp;
            }
        }
    }
}

Here is where the trap snaps shut. When that code hits production and you suddenly have 10,000 users, your application doesn't just slow down—it crawls. Bubble Sort has a time complexity of $O(n^2)$. To put that in perspective: for 10 items, you do roughly 100 operations. For 10,000 items, you're looking at 100,000,000 operations. That is the difference between an instantaneous UI and a frozen application.

Thinking in Big O and Choosing the Right Tool

To write professional C#, you have to stop looking at whether the code works and start looking at how it scales. This is where we move from "naive" sorting to "efficient" sorting. In the real world, you'll almost always use Array.Sort() or LINQ's OrderBy() because they use highly optimized versions of QuickSort or Timsort. But as an engineer, you need to understand the mechanics of these algorithms to know when they'll fail you.

Take QuickSort, for example. Instead of blindly swapping every element, it uses a "divide and conquer" strategy. It picks a pivot and partitions the array into two halves: those smaller than the pivot and those larger. This brings the average complexity down to $O(n \log n)$. For our 10,000 users, that's roughly 133,000 operations instead of 100 million. I'll be honest: the math is the most important part of this lesson, not the syntax.

public void QuickSort(int[] array, int left, int right)
{
    if (left < right)
    {
        int pivotIndex = Partition(array, left, right);
        QuickSort(array, left, pivotIndex - 1);
        QuickSort(array, pivotIndex + 1, right);
    }
}

private int Partition(int[] array, int left, int right)
{
    int pivot = array[right];
    int i = left - 1;
    for (int j = left; j < right; j++)
    {
        if (array[j] <= pivot)
        {
            i++;
            (array[i], array[j]) = (array[j], array[i]); // C# Tuple swap
        }
    }
    (array[i + 1], array[right]) = (array[right], array[i + 1]);
    return i + 1;
}

Searching follows a similar logic. If your data is unsorted, you're stuck with a Linear Search—checking every single item until you find a match. But if you've already invested the effort to sort your data, you can use a Binary Search. Instead of scanning, you jump to the middle, see if your target is higher or lower, and discard half the remaining list with every single step. This turns a search through a million items from a million checks into roughly 20 checks.

public int BinarySearch(int[] sortedArray, int target)
{
    int low = 0;
    int high = sortedArray.Length - 1;

    while (low <= high)
    {
        int mid = low + (high - low) / 2;
        if (sortedArray[mid] == target) return mid;
        if (sortedArray[mid] < target) low = mid + 1;
        else high = mid - 1;
    }
    return -1; // Not found
}

My advice? Don't memorize these implementations for the sake of it. Instead, focus on the trade-off. Sorting takes time, but it makes searching nearly instantaneous. If you're searching a list once, don't bother sorting it. If you're searching it thousands of times, sort it first.




📋 Practical Task

Build a High-Score Leaderboard with Binary Search Insertion

Instead of sorting a list every time a new score is added (which is inefficient), your task is to build a Leaderboard class that maintains a sorted list of integers in descending order.

Requirements:

  • Implement a method AddScore(int score). This method must not use List.Sort() or LINQ. Instead, it should use a binary search logic to find the exact index where the score should be inserted to keep the list sorted.
  • Implement a method GetRank(int score) that returns the 1-based rank of a given score using binary search. If the score isn't in the list, return -1.
  • Ensure the leaderboard is sorted from Highest to Lowest.

Test Case: If you add scores in the order [50, 100, 75, 20], the internal list should be [100, 75, 50, 20]. Calling GetRank(75) should return 2.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.