11

I'm looking for a data structure that I can search with multiple keys. Easier to explain with an example:

var myDataStructure = new MultiKeyDataStructure<int, string, MyType>();
myDataStructure.Add(1, "some string 1", new MyType());
myDataStructure.Add(2, "some string 2", new MyType());

var myType = new MyType();
myDataStructure.Add(3, "some string 3", myType);

Tuple<string, MyType> t1 = myDataStructure[1];
Tuple<int, MyType> t2 = myDataStructure["some string 1"];
Tuple<int, string> t3 = myDataStructure[myType];

Is something like this possible, and if so, is there anything that already exists to do it? How would you implement something that treats everything like a key and returns all associated keys by searching for any of them?

Ideally you would also be allowed to use any number and/or type of parameters:

var myDataStructure = new MultiKeyDataStructure<int, string, Foo, Bar>();
ConditionRacer
  • 4,418
  • 6
  • 45
  • 67
  • 4
    So what will `t3` contain in your case? Your `MyType` objects aren't unique, so there are three touples of objects that match. – Servy Jan 11 '13 at 16:51
  • Several dictionaries where you add the same set of values? – O. R. Mapper Jan 11 '13 at 16:51
  • As far as I'm ware you can't do a param array for type parameters – Jodrell Jan 11 '13 at 16:58
  • Look at your last line. When you do `myDictionary[1]` how can you know that 1 refers to the first key instead of the last 2 keys ? – digEmAll Jan 11 '13 at 16:59
  • Why do you need to use multiple indexes? – Ryan Gates Jan 11 '13 at 16:59
  • @Jodrell I'm interested in any equivalent as well. Treat the above as pseudo-code. – ConditionRacer Jan 11 '13 at 16:59
  • 4
    I think what you want is a database. – Agent_L Jan 11 '13 at 17:03
  • @Agent_L Haha, I agree. Unfortunately I don't get to make that decision. – ConditionRacer Jan 11 '13 at 17:04
  • @Justin984 Still looking for an answer as to how duplicates are supported. Do you want to ensure that no keys (for any one column) are ever duplicated? Should the results be a collection of all matching Tuple types for that key? Should it just be one arbitrary key of the multiple valid ones? – Servy Jan 11 '13 at 17:06
  • well, not even 2 parameters dictionary matches your requirements. You can't easily get a key from a value, and values are not guaranteed to be unique, as you seem to require. – Agent_L Jan 11 '13 at 17:06
  • @Servy All values should be treated as keys, meaning everything is unique. There should only be one set returned for any key. Does that help? – ConditionRacer Jan 11 '13 at 17:07
  • I think he does not allow any duplicates. The name "dictionary" is misleading, it's a collection of n-tuples where each "collumn" is unique and indexed. – Agent_L Jan 11 '13 at 17:07
  • @Agent_L Dictionary is misleading, what should I rename it to? – ConditionRacer Jan 11 '13 at 17:08
  • It would be doable if you just had one type values and it would be ok to receive a list instead of a tuple. I bet your primary language is not statically-typed:) – ren Jan 11 '13 at 17:08
  • I have no idea. But dictionary implies only 1 index, that's what get ppl confused. – Agent_L Jan 11 '13 at 17:09
  • Is performance essential? How about doing linq queries? – Sebastian Graf Jan 11 '13 at 17:13
  • @Sebastian performance is not essential, I'm looking for the most elegant solution. – ConditionRacer Jan 11 '13 at 17:16
  • 3
    @Justin984 Which is more important, fast lookup speed or a smaller memory footprint? If you have a dictionary for each part of the key you have fast lookup and lots of memory; if you have one data structure and do a linear search on it for searching you'll be much slower but have less memory. – Servy Jan 11 '13 at 17:23
  • @Servy Lower memory footprint – ConditionRacer Jan 11 '13 at 17:28
  • 1
    @Justin984 - I just edited in an implementation with a much smaller footprint in my answer. – Bobson Jan 11 '13 at 17:43

8 Answers8

5

So here's one that will work for exactly three keys. You could follow the listed pattern to make one for 4, 5, 6, etc. keys. It would be a lot of code, but not a particularly difficult task (just tedious).

