21

I need to write some code for linear interpolation and I am trying to figure out the most efficient way to search the Keys of a SortedList<K, V> for the upper and lower keys that surround my target key.

SortedList<int, double> xyTable = new SortedList<int, double>()
{
    {1, 10}, {2, 20}, {3, 30}, {4,40}
};

double targetX = 3.5;

What is the most efficient way to search the list and determine that 3.5 is between 3 and 4? I have a method / cheat that works for integers (temporarily insert the target Key into the list then find the index) but I figured I'd ask the pros so I could produce quality code.

Thanks.

nawfal
  • 70,104
  • 56
  • 326
  • 368
Nitax
  • 450
  • 1
  • 4
  • 18

4 Answers4

12

A binary search gives you decent performance on a list. However the Keys property on SortedList is of type IList, whereas BinarySearch is defined on List. Fortunately, you can find an implementation of binary search for IList in this related question:

How to perform a binary search on IList<T>?

Community
  • 1
  • 1
ColinE
  • 68,894
  • 15
  • 164
  • 232
  • 2
    This is my fav answer here :) Should have been built in on SortedList. – nawfal Jun 14 '14 at 09:33
  • 1
    And if you want to do more than just finding *existing* elements in the sorted list, like [iterating over the head or the tail of a SortedList](http://stackoverflow.com/a/31447218/709537) be sure to use [Antoine Aubry's version](http://stackoverflow.com/a/2948872/709537), rather than the accepted solution, which returns a mere `-1` in case of non-existing elements. – Evgeniy Berezovsky Jul 17 '15 at 00:29
4

In my case the source SortedList is not changing much, since its being used as a lookup table. So in this case it makes sense to convert the SortedList to a List<T> once. After that it is quite easy to use the built-in BinarySearch method of List<T>...

double targetX = 3.5;

// Assume keys are doubles, may need to convert to doubles if required here.
// The below line should only be performed sparingly as it is an O(n) operation.
// In my case I only do this once, as the list is unchanging.
List<double> keys = xyTable.Keys.ToList();

int ipos = keys.BinarySearch(targetX);

if (ipos >= 0)
{
    // exact target found at position "ipos"
}
else
{
    // Exact key not found: BinarySearch returns negative when the 
    // exact target is not found, which is the bitwise complement 
    // of the next index in the list larger than the target.
    ipos = ~ipos;
    if (ipos >= 0 && ipos < keys.Count)
    {
        if (ipos > 0)
        {
            // target is between positions "ipos-1" and "ipos"
        }
        else
        {
            // target is below position "ipos"
        }
    }
    else
    {
        // target is above position "ipos"
    }
}
dodgy_coder
  • 12,407
  • 10
  • 54
  • 67
  • *target is below position "ipos"* - do you mean it's below lowest key in array? – Mołot Jul 03 '15 at 12:15
  • 7
    Downvoted, because someone asking for a binary search is interested in performance. `ToList` however is a superfluous and slow O(n) operation and it is better for performance (CPU and memory consumption) to work directly on the `IList Keys`, as indicated by [ColinE's answer](http://stackoverflow.com/a/6101989/709537), which also links to a question with a [copy-and-pasteable answer](http://stackoverflow.com/a/2948872/709537). – Evgeniy Berezovsky Jul 17 '15 at 00:33
  • @EugeneBeresovsky thanks for your comment - I added a comment regarding this fact. In my case as I mentioned at the top, the list I was performing a binary search on is unchanging, and so I was only performing the ToList() once, in a static constructer method. – dodgy_coder Apr 25 '16 at 03:54
1

From MSDN,

The elements of a SortedList object are sorted by the keys either according to a specific IComparer implementation specified when the SortedList is created, or according to the IComparable implementation provided by the keys themselves. The index sequence is based on the sort sequence. When an element is added, it is inserted into SortedList in the correct sort order, and the indexing adjusts accordingly. When an element is removed, the indexing also adjusts accordingly. Therefore, the index of a specific key/value pair might change as elements are added or removed from the SortedList.

*****This method uses a binary search algorithm; therefore, this method is an O(log n) operation, where n is Count.*****

Starting with the .NET Framework 2.0, this method uses the collection’s objects’ Equals and CompareTo methods on item to determine whether item exists. In the earlier versions of the .NET Framework, this determination was made by using the Equals and CompareTo methods of the item parameter on the objects in the collection.

In other words, the method IndexOfKey in SortedList is actually using a binarySearch algorithm already, so there is no need to convert form SortedList to List in your case.

Hope it helps..

  • 2
    IndexOfKey only finds exact match. The question is clear that’s not what is needed, they need “upper and lower keys that surround my target key”. – Soonts Apr 24 '16 at 21:58
  • 1
    Since IndexOfKey is performing a BinarySearch algorithm, I'm curious why Microsoft wouldn't have implemented the more useful BinarySearch instead (or both). Seems a no brainer unless my brain is missing something? – Collie Jan 23 '20 at 22:26
1
public class Bounds
{
    int lower;
    int upper;

    public Bounds(int lower, int upper)
    {
       this.lower = lower;
       this.upper = upper;
    }
}

public Bounds BinarySearch(List<int> keys, double target)
{
    // lower boundary case returns the smallest key as the lower and upper bounds
    if (target < keys[0])
        return new Bounds(0, 0);

    else if (target < keys[1])
        return new Bounds(0, 1);

    // upper boundary case returns the largest key as the lower and upper bounds
    else if (target > keys[keys.Length - 1])
        return new Bounds(keys.Length - 1, keys.Length - 1);

    else if (target > keys[keys.Length - 2])
        return new Bounds(keys.Length - 2, keys.Length - 1);

    else
        return BinarySearch(keys, target, 0, keys.Length - 1);

}

// 'keys' is a List storing all of the keys from your SortedList.
public Bounds BinarySearch(List<int> keys, double target, int lower, int upper)
{
    int middle = (upper + lower)/2;

    // target is equal to one of the keys
    if (keys[middle] == target)
        return new Bounds(middle - 1, middle + 1);

    else if (keys[middle] < target && keys[middle + 1] > target)
        return new Bounds(middle, middle + 1);

    else if (keys[middle] > target && keys[middle - 1] < target)
        return new Bounds(middle - 1, middle);

    if (list[middle] < target)
         return BinarySearch(list, target, lower, upper/2 - 1);

    if (list[middle] > target)
         return BinarySearch(list, target, upper/2 + 1, upper);
}

This might work..I didn't test it out. If not, hopefully it's close enough that you can use it with minor tweaks. This is a strange problem, so I handled all of the boundary cases so I didn't have to think about what the algorithm would do when the range was down to 2 elements or less.

alexD
  • 2,368
  • 2
  • 32
  • 43