0

Recently, I learned that the Sorted Dictionary class implements binary search over the keys, and I wanted to use this to my advantage. I am making a piecewise linear function class that represents a collection of lines over a bunch of intervals.

I defined an Interval class like this:

public class Interval : ICloneable, IComparable, IComparable<Interval>
{
    public Interval()
    {

    }
    public Interval(double start, double end)
    {
        Start = start;
        End = end;
    }

    // Properties
    public double Start { get; set; } = double.NaN;
    public double End { get; set; } = double.NaN;
    public double Span => End - Start;

    // Methods
    public object Clone() => MemberwiseClone();

    public int CompareTo(object obj)
    {
        return Start.CompareTo(obj);
    }

    public int CompareTo([AllowNull] Interval other)
    {
        if (Start < other.Start)
        {
            return -1;
        }
        else if (Start > other.Start)
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }

    public bool Contains(double x) => Start <= x && x <= End;
    public override string ToString() => $"[{Start}, {End}]";
}

And the SortedDictionary in question works like this in the piecewise function class:

public class PiecewiseLinearFunction : ICloneable
{
    ...
    // The dictionary
    public SortedDictionary<Interval, Line2D> Functions { get; set; } = new SortedDictionary<Interval, Line2D>(); // Where Line2D is just a class that contains a function definition for a line

    // Methods
    public Interval FindInterval(double x)
        => Functions.Keys.Where(interval => interval.Contains(x)).FirstOrDefault();
    public double Solve(double x)
    {
        var interval = FindInterval(x);
        if (interval != null && Functions.ContainsKey(interval))
        {
            return Functions[interval].Solve(x);
        }
        else
        {
            return double.NaN;
        }
    }
}

So as you can see, the PiecewiseLinearFunction.FindInterval(double x) method linearly searches through the dictionary's keys in order to find the interval that contains (or doesn't contain) x, which can be used for binary look-up, but that obviously defeats the purpose of doing the binary look-up at all.

I was wondering if I could somehow make the dictionary look up the double x value instead, and do a binary search over the intervals while checking if Interval.Contains(double x) is true or false to decide if there is a valid interval (key) and a corresponding line that can be used to get the function value at x.

In other words, is there a way to search with a predicate, something like FindInterval(double x) => Functions.Keys.FindBinary(i => i.Contains(x)).

JansthcirlU
  • 688
  • 5
  • 21
  • You'll face with precision loss for double values and should use some kind of tolerance – Pavel Anikhouski Aug 17 '20 at 07:07
  • I agree, and in the full code there are plenty of improvements to be made as well, but I'm mainly concerned with the look-up at the moment. – JansthcirlU Aug 17 '20 at 07:09
  • I don't think the fact that `SortedDictionary` uses a binary tree internally helps you here. All the consumer of the class knows is that the dictionary keys are sorted. You want to perform a binary search on the data, but `SortedDictionary` doesn't provide indexed access to the data, so isn't suitable for binary search. – Peter Duniho Aug 17 '20 at 07:41
  • So you mean I'd be better off writing a class that itself contains an interval and a line, and write a custom binary search for a collection of that class? – JansthcirlU Aug 17 '20 at 07:42
  • If the intervals can overlap I you need an encapsulating data-structure keeping track of extra information to be able to support binary-search, just a custom interval class and binary search are not enough. –  Aug 17 '20 at 08:22
  • A dictionary uses a hash lookup that reduces time from N/2 to Log(N). So definitely a dictionary help if you have an exact key. You are using contains which is different. I think you need to bin you data to speed up results by using Quantization : https://en.wikipedia.org/wiki/Quantization_(signal_processing) – jdweng Aug 17 '20 at 08:23
  • @Knoop though I haven't implemented any checks, I intend to never have them overlap but you're right, I should make sure of that explicitly. – JansthcirlU Aug 17 '20 at 08:39
  • @jdweng Well, the dictionary only accepts `Interval` objects as keys, whereas ideally I could use a predicate while searching. I'll update my question to include that. – JansthcirlU Aug 17 '20 at 08:40
  • The simple method with double is multiply the double by a constant like 1000 and round to an integer. Then the integer is the key to the dictionary. – jdweng Aug 17 '20 at 09:41

1 Answers1

0

So if the intervals don't overlap there is actually not really a need for a custom binary search.

First of all it helps if we make Interval comparable to double:

public class Interval: IComparable<double>
{
    public double Start { get; set; }
    public double End { get; set; }

    public bool Contains(double x) => x >= Start && x <= End;
    public int CompareTo(double other)
    {
        if (Contains(other))
            return 0;
        return other < Start ? 1 : -1;
    }
}

Now we create a List extension so we're able to do binary search with other types then just the type of the list:

public static class ListExtensions
{
    public static int BinarySearch<T, TU>(this List<T> sortedSource, TU val) where T : IComparable<TU>
    {
        return sortedSource.BinarySearch(default(T), Comparer<T>.Create((item, _) => item.CompareTo(val)));
    }
}

And now we can test it like this:

var list = new List<Interval>
{
    new Interval
    {
        Start = 0,
        End = 4
    },
    new Interval
    {
        Start = 6,
        End = 7
    },
    new Interval
    {
        Start = 10,
        End = 12
    }
};

var index = list.BinarySearch(6.0d); //index = 1
var interval = index < 0 ? null : list[index]; //interval = [6, 7]

index = list.BinarySearch(9.0d); //index = -3
interval = index < 0 ? null : list[index]; //interval = null
  • Oh, my bad, I just noticed you'd posted an answer, let me give it a shot real quick! – JansthcirlU Aug 17 '20 at 11:00
  • So if I use this implementation, I might as well use a regular `Dictionary` since that's a much faster look-up, right? The way I do currently do it would have a complexity of `O(n log n)` whereas combining binary search with a regular dictionary would have a complexity close to `O(log n)`? – JansthcirlU Aug 17 '20 at 12:05
  • @JansthcirlU That is correct, binary searching in the list and then doing a lookup in the dictionary combined would be expected `O(log n)`. Maybe wrap the whole thing in a custom datastructure that holds both the sorted `List` and the `Dictionary` to centralize the adding, removing etc. Then you can also use the BinarySearch for optimized insertions in the sorted `List`. –  Aug 17 '20 at 12:15