C#中使用多维键的哈希表

88

我基本上是在寻找一种在C#中使用二维类型键访问哈希表值的方法。

最终,我将能够像这样做:

HashTable[1][false] = 5;
int a = HashTable[1][false];
//a = 5

这就是我一直在尝试做的...但没有成功

Hashtable test = new Hashtable();
test.Add(new Dictionary<int, bool>() { { 1, true } }, 555);
Dictionary<int, bool> temp = new Dictionary<int, bool>() {{1, true}};
string testz = test[temp].ToString(); 
16个回答

82

我认为更好的方法是将多维键的许多字段封装到一个类/结构体中。例如:

struct Key {
  public readonly int Dimension1;
  public readonly bool Dimension2;
  public Key(int p1, bool p2) {
    Dimension1 = p1;
    Dimension2 = p2;
  }
  // Equals and GetHashCode ommitted
}

现在你可以创建并使用一个常规的HashTable,并将此包装器用作Key。


10
不要忘记你需要重写 GetHashCode 和 Equals 方法才能在 Hashtable 中使用它。 - David M
7
@David,在这种情况下不需要。Equals的默认实现将在所有字段上执行相等性检查,这在此情况下可行。GetHashCode可能不如用户所希望的那样高效,但它也可以与默认实现一起使用。 - JaredPar
6
@David,话虽如此,实际上这通常是一个好的做法。 - JaredPar
6
@JaredPar 做这样做是值得的。我们最近发现了struct类型的默认GetHashCode实现存在性能问题。手动实现可以完全消除这个瓶颈。此外,虽然是另一种解决方案,但我们发现“字典中的字典”方法在所有常见操作(Dictionary<int, Dictionary<string, object>>)上的运行速度更快。然而,这种方法不允许组合键的某些部分为空,而像上面提到的struct/class键可以轻松地允许空值,而无需额外的工作。 - Adam Houldsworth
这个方法是可行的,但正如@Adam所提到的,值得注意的是结构体默认的GetHashCode性能可能很差(或者可能很好)。无论哪种情况,它都将是正确的(即与Equals()一致)。有些结构体,比如KeyValuePair<ushort, uint>似乎根本没有使用结构体中的任何字段,并且总是返回相同的哈希值 - 这样的键会给您带来O(n)的字典查找。只需意识到结构体并不是万能药,并且您_可能_最终必须实现自己的哈希码。 - bacar
显示剩余2条评论

79

现在,您可以使用新元组在C# 7.0中完成此操作:

// Declare
var test = new Dictionary<(int, bool), int>();

// Add
test.Add((1, false), 5);

// Get
int a = test[(1, false)];

4
注意:如果要在较旧的.NET目标框架上使用此功能,则需要使用额外的nuget包 - pogosama
1
注意:自Visual Basic 2017 (15.x)起,此方法也适用于VB.NET。 - Keith Stein

24

使用一个带有某种元组结构作为键的常规字典如何?

public class TwoKeyDictionary<K1,K2,V>
{
    private readonly Dictionary<Pair<K1,K2>, V> _dict;

    public V this[K1 k1, K2 k2]
    {
        get { return _dict[new Pair(k1,k2)]; }
    }

    private struct Pair
    {
        public K1 First;
        public K2 Second;

        public override Int32 GetHashCode()
        {
            return First.GetHashCode() ^ Second.GetHashCode();
        }

        // ... Equals, ctor, etc...
    }
}

10
不必实现Pair类,可以使用System.Tuple。 - Mugen
18
@Mugen Tuples在'09年还不存在。 - Defenestrator

21

我认为这可能更接近你要找的东西...

var data = new Dictionary<int, Dictionary<bool, int>>();

1
这有一些优点。使用citiesByState["wa"]["seattle"]更清晰美观。但是请注意,每个查询需要进行两次哈希比较和isEquals操作。当性能至关重要时,具有组合(元组)键的单个字典可能会快得多。我通常喜欢这个答案中的形式,因为它更优雅,但7.0使元组变得不那么丑陋了。 - Parrhesia Joe

21

如果有人最近来到这里,以下是一个评论者介绍的在 .Net 4.0 中快速且简单地完成此操作的示例:

class Program
{
  static void Main(string[] args)
  {
     var twoDic = new Dictionary<Tuple<int, bool>, String>();
     twoDic.Add(new Tuple<int, bool>(3, true), "3 and true." );
     twoDic.Add(new Tuple<int, bool>(4, true), "4 and true." );
     twoDic.Add(new Tuple<int, bool>(3, false), "3 and false.");

     // Will throw exception. Item with the same key already exists.
     // twoDic.Add(new Tuple<int, bool>(3, true), "3 and true." );

     Console.WriteLine(twoDic[new Tuple<int, bool>(3,false)]);
     Console.WriteLine(twoDic[new Tuple<int, bool>(4,true)]);
     // Outputs "3 and false." and "4 and true."
  }
}

6
我建议对jachymko的解决方案进行轻微调整,这将允许您避免为键值对创建类。相反,可以将私有字典包装在字典中,如下所示:
public class MultiDictionary<K1, K2, V>
{
    private Dictionary<K1, Dictionary<K2, V>> dict = 
        new Dictionary<K1, Dictionary<K2, V>>();

    public V this[K1 key1, K2 key2]
    {
        get
        {
            return dict[key1][key2];
        }

        set
        {
            if (!dict.ContainsKey(key1))
            {
                dict[key1] = new Dictionary<K2, V>();
            }
            dict[key1][key2] = value;
        }
    }
}

3
为什么?我思考了一秒,但对我来说似乎更糟糕,因为它具有更大的开销(更多的垃圾回收压力,更差的局部性),而且没有任何好处 - 在哈希表中搜索平均具有恒定的时间效率,所以没有必要做两次。 - user83286
4
不是因为任何效率原因,而是因为我认为代码更简单(看看你必须省略掉的内容)。如果效率是首要关注点,那么我同意你原来的解决方案更好。我不喜欢像Pair<T,U>这样的类型,特别是当.NET 4.0将会默认支持元组时。 - kvb
2
如果效率被定义为速度而不是内存使用,则使用更简单的类型在字典中使用直接的字典解决方案实际上往往会表现得更快。使用struct或其他类型创建的复合键会导致性能下降,原因我尚未确定。 - Adam Houldsworth
这正是我在使用五维数组时一直在寻找的。谢谢你。不过,有人能否向我解释一下如何在需要设置值和获取值时使用它呢?... - user3916429
如果有帮助到其他人的话... 初始化: MultiDictionary <string, string, string> arrayName = new MultiDictionary<string, string, string>(); 设置: arrayName[key1,key2] = value; 获取: string value = arrayName[key1,key2]; - user3916429
1
这种方法还允许MultiDictionary类轻松返回第二维中的所有键。假设您有一个州->城市->人员的字典,并且您想要使用一个数据结构来能够获取给定州中的所有城市以及给定州/城市对中的所有人员。 - wizard07KSU

4

你需要一个关键类来正确地实现DictonaryGetHashCode方法。

并且你可以扩展Dictonary使其以一种友好的方式访问。

KeyPair类:

public class KeyPair<Tkey1, Tkey2>
{
    public KeyPair(Tkey1 key1, Tkey2 key2)
    {
        Key1 = key1;
        Key2 = key2;
    }

    public Tkey1 Key1 { get; set; }
    public Tkey2 Key2 { get; set; }

    public override int GetHashCode()
    {
        return Key1.GetHashCode() ^ Key2.GetHashCode();
    }
    public override bool Equals(object obj)
    {
        KeyPair<Tkey1, Tkey2> o = obj as KeyPair<Tkey1, Tkey2>;
        if (o == null)
            return false;
        else
            return Key1.Equals(o.Key1) && Key2.Equals(o.Key2);
    }
}

扩展 Dictionary<>

public class KeyPairDictonary<Tkey1, Tkey2, Tvalue> 
    : Dictionary<KeyPair<Tkey1, Tkey2>, Tvalue>
{
    public Tvalue this[Tkey1 key1, Tkey2 key2]
    {
        get
        {
            return this[new KeyPair<Tkey1, Tkey2>(key1, key2)];
        }
        set
        {
            this[new KeyPair<Tkey1, Tkey2>(key1, key2)] = value;
        }
    }
}

您可以像这样使用它:
KeyPairDictonary<int, bool, string> dict = 
    new KeyPairDictonary<int, bool, string>();

dict[1, false] = "test";
string test = dict[1, false];

