Creating a One-to-Many Map (MultiMap)




Creating a One-to-Many Map (MultiMap)

Problem

A Hashtable or a Dictionary<T,U> can map only a single key to a single value, but you need to map a key to one or more values. In addition, it may also be possible to map a key to null.

Solution

Use a Dictionary<T,U> with values that are a List<U>. This structure allows you to add multiple values (in the List<U>) for each key of the Dictionary<T,U>. The MultiMap<T,U> class shown in Figure, which is used in practically the same manner as a Dictionary<T,U> class, does this.

MultiMap class

using System;
using System Collections;
using System.Collections.Generic;

public class MultiMap<TKey, UValue> : IDictionary<TKey, IList<UValue>>
{
    private Dictionary<TKey, IList<UValue>> map =
        new Dictionary<TKey, IList<UValue>>();

    public IList<UValue> this[TKey key]
    {
        get {return (map[key]);}
        set {map[key] = value;}
    }

    public void Add(TKey key, UValue item)
    {
        AddSingleMap(key, item);
    }

    public void Add(TKey key, IList<UValue> items)
    {
        foreach (UValue val in items)
            AddSingleMap(key, val);
    }

    public void Add(KeyValuePair<TKey, IList<UValue>> keyValuePair)
    {
        foreach (UValue val in keyValuePair.Value)
            AddSingleMap(keyValuePair.Key, val);
    }

    public void Clear()
    {
        map.Clear();
    }

    public int Count
    {
        get {return (map.Count);}
    }

    public bool IsReadOnly
    {
        get {throw(new NotSupportedException(
             "This operation is not supported."));}
    }

    public bool Contains(TKey key)
    {
        return (map.ContainsKey(key));
    }

    public bool Contains(KeyValuePair<TKey, IList<UValue>> keyValuePair)
    {
        return (map.ContainsKey(keyValuePair.Key) &&
                map.ContainsValue(keyValuePair.Value));
    }

    public bool ContainsKey (TKey key)
    {
        return (map.ContainsKey(key));
    }

    public bool ContainsValue(UValue item)
    {
        if (item == null)
        {
            foreach (KeyValuePair<TKey, IList<UValue>> kvp in map)
            {
                if (((List<UValue>)kvp.Value).Count == 0)
                {
                    return (true);
                }
            }

            return (false);
        }
        else
        {
            foreach (KeyValuePair<TKey, IList<UValue>> kvp in map)
            {
                if (((List<UValue>)kvp.Value).Contains(item))
                {
                    return (true);
                }
            }

            return (false);
        }
    }

    IEnumerator<KeyValuePair<TKey, IList<UValue>>> IEnumerable<KeyValuePair<TKey,
                                   IList<UValue>>>.GetEnumerator()
    {
        return (map.GetEnumerator());
    }

    IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return (map.GetEnumerator());
    }

    public bool Remove(TKey key)
    {
        return (RemoveSingleMap(key));
    }

    public bool Remove(KeyValuePair<TKey, IList<UValue>> keyValuePair)
    {
        return (Remove(keyValuePair.Key));
    }

    protected virtual void AddSingleMap(TKey key, UValue item)
    {
        // Search for key in map Hashtable.
        if (map.ContainsKey(key))
        {
            // Add value to List in map.
            List<UValue> values = (List<UValue>)map[key];

            // Add this value to this existing key.
            values.Add(item);
        }
        else
        {
            if (item == null)
            {
                // Create new key and mapping to an empty List.
                map.Add(key, new List<UValue>());
            }
            else
            {
                List<UValue> values = new List<UValue>();
                values.Add(item);

                // Create new key and mapping to its value.
                map.Add(key, values);
            }
        }
    }

    protected virtual bool RemoveSingleMap(TKey key)
    {
        if (this.ContainsKey(key))
        {
            // Remove the key from KeysTable.
            return (map.Remove(key));
        }
        else
        {
           throw (new ArgumentOutOfRangeException("key", key,
                   "This key does not exists in the map."));
        }
    }

    public bool TryGetValue(TKey key, out UValue item)
    {
        throw (new NotSupportedException(
               "This operation is not supported, use " +
               "TryGetValue(TKey, out IList<UValue>) instead."));
    }

    public bool TryGetValue(TKey key, out IList<UValue> items)
    {
        return (map.TryGetValue(key, out items));
    }
    public ICollection<TKey> Keys
    {
        get { return (map.Keys); }
    }

    public ICollection<IList<UValue>> Values
    {
        get { return (map.Values); }
    }

    public void CopyTo(TKey[] arr, int index)
    {
        int cntr = 0;
        foreach (KeyValuePair<TKey, IList<UValue>> keyValuePair in map)
        {
            arr[cntr + index] = keyValuePair.Key;
            cntr++;
        }
    }

    public void CopyTo(KeyValuePair<TKey, IList<UValue>>[] arr, int index)
    {
        CopyTo(arr, index);
    }
}