Note that since there's a dictionary for each part of the key it will use up quite a lot of memory; that's the price you pay for the flexibility of very fact access from any key.

public class MultiKeyDictionary<T1, T2, T3>
{
    private Dictionary<T1, Tuple<T1, T2, T3>> firstLookup = new Dictionary<T1, Tuple<T1, T2, T3>>();
    private Dictionary<T2, Tuple<T1, T2, T3>> secondLookup = new Dictionary<T2, Tuple<T1, T2, T3>>();
    private Dictionary<T3, Tuple<T1, T2, T3>> thirdLookup = new Dictionary<T3, Tuple<T1, T2, T3>>();

    public void Add(Tuple<T1, T2, T3> values)
    {
        if (!firstLookup.ContainsKey(values.Item1) &&
            !secondLookup.ContainsKey(values.Item2) &&
            !thirdLookup.ContainsKey(values.Item3))
        {
            firstLookup.Add(values.Item1, values);
            secondLookup.Add(values.Item2, values);
            thirdLookup.Add(values.Item3, values);
        }
        else
        {
            //throw an exeption or something.
        }
    }

    public Tuple<T1, T2, T3> GetFirst(T1 key)
    {
        return firstLookup[key];
    }

    public Tuple<T1, T2, T3> GetSecond(T2 key)
    {
        return secondLookup[key];
    }

    public Tuple<T1, T2, T3> GetThird(T3 key)
    {
        return thirdLookup[key];
    }

    public void RemoveFirst(T1 key)
    {
        var values = GetFirst(key);

        firstLookup.Remove(values.Item1);
        secondLookup.Remove(values.Item2);
        thirdLookup.Remove(values.Item3);
    }

    public void RemoveSecond(T2 key)
    {
        var values = GetSecond(key);

        firstLookup.Remove(values.Item1);
        secondLookup.Remove(values.Item2);
        thirdLookup.Remove(values.Item3);
    }

    public void RemoveThird(T3 key)
    {
        var values = GetThird(key);

        firstLookup.Remove(values.Item1);
        secondLookup.Remove(values.Item2);
        thirdLookup.Remove(values.Item3);
    }
}

Below is an entirely different approach. Instead of populating a lookup for each key it just stores all of the values in a single collection and performs a linear search to find an item for a given key. It will have O(n) Search/Remove time, but O(1) Add. The previous implementation has O(1) add, remove, and search, but takes up a lot more memory to do it.

public class MultiKeyDictionary2<T1, T2, T3>
{
    private HashSet<Tuple<T1, T2, T3>> lookup = new HashSet<Tuple<T1, T2, T3>>();
    private HashSet<T1> firstKeys = new HashSet<T1>();
    private HashSet<T2> secondKeys = new HashSet<T2>();
    private HashSet<T3> thirdKeys = new HashSet<T3>();

    public void Add(Tuple<T1, T2, T3> values)
    {
        if (lookup.Any(multiKey => object.Equals(multiKey.Item1, values.Item1) ||
            object.Equals(multiKey.Item2, values.Item2) ||
            object.Equals(multiKey.Item3, values.Item3)))
        {
            //throw an exception or something
        }
        else
        {
            lookup.Add(values);
        }
    }

    public Tuple<T1, T2, T3> GetFirst(T1 key)
    {
        return lookup.FirstOrDefault(values => object.Equals(values.Item1, key));
    }

    public Tuple<T1, T2, T3> GetSecond(T2 key)
    {
        return lookup.FirstOrDefault(values => object.Equals(values.Item2, key));
    }

    public Tuple<T1, T2, T3> GetThird(T3 key)
    {
        return lookup.FirstOrDefault(values => object.Equals(values.Item3, key));
    }

    public void RemoveFirst(T1 key)
    {
        var values = GetFirst(key);
        if (values != null)
            lookup.Remove(values);
    }

    public void RemoveSecond(T2 key)
    {
        var values = GetSecond(key);
        if (values != null)
            lookup.Remove(values);
    }