1
我建议您创建一个小的自定义类,公开bool和int属性,并重写其GetHashCode和Equals方法,然后将其用作键。

1

你需要使用嵌入的哈希表。如果你考虑一下你的问题,拥有两个键的哈希表是一个具有两个独立变量的函数,并且 f(x,y) 按定义是二维的。

但是你想要像使用一个哈希表一样使用它,而不是使用嵌入式哈希表。所以你需要创建一个对象,它包装了嵌入式哈希表的思想,并像单个哈希表一样运行。

几个要注意的问题:

  • 你想要迭代它,因此你需要重写 GetEnumerator() 方法。并且你需要自己的迭代器来正确地迭代二维数组。
  • 你需要做更多的检查以确保没有重复项。

我已经包含了代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Windows.Forms;

namespace YourProjectNameHere
{
    public class Hashtable2D
    {
        /// <summary>
        /// This is a hashtable of hashtables
        /// The X dim is the root key, and the y is the internal hashes key
        /// </summary>
        /// 
        private Hashtable root = new Hashtable();
        public bool overwriteDuplicates = false;
        public bool alertOnDuplicates = true;

        public void Add(object key_x, object key_y, object toStore)
        {
            if(root[key_x]!=null)//If key_x has already been entered 
            {
                Hashtable tempHT = (Hashtable)root[key_x];//IF the hash table does not exist then focus will skip to the catch statement
                if (tempHT[key_y] == null)  tempHT.Add(key_y, toStore);
                else handleDuplicate(tempHT, key_y, toStore);
            }else{//Making a new hashtable 
                Hashtable tempHT = new Hashtable();
                tempHT.Add(key_y, toStore);
                root.Add(key_x, tempHT);
            }

        }

        public void Remove(object key_x, object key_y)
        {
            try{
                ((Hashtable)root[key_x]).Remove(key_y);
            }catch(Exception e){
                MessageBox.Show("That item does not exist");
            }

        }

        public void handleDuplicate (Hashtable tempHT, object key_y, object toStore)
        {
            if (alertOnDuplicates) MessageBox.Show("This Item already Exists in the collection");

            if (overwriteDuplicates)
            {
                tempHT.Remove(key_y);
                tempHT.Add(key_y,toStore);
            }
        }

        public object getItem(object key_x, object key_y)
        {
            Hashtable tempHT = (Hashtable)root[key_x];
            return tempHT[key_y];
        }

        public ClassEnumerator GetEnumerator()
        {
            return new ClassEnumerator(root);
        }

        public class ClassEnumerator : IEnumerator
        {
            private Hashtable ht;
            private IEnumerator iEnumRoot;
            private Hashtable innerHt;
            private IEnumerator iEnumInner;

            public ClassEnumerator(Hashtable _ht)
            {
                ht = _ht;
                iEnumRoot = ht.GetEnumerator();

                iEnumRoot.MoveNext();//THIS ASSUMES THAT THERE IS AT LEAST ONE ITEM

                innerHt = (Hashtable)((DictionaryEntry)iEnumRoot.Current).Value;
                iEnumInner = innerHt.GetEnumerator();
            }

            #region IEnumerator Members

            public void Reset()
            {
                iEnumRoot = ht.GetEnumerator();
            }

            public object Current
            {
                get
                {
                    return iEnumInner.Current; 
                }
            }

            public bool MoveNext()
            {
                if(!iEnumInner.MoveNext())
                {
                    if (!iEnumRoot.MoveNext()) return false;
                    innerHt = (Hashtable)((DictionaryEntry)iEnumRoot.Current).Value;
                    iEnumInner = innerHt.GetEnumerator();
                    iEnumInner.MoveNext();
                }
                return true;
            }

            #endregion
        }

    }
}

0

我认为现在最简单的方法是使用Tupple.Create和ValueTuple.Create:

> var k1 =  Tuple.Create("test", int.MinValue, DateTime.MinValue, double.MinValue);
> var k2 = Tuple.Create("test", int.MinValue, DateTime.MinValue, double.MinValue);
> var dict = new Dictionary<object, object>();
> dict.Add(k1, "item");
> dict.Add(k2, "item");
An item with the same key has already been added....
> dict[k1] == dict[k2]
true

或者使用新的C#7元组语法创建元组键:

var k = (item1: "value1", item2: 123);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接