The methods defined in Figure are of particular interest to using a MultiMap<T,U> object.

Members of the MultiMap class

Member

Description

Indexer

The get accessor obtains a List<U> of all values that are associated with a key. The set accessor adds an entire List<U> of values to a key. Its syntax is:

public List<U> this[T key]

where key is the key to be added to the MultiMap<T,U> through the set accessor, or it is the key with values that you want to retrieve via the get accessor.

Add method

Adds a key to the Dictionary<T,List<U>> and its associated value. Its syntax is:

Add(T key, T value)

where key is the key to be added to the MultiMap<T,U> and value is the value to add to the internal List<U> of the private map field.

Clear method

Removes all items from the MultiMap<T,U> object.

Count method

Returns a count of all keys in the MultiMap<T,U> object.

Clone method

Returns a deep copy of the MultiMap<T,U> object.

ContainsKey method

Returns a bool indicating whether the MultiMap<T,U> contains a particular value as its key. Its syntax is:

ContainsKey(T key)

where key is the key to be found in the MultiMap<T,U>.

ContainsValue method

Returns a bool indicating whether the MultiMap<T,U> contains a particular value. Its syntax is:

ContainsValue(T value)

where value is the object to be found in the MultiMap<T,U>.

Remove method

Removes a key from the MultiMap<T,U> and all its referent values in the internal map Dictionary<T, List<U>>. Its syntax is:

Remove(T key)

where key is the key to be removed.


Items may be added to a MultiMap<T,U> object by running the code shown in Figure.

Testing the MultiMap class

public static void TestMultiMap()
{
    string s = "foo";

    // Create and populate a MultiMap object.
    MultiMap<int, string> myMap = new MultiMap<int, string>();
    myMap.Add(0, "zero");
    myMap.Add(1, "one");
    myMap.Add(2, "two");
    myMap.Add(3, "three");
    myMap.Add(3, "duplicate three");
    myMap.Add(3, "duplicate three");
    myMap.Add(4, "null");
    myMap.Add(5, s);
    myMap.Add(6, s);

    // Display contents.
    foreach (KeyValuePair<int, List<string>> entry in myMap)
    {

        Console.Write("Key: " + entry.Key.ToString() + "\tValue: ");
        foreach (string str in myMap[entry.Key])
        {
            Console.Write(str + " : ");
        }
        Console.WriteLine();
    }

    // Obtain values through the indexer.
    Console.WriteLine();
    Console.WriteLine("((ArrayList) myMap[3])[0]: " + myMap[3][0]);
    Console.WriteLine("((ArrayList) myMap[3])[1]: " + myMap[3][1]);

    // Add items to MultiMap using a List.
    List<string> testArray = new List<string>();
    testArray.Add("BAR");
    testArray.Add("BAZ");
    myMap[10] = testArray;
    myMap[10] = testArray;

    // Remove items from MultiMap.
    myMap.Remove(0);
    myMap.Remove(1);

    // Display MultiMap.
    Console.WriteLine();
    Console.WriteLine("myMap.Count: " + myMap.Count);
    foreach (KeyValuePair<int, List<string>> entry in myMap)
{
        Console.Write("entry.Key: " + entry.Key.ToString() +
                      "\tentry.Value(s): ");
        foreach (string str in myMap[entry.Key])
        {
            if (str == null)
            {
                Console.Write("null : ");
            }
            else
            {
                Console.Write(str + " : ");
            }
        }
        Console.WriteLine();
    }

    // Determine if the map contains the key or the value.
    Console.WriteLine();
    Console.WriteLine("myMap.ContainsKey(2): " + myMap.ContainsKey(2));
    Console.WriteLine("myMap.ContainsValue(two): " +
    myMap.ContainsValue("two"));

    Console.WriteLine("Contains Key 2: " + myMap.ContainsKey(2));
    Console.WriteLine("Contains Key 12: " + myMap.ContainsKey(12));

    Console.WriteLine("Contains Value two: " + myMap.ContainsValue("two"));
    Console.WriteLine("Contains Value BAR: " + myMap.ContainsValue("BAR"));

    // Clear all items from MultiMap.
    myMap.Clear();
}

This code displays the following:

	Key: 0  Value: zero :
	Key: 1  Value: one :
	Key: 2  Value: two :
	Key: 3  Value: three : duplicate three : duplicate three :
	Key: 4  Value:
	Key: 5  Value: foo :
	Key: 6  Value: foo :

	((ArrayList) myMap[3])[0]: three
	((ArrayList) myMap[3])[1]: duplicate three

	myMap.Count:  6
	entry.Key:  2    entry.Value(s): two :
	entry.Key:  3    entry.Value(s): three : duplicate three : duplicate three :
	entry.Key:  4    entry.Value(s):
	entry.Key:  5    entry.Value(s): foo :
	entry.Key:  6    entry.Value(s): foo :
	entry.Key:  10    entry.Value(s): BAR : BAZ :

	myMap.ContainsKey(2):  True
	myMap.ContainsValue(two):  True
	Contains Key  2:   True
	Contains Key  12:   False
	Contains Value  two:   True
	Contains Value  BAR:   True