    public void RemoveThird(T3 key)
    {
        var values = GetThird(key);
        if (values != null)
            lookup.Remove(values);
    }
}
Servy
  • 202,030
  • 26
  • 332
  • 449
  • hah, I was just going to post my answer which is almost same! – Agent_L Jan 11 '13 at 17:19
  • Yeah. I waited a while, to see if any better options came up, but after 5 incorrect answers it seemed that getting something that's ugly but works at least on the board was worth doing. – Servy Jan 11 '13 at 17:21
  • @Bobson For the record, I'd never use it. It will be *much* slower. – Servy Jan 11 '13 at 18:17
2

Since you said you want compile-time type safety, there are a number of things you have to give up:

  1. The ability to have any number of parameters (C# does not have variadic generics)
  2. The ability to have multiple keys of the same type (the compiler will complain about ambiguous overloads)

These two limitations can be solved by using a reflection-based approach, but then you would lose the compile-time type safety.

So this is the solution you would use, according to your constraints (only works when all generic types are distinct!)

class TripleKeyDictionnary<TKey1, TKey2, TKey3>
{
    public Tuple<TKey2, TKey3> this[TKey1 key]
    {
        get
        {
            return _key1Lookup[key];
        }
    }

    public Tuple<TKey1, TKey3> this[TKey2 key]
    {
        get
        {
            return _key2Lookup[key];
        }
    }

    public Tuple<TKey1, TKey2> this[TKey3 key]
    {
        get
        {
            return _key3Lookup[key];
        }
    }

    private Dictionary<TKey1, Tuple<TKey2, TKey3>> _key1Lookup = new Dictionary<TKey1, Tuple<TKey2, TKey3>>();
    private Dictionary<TKey2, Tuple<TKey1, TKey3>> _key2Lookup = new Dictionary<TKey2, Tuple<TKey1, TKey3>>();
    private Dictionary<TKey3, Tuple<TKey1, TKey2>> _key3Lookup = new Dictionary<TKey3, Tuple<TKey1, TKey2>>();

    public void Add(TKey1 key1, TKey2 key2, TKey3 key3)
    {
        _key1Lookup.Add(key1, Tuple.Create(key2, key3));
        _key2Lookup.Add(key2, Tuple.Create(key1, key3));
        _key3Lookup.Add(key3, Tuple.Create(key1, key2));
    }
}
user703016
  • 37,307
  • 8
  • 87
  • 112
2

First of all, unfortunately there's nothing built-in, so you must implement something by hand.

The problem here is that you can't have a class with an unspecified number of generic type definition i.e. does not exist something like this:

class MultiKeyDictionary<T1, ...>
{}

So, either you can decide to implement some cases (2-keys,3-keys etc., using an approach similar to Tuple<> implementation), or you should give up type safety.

If you decide for the first approach, you should can do something like this (example with 3 keys):

class ThreeKeysDict<T1,T2,T3>
{
   var dict1 = new Dictionary<T1,Tuple<T2,T3>>();
   var dict2 = new Dictionary<T2,Tuple<T1,T3>>();
   var dict3 = new Dictionary<T3,Tuple<T1,T2>>();
   public void Add(T1 key1,T2 key2, T3 key3)
   {
      dict1.Add(key1,Tuple.Create(key2,key3));
      dict2.Add(key2,Tuple.Create(key1,key3));
      dict3.Add(key3,Tuple.Create(key1,key2));
   }
   public Tuple<T2,T3> GetByKey1(T1 key1)
   {
      return dict1[key1];
   }
   public Tuple<T1,T3> GetByKey2(T2 key2)
   {
      return dict2[key2];
   }
   public Tuple<T1,T2> GetByKey3(T3 key3)
   {
      return dict3[key3];
   }
}

The non generic version would be something like this:

class MultiKeyDict
{
    Dictionary<object, object[]>[] indexesByKey;
    public MultiKeyDict(int nKeys)
    {
        indexesByKey = new Dictionary<object, object[]>[nKeys];
    }
    public void Add(params object[] values)
    {
        if (values.Length != indexesByKey.Length)
            throw new ArgumentException("Wrong number of arguments given");
        var objects = values.ToArray();
        for (int i = 0; i < indexesByKey.Length; i++)
            this.indexesByKey[i].Add(values[i], objects);
    }
    public object[] Get(int keyNum, object key)
    {
        return this.indexesByKey[keyNum][key];
    }
}

These two approached both use a lot of memory if the number of different keys grows (because they adopt one dictionary for each key).


Disclaimer:

The pieces of codes are not tested and lack of null/out-of-range checking etc.
They're just to give you the overall idea.

digEmAll
  • 56,430
  • 9
  • 115
  • 140
1

When I run into situations like this, I just use two Dictionaries rather than trying to come up with some new data structure. Each Dictionary has one of the possible keys mapped to the value.

If you really want it to be abstracted, you could always make a class that internally uses two or more dictionaries, depending how many different types of keys you need.

MgSam
  • 12,139
  • 19
  • 64
  • 95
0

The closest thing you can get is probably a HashSet<Tuple<int, string, MyType>>. HashSets automatically check for duplicates, and a Tuple checks it's values for equivalencies.

class MultiKey<T1, T2, T3> : HashSet<Tuple<T1, T2, T3>>
{
    public bool Add(T1 t1, T2 t2, T3 t3)
    {
        return this.Add(Tuple.Create(t1, t2, t3));
    }
    public T1 Get(T2 t2, T3 t3)
    {
        var match = this.SingleOrDefault(x => x.Item2.Equals(t2) && x.Item3.Equals(t3));
        if (match == null) return default(T1);
        else return match.Item1;
    }
    public T2 Get(T1 t1, T3 t3)
    {
        var match = this.SingleOrDefault(x => x.Item1.Equals(t1) && x.Item3.Equals(t3));
        if (match == null) return default(T2);
        else return match.Item2;
    }
    public T3 Get(T1 t1, T2 t2)
    {
        var match = this.SingleOrDefault(x => x.Item1.Equals(t1) && x.Item2.Equals(t2));
        if (match == null) return default(T3);
        else return match.Item3;
    }
}

Usage:

key.Add(1, "Foo", new MyType("foo"));
key.Add(2, "Bar", new MyType("bar"));
key.Add(2, "Bar", new MyType("bar")); // Does not add, because it already exists.
var key1 = key.Get("Bar", new Foo("bar")); // Returns 2
var defaultKey = key.Get("Bar", new Foo("foo")); // Returns 0, the default value for an int
var key2 = key.Get(1, new Foo("foo")); // returns "Foo"

You need to make sure that MyType compares for equality by its values if you use it with a lot of news. Otherwise, creating a new one will guarantee a unique value.

Bobson
  • 13,498
  • 5
  • 55
  • 80
  • `It should be possible to write a class which inherits from that and adds the functionality to retrieve a key given the two other keys.` Well, given that that's the real question here you should really provide more than that in the answer. You thinking it's possibly isn't really an answer. How would you do it? – Servy Jan 11 '13 at 17:14
  • @Servy - It's a start. It points at a data structure which might help. I'll work on expanding it, though. – Bobson Jan 11 '13 at 17:19
  • I can tell you that there's no way to search a `HashSet` by an arbitrary property of the type it holds. You need to pick *one* that the IEqualityComparer is based on and that's it. So no, it's not possible to use the layout you've described. If it is, then demonstrate it; the answer is useless without that. – Servy Jan 11 '13 at 17:21
  • @Servy - Implementation provided. – Bobson Jan 11 '13 at 17:42
  • With `SingleOrDefault`, `HashSet` has no advantages over `List` or any other `Enumerable`. `Add` needs to check if any of tuple members aready is present, in other examples dictionaries made sure it's not possible. – Agent_L Jan 11 '13 at 17:52
  • Ooh, I'd missed that each key had to be unique - I thought it was just the combination, so I used `HashSet` for checking for that. Let me see what I can do... – Bobson Jan 11 '13 at 18:05
  • And, my next idea was pretty close to what @Servy posted as his second idea as well. Three `HashSet`s for tracking the keys. I'll just go upvote that one. – Bobson Jan 11 '13 at 18:13
0

I am not sure if there exists any such data structure, but you can create one.

Assuming keys/sub-keys will be unique

Following is the MultiKeyDictionary (using 2 internal dictionaries, one for keys(as object), and one for values).

public class MultiKeyDictionary<TValue> 
{
    private Dictionary<Guid, TValue> values;
    private Dictionary<Object, Guid> keys;

    public MultiKeyDictionary()
    {
        keys = new Dictionary<Object,Guid>();
        values = new Dictionary<Guid,TValue>();
    }
    public IEnumerable<Object> Keys
    {
        get { return keys.Keys.AsEnumerable();} // May group according to values here
    }

    public IEnumerable<TValue> Values
    {
        get { return values.Values;}
    }

    public TValue this[object key]
    {
        get 
        {
            if (keys.ContainsKey(key)) 
            {
                var internalKey = keys[key];
                return values[internalKey];
            }
            throw new KeyNotFoundException();
        }
    }


    public void Add(TValue value,object key1, params object[] keys) // key1 to force minimum 1 key
    {
        Add(key1 , value);
        foreach( var key in keys)
        {
        Add (key, value);
        }
    }

    private void Add(Object key, TValue value)
    {
        var internalKey = Guid.NewGuid();
        keys.Add( key, internalKey);
        values.Add(internalKey, value);     
    }   
}

It can be used as

MultiKeyDictionary<string> dict = new MultiKeyDictionary<string>();
dict.Add("Hello" , 1,2,3,"StringKey"); // First item is value, remaining all are keys
Console.WriteLine(dict[1]); // Note 1 is key and not intex
Console.WriteLine(dict[2]); // Note 2 is key and not index
Console.WriteLine(dict["StringKey"]);
Tilak
  • 30,108
  • 19
  • 83
  • 131
0

How about

class MultiKeyLookup<A, B, C> : IEnumerable<Tuple<A, B, C>>
{
    private readonly ILookup<A, Tuple<B, C>> a;
    private readonly ILookup<B, Tuple<A, C>> b;
    private readonly ILookup<C, Tuple<A, B>> c;
    private readonly IEnumerable<Tuple<A, B, C>> order;

    public MultiKeyLookup(IEnumerable<Tuple<A, B, C>> source)
    {
        this.order = source.ToList();
        this.a = this.order.ToLookup(
            o => o.Item1, 
            o => new Tuple<B, C>(o.Item2, o.Item3));
        this.b = this.order.ToLookup(
            o => o.Item2, 
            o => new Tuple<A, C>(o.Item1, o.Item3));
        this.c = this.order.ToLookup(
            o => o.Item3, 
            o => new Tuple<A, B>(o.Item1, o.Item2));
    }

    public ILookup<A, Tuple<B, C>> Item1
    {
        get
        {
            return this.a
        }
    }

    public ILookup<B, Tuple<A, C>> Item2
    {
        get
        {
            return this.b
        }
    }

    public ILookup<C, Tuple<A, B>> Item3
    {
        get
        {
            return this.c
        }
    }

    public IEnumerator<Tuple<A, B, C>> GetEnumerator()
    {
        this.order.GetEnumerator();
    }

    public IEnumerator IEnumerable.GetEnumerator()
    {
        this.order.GetEnumerator();
    }
}

Which you would use like,

var multiKeyLookup = new MultiKeyLookup(
    new[] {
        Tuple.Create(1, "some string 1", new MyType()),
        Tuple.Create(2, "some string 2", new MyType())}); 

var intMatches = multiKeyLookup.Item1[1];
var stringMatches = multiKeyLookup.Item2["some string 1"];
var typeMatches = multiKeyLookup.Item3[myType];
Jodrell
  • 34,946
  • 5
  • 87
  • 124
-2

System.Tuple was added with using it as a dictionary key kept in mind. Usage:

var dict = new Dictionary<Tuple<string, int>, DateTime>();
dict.Add(Tuple.Create("Louis", 14), new DateTime(1638, 9, 5));

Although Tuple syntax is cumbersome, the static factory method takes much of the pain on the creation site.

Sebastian Graf
  • 3,602
  • 3
  • 27
  • 38
  • I had the same thought, but then I realized the OP needed to look up any key given the other two... – Bobson Jan 11 '13 at 17:07