What's annoying about SortedList.IndexOfKey

The Philosopher Developer

May 24, 2010

Update: Some time after writing this post, I created a project on GitHub for this.

Here's a problem I'm sure tons of developers have run into, resulting in much frustration.

Say you have a sorted list of some kind, but it's not a List(T), and it's not an array. In other words, it's an IList(T) that you know is sorted. If you want to search for an item within that list, it makes sense to utilize a binary search, rather than a linear search; but guess what? That doesn't exist in the .NET framework. There's List(T).BinarySearch, and there's Array.BinarySearch(T), but there's no general BinarySearch (just like there's no general Sort).

One data structure where this proves particularly frustrating is SortedList(TKey, TValue), because, although SortedList(TKey, Value) does have an IndexOfKey method, and this method is a binary search, it returns -1 if the value passed isn't found. This basically makes it a crippled version of Array.BinarySearch(T), which actually returns the negative bitwise complement of the "next best" index (that of the first value greater than the value searched for) -- a useful little piece of information that allows you to write code like this:

int closestIndex = Array.BinarySearch(myArray, someValue);
if (closestIndex < 0)
    closestIndex = ~closestIndex;

Now, why do you suppose IndexOfKey is inconsistent with List(T).BinarySearch and Array.BinarySearch(T)? Is it due to some subtle difference in implementation that is necessary for the internal structure of SortedList(TKey, TValue)?

Buh, no. Take a look at the source code for the IndexOfKey method using Reflector, and what you'll find is this peculiar little bit of code:

    int num = Array.BinarySearch<TKey>(this.keys, 0, this._size, key, this.comparer);
    if (num < 0)
    {
        return -1;
    }
    return num;

Seriously? You're telling me IndexOfKey actually uses Array.BinarySearch(T), and just refuses to return the same value?

This is NOT ACCEPTABLE (to me, anyway). Thankfully, Reflector allows us to take a gander at the implementation of Array.BinarySearch(T) as well, which can be refitted to work on any IList(T), no problem. (Believe me when I say this is extremely preferable to implementing one's own binary search... especially when you see how simple this implementation is.)

Behold, a generic BinarySearch method that will work on any IList(T), based on Microsoft's own Array.BinarySearch(T) implementation:

(I've also included the obvious overloads.)

public static int BinarySearch<T>(this IList<T> list, int index, int length, T value, IComparer<T> comparer) {
    if (list == null)
        throw new ArgumentNullException("list");
    else if (index < 0 || length < 0)
        throw new ArgumentOutOfRangeException(
            (index < 0) ? "index" : "length"
        );
    else if (list.Count - index < length)
        throw new ArgumentException();

    int lower = index;
    int upper = (index + length) - 1;

    while (lower <= upper) {
        int adjustedIndex = lower + ((upper - lower) >> 1);
        int comparison = comparer.Compare(list[adjustedIndex], value);
        if (comparison == 0)
            return adjustedIndex;
        else if (comparison < 0)
            lower = adjustedIndex + 1;
        else
            upper = adjustedIndex - 1;
    }

    return ~lower;
}

public static int BinarySearch<T>(this IList<T> list, T value, IComparer<T> comparer) {
    return list.BinarySearch(0, list.Count, value, comparer);
}

public static int BinarySearch<T>(this IList<T> list, T value) where T : IComparable<T> {
    return list.BinarySearch(value, Comparer<T>.Default);
}

Feel free to add this code to any static helper class you like. I know I will.