28

Is there a Lower Bound function on a SortedList<K ,V>? The function should return the first element equal to or greater than the specified key. Is there some other class that supports this?

Guys - please read the question once again. I do not need a function that returns the key if it is present. I'm interested in scenario when there is no exact key matching.

I'm interested in O(log n) time. It means that I do not have a problem with foreach loop, but rather would like to have an efficient way of doing this.

I have done some tests on this.

Linq statements are not optimized by neither the compiler nor runtime machine, so they walk through all collection elements and are slow O(n). Based on Mehrdad Afshari answer, here is a Binary Search that works in O(log n) on the Keys collection:

public static int FindFirstIndexGreaterThanOrEqualTo<T>(
            this IList<T> sortedCollection, T key
        ) where T : IComparable<T> {
    int begin = 0;
    int end = sortedCollection.Count;
    while (end > begin) {
        int index = (begin + end) / 2;
        T el = sortedCollection[index];
        if (el.CompareTo(key) >= 0)
            end = index;
        else
            begin = index + 1;
    }
    return end;
}
nawfal
  • 66,413
  • 54
  • 311
  • 354
agsamek
  • 8,234
  • 11
  • 35
  • 43

6 Answers6

26

Binary search the SortedList.Keys collection.

Here we go. This is O(log n):

private static int BinarySearch<T>(IList<T> list, T value)
{
    if (list == null)
        throw new ArgumentNullException("list");
    var comp = Comparer<T>.Default;
    int lo = 0, hi = list.Count - 1;
    while (lo < hi) {
            int m = (hi + lo) / 2;  // this might overflow; be careful.
            if (comp.Compare(list[m], value) < 0) lo = m + 1;
            else hi = m - 1;
    }
    if (comp.Compare(list[lo], value) < 0) lo++;
    return lo;
}

public static int FindFirstIndexGreaterThanOrEqualTo<T,U>
                          (this SortedList<T,U> sortedList, T key)
{
    return BinarySearch(sortedList.Keys, key);
}
Tim Schmelter
  • 429,027
  • 67
  • 649
  • 891
mmx
  • 402,675
  • 87
  • 836
  • 780
  • Isn't the collection generated every time we read Keys property? – agsamek Feb 27 '09 at 13:35
  • 1
    agsamek: Nope, it's not regenerated. It'll return an instance of internal class KeyList which provides direct access to the elements in the original collection. Nothing is copied in the process. – mmx Feb 27 '09 at 13:47
  • The "no copy for Keys and Values" is the main diference with a SortedDictionary – Julien Roncaglia Feb 27 '09 at 15:02
  • A minor quibble, but I believe this does not correctly handle the edge case of all values in the list being less than the key. This example returns n, but it should return -1 (or throw exception). – terphi May 11 '12 at 02:45
  • @terphi You are right. It will return 0 in that case, which might or might not be the desired result depending on the context. In any case, that can be fixed with an if statement. – mmx May 11 '12 at 19:07
  • @ralu You can't do this without a linear search on a Dictionary. – mmx Nov 30 '12 at 23:37
  • 9
    To avoid the overflow: var m = low + (hi - low)/2 – Erwin Mayer Feb 06 '13 at 16:53
  • When the list is empty, this method throws an exception... it should return 0, shouldn't it ? – digEmAll Dec 18 '15 at 09:06
  • This repo contains the implementation of lower_bound() and upper_bound() for C# Array, List, and SortedList classes: https://github.com/mohanadHamed/MNDSearchUtil – Mohanad S. Hamed Jun 11 '17 at 08:33
  • Isn't this `O(log(2)N)`? – AgentFire Dec 17 '18 at 22:02
  • 1
    @AgentFire O(log(n, base1)) == O(log(n, base2)) for all valid bases. proof: log(n, base1) = log(n, base2) * log(base1, base2) and log(base1, base2) is a constant which makes it asymptotically irrelevant. So you can omit the base when talking about O(log(..)) of something – mmx Dec 17 '18 at 23:06
  • But assumed that the complexity of a call to `list[m]` is itself logarithmic, the whole function's complexity evaluates to `O(log(n)²)`, doesn't it? –  Jan 17 '19 at 15:35
  • Well, it's worth mentioning that there is a difference between just List and SortedList in terms of time complexity. Although you use IList for BinarySearch function the time complexity will be different for SortedList as accessing an element at random index is O(log n). So, the resulted complexity of this method is O(log² n). – Boris Parfenenkov Aug 13 '20 at 21:58
5

I'd go with LINQ (presuming you're using C#3), but using the overload of FirstOrDefault that takes a predicate:

first = sortedList.FirstOrDefault(x => x >= theObjectForComparison);

(a lot of the other Enumerable methods can also take predicates which is a nice shortcut)

Dave W
  • 429
  • 3
  • 10
3

Not aware of one, but it's a simple LINQ statement:

first = sortedList.Where(x => x >= theObjectForComparison).FirstOrDefault();

first will either be the first object that passes the comparison or default(T) (which is normally null).

Edit

DaveW's version:

first = sortedList.FirstOrDefault(x => x >= theObjectForComparison);

does the same job but could potentially be faster, you'd have to test it though.

Garry Shutler
  • 31,484
  • 12
  • 78
  • 118
0

Or you can write own extension method to do this. Note that all those functions are NOT guaranteed to go in a sequesce.

Migol
  • 7,852
  • 8
  • 46
  • 69
0

Is there a Lower Bound function on a SortedList<K ,V>? The function should return the first element equal to or greater than the specified key.

This example is implemented as an extension to SortedList and it returns the value of the smallest element greater than or equal to provided key. It returns default(TValue) in case all keys are smaller than provided key or if an empty list was provided

public static TValue LowerBound<TKey, TValue>(this SortedList<TKey, TValue> list, TKey key) {
  if (list.Count == 0)
    return default;

  var comparer = list.Comparer;
  if (comparer.Compare(list.Keys[list.Keys.Count - 1], key) < 0)
    return default; // if all elements are smaller, then no lower bound

  int first = 0, last = list.Count - 1;
  while (first < last) {
    var middle = first + (last - first) / 2;
    if (comparer.Compare(list.Keys[middle], key) >= 0)
      last = middle;
    else
      first = middle + 1;
  }
  return list[list.Keys[last]];
}

Usage example:

SortedList<string, Object> myList = new SortedList<string, Object>();
...
var value = myList.LowerBound<string, Object>("theKey");
-2

Hopefully this should be faster, depending on SortedList's implementation.

public static int FindFirstIndexGreaterThanOrEqualTo<K, V>(
        this SortedList<K, V> sortedCollection, K key
    ) where V : new()
{
    if (sortedCollection.ContainsKey(key))
    {
        return sortedCollection.IndexOfKey(key);
    }
    sortedCollection[key] = new V();
    int retval = sortedCollection.IndexOfKey(key);
    sortedCollection.Remove(key);
    return retval;
}