Given a populated SortedList
. I would like to get all the keys (or their index range, what should be a closed int interval (mis
You can solve the problem by running an adapted binary search on the Keys
twice to find the indexes that bound the range of interest in the Keys
collection.
Since IList<T>
does not offer binary search facilities you need to write your own. Fortunately, there is also the option of stealing the ready-made implementation from How to perform a binary search on IList.
Here's an adapted version to find the lower bound:
public static int LowerBound<T>(this IList<T> list, T value, IComparer<T> comparer = null)
{
if (list == null)
throw new ArgumentNullException("list");
comparer = comparer ?? Comparer<T>.Default;
int lower = 0, upper = list.Count - 1;
while (lower <= upper)
{
int middle = lower + (upper - lower) / 2;
int comparisonResult = comparer.Compare(value, list[middle]);
// slightly adapted here
if (comparisonResult <= 0)
upper = middle - 1;
else
lower = middle + 1;
}
return lower;
}
To implement UpperBound
, simply change
if (comparisonResult <= 0)
to
if (comparisonResult < 0)
It's now trivial to do this:
var low = set.Keys.LowerBound(value);
var high = set.Keys.UpperBound(value);
// These extra comparisons are required because the adapted binary search
// does not tell us if it actually found the needle. They could be rolled
// into the methods themselves, but this would require another out parameter.
if (set.Keys[low] != value) ++low;
if (set.Keys[high] != value) --high;
if (low <= high) /* remove keys in the range [low, high] */
As the list is sorted you can use binary search to locate the endpoints of your interval. Worst case performance will be O(log n).
I wondered why SortedList<TKey, TValue>
doesn't provide a BinarySearch
when it's already sorted by the keys. It also uses the method itself(f.e. in IndexOf
), but the array used is a private field.
So i've tried to create an extension method for this, have a look:
public static class SortedListExtensions
{
public static int BinarySearch<TKey, TValue>(this SortedList<TKey, TValue> sortedList, TKey keyToFind, IComparer<TKey> comparer = null)
{
// need to create an array because SortedList.keys is a private array
var keys = sortedList.Keys;
TKey[] keyArray = new TKey[keys.Count];
for (int i = 0; i < keyArray.Length; i++)
keyArray[i] = keys[i];
if(comparer == null) comparer = Comparer<TKey>.Default;
int index = Array.BinarySearch<TKey>(keyArray, keyToFind, comparer);
return index;
}
public static IEnumerable<TKey> GetKeyRangeBetween<TKey, TValue>(this SortedList<TKey, TValue> sortedList, TKey low, TKey high, IComparer<TKey> comparer = null)
{
int lowIndex = sortedList.BinarySearch(low, comparer);
if (lowIndex < 0)
{
// list doesn't contain the key, find nearest behind
// If not found, BinarySearch returns the complement of the index
lowIndex = ~lowIndex;
}
int highIndex = sortedList.BinarySearch(high, comparer);
if (highIndex < 0)
{
// list doesn't contain the key, find nearest before
// If not found, BinarySearch returns the complement of the index
highIndex = ~highIndex - 1;
}
var keys = sortedList.Keys;
for (int i = lowIndex; i < highIndex; i++)
{
yield return keys[i];
}
}
}
Create a sample SortedList
:
DateTime start = DateTime.Today.AddDays(-50);
var sortedList = new SortedList<DateTime, string>();
for(int i = 0; i < 50; i+=2)
{
var dt = start.AddDays(i);
sortedList.Add(dt, string.Format("Date #{0}: {1}", i, dt.ToShortDateString()));
}
DateTime low = start.AddDays(1); // is not in the SortedList which contains only every second day
DateTime high = start.AddDays(10);
Now you can use the extension method above to get the range of keys between a low- and high-key:
IEnumerable<DateTime> dateRange = sortedList.GetKeyRangeBetween(low, high).ToList();
Result:
04/04/2014
04/06/2014
04/08/2014
04/10/2014
Note that this is built from scratch and not really tested, but it could give you an idea anyway.