Discussion

A one-to-many map, or multimap, allows one object, a key, to be associated, or mapped, to zero or more objects. The MultiMap<T,U> class presented here operates similarly to a Dictionary<T,U>. The MultiMap<T,U> class contains a Dictionary<T, List<U>> field called map that contains the actual mapping of keys to values. Several of the MultiMap<T,U> methods are delegated to the methods on the map Dictionary<T, List<U>> object.

A Dictionary<T,U> operates on a one-to-one principle: only one key may be associated with one value at any time. However, if you need to associate multiple values with a single key, you must use the approach used by the MultiMap<T,U> class. The private map field associates a key with a single List<U> of values, which allows multiple mappings of values to a single key and mappings of a single value to multiple keys. As an added feature, a key can also be mapped to a null value.

Here's what happens when key-value pairs are added to a MultiMap<t,U> object:

  1. The MultiMap<T,U>.Add method is called with a key and value provided as parameters.

  2. The Add method checks to see whether key exists in the map Dictionary<T, List<U>> object.

  3. If key does not exist, it is added as a key in the map Dictionary<T, List<U>> object. This key is associated with a new List<U> as the value associated with key in this Hashtable.

  4. If the key does exist, the key is looked up in the map Dictionary<T, List<U>> object and the value is added to the key's List<U>.

To remove a key using the Remove method, the key and List<U> pair are removed from the map Dictionary<T, List<U>>. This allows removal of all values associated with a single key. The MultiMap<T,U>.Remove method calls the RemoveSingleMap method, which encapsulates this behavior. Removal of key "0", and all values mapped to this key, is performed with the following code:

	myMap.Remove(1);

To remove all keys and their associated values, use the MultiMap<T,U>.Clear method. This method removes all items from the map Dictionary<T, List<U>>.

The other major member of the MultiMap<T,U> class needing discussion is its indexer. The indexer returns the List<U> of values for a particular key through its get accessor. The set accessor simply adds the List<U> provided to a single key. This code creates an array of values and attempts to map them to key "5" in the myMap object:

	List<string> testArray = new List<string>();
	testArray.Add("BAR");
	testArray.Add("BAZ");
	myMap["5"] = testArray;

The following code makes use of the get accessor to access each value associated with key "3":

	Console.WriteLine(myMap[3][0]);
	Console.WriteLine(myMap[3][1]);
	Console.WriteLine(myMap[3][2]);

This looks somewhat similar to using a jagged array. The first indexer ([3] in the preceding examples) is used to pull the List<U> from the map Dictionary<T, List<U>>, and the second indexer is used to obtain the value in the List<U>. This code displays the following:

	three
	duplicate three
	duplicate three

This MultiMap<T,U> class also allows the use of the foreach loop to enumerate its key-value pairs. The following code displays each key-value pair in the MyMap object:

	foreach (KeyValuePair<int, List<string>> entry in myMap)
	{
	    Console.Write("Key: " + entry.Key.ToString() + "\tValue: ");
	    foreach (string str in myMap[entry.Key])
	    {
	        Console.Write(str + " : ");
	    }
	    Console.WriteLine();
	}

The outer foreach loop is used to retrieve all the keys and the inner foreach loop is used to display each value mapped to a particular key. This code displays the following for the initial MyMap object:

	Key: 0 Value: zero :
	Key: 1 Value: one :
	Key: 2 Value: two :
	Key: 3 Value: three : duplicate three : duplicate three :
	Key: 4 Value:
	Key: 5 Value: foo :
	Key: 6 Value: foo :

Two methods that allow searching of the MultiMap<T,U> object are ContainsKey and ContainsValue. The ContainsKey method searches for the specified key in the map Dictionary<T, List<U>>. The ContainsValue method searches for the specified value in a List<U> in the map Dictionary<T, List<U>>. Both methods return true if the key-value was found or false otherwise:

	Console.WriteLine("Contains Key 2: " + myMap.ContainsKey(2));
	Console.WriteLine("Contains Key 12: " + myMap.ContainsKey(12));

	Console.WriteLine("Contains Value two: " + myMap.ContainsValue("two"));
	Console.WriteLine("Contains Value BAR: " + myMap.ContainsValue("BAR"));

Note that the ContainsKey and ContainsValue methods are both case-sensitive.

See Also

See the "List<T> Class," "Dictionary<T,U> Class," and "IEnumerator Interface" topics in the MSDN documentation.