【问题标题】:Critique this C# Hashmap Implementation?批评这个 C# Hashmap 实现?
【发布时间】:2010-09-06 16:27:15
【问题描述】:

我在 C# 中编写了一个 hashmap 作为自学练习。我想将链接实现为一种碰撞处理技术。起初我以为我会简单地使用 GetHashCode 作为我的哈希算法,但我很快发现使用 GetHashCode 返回的数字并不总是可行的(如果你想索引和数组,int 的大小会导致内存不足) number 和 numbers 可以是负数:()。所以,我想出了一个缩小数字的 kludgey 方法(参见 MyGetHashCode)。

是否有人对此实现(散列函数和一般而言)有任何指针/提示/批评?提前致谢!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace HashMap
{
    class Program
    {

        public class MyKVP<T, K>
        {
            public T Key { get; set; }
            public K Value { get; set; }
            public MyKVP(T key, K value)
            {
                Key = key;
                Value = value;
            }
        }


        public class MyHashMap<T, K> : IEnumerable<MyKVP<T,K>>
            where T:IComparable
        {

            private const int map_size = 5000;
            private List<MyKVP<T,K>>[] storage;
            public MyHashMap()
            {
                storage = new List<MyKVP<T,K>>[map_size];
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
            public IEnumerator<MyKVP<T, K>> GetEnumerator()
            {
                foreach (List<MyKVP<T, K>> kvpList in storage)
                {
                    if (kvpList != null)
                    {
                        foreach (MyKVP<T, K> kvp in kvpList)
                        {
                            yield return kvp;
                        }
                    }
                }
            }


            private int MyGetHashCode(T key)
            {
                int i = key.GetHashCode();
                if (i<0) i=i*-1;
                return i / 10000;
            }

            public void Add(T key, K data)
            {
                int value = MyGetHashCode(key);

                SizeIfNeeded(value);

                //is this spot in the hashmap null?
                if (storage[value] == null)
                {
                    //create a new chain
                    storage[value] = new List<MyKVP<T, K>>();
                    storage[value].Add(new MyKVP<T, K>(key, data));
                }
                else
                { 
                    //is this spot taken?
                    MyKVP<T, K> myKvp = Find(value, key);
                    if (myKvp != null) //key exists, throw
                    {
                        throw new Exception("This key exists. no soup for you.");
                    }

                    //if we didn't throw, then add us
                    storage[value].Add(new MyKVP<T, K>(key, data));
                }

            }

            private MyKVP<T, K> Find(int value, T key)
            {
                foreach (MyKVP<T, K> kvp in storage[value])
                {
                    if (kvp.Key.CompareTo(key) == 0)
                    {
                        return kvp;
                    }
                }

                return null;
            }

            private void SizeIfNeeded(int value)
            {
                if (value >= storage.Length)
                {
                    List<MyKVP<T, K>>[] temp = storage;
                    storage = new List<MyKVP<T, K>>[value+1];
                    Array.Copy(temp, storage, temp.Length);
                }
            }

            public K this[T key]
            {

                get 
                {
                    int value = MyGetHashCode(key);
                    if (value > storage.Length) { throw new IndexOutOfRangeException("Key does not exist."); }
                    MyKVP<T, K> myKvp = Find(value, key);
                    if (myKvp == null) throw new Exception("key does not exist");
                    return myKvp.Value;
                }
                set 
                {
                    Add(key, value);
                }
            }


            public void Remove(T key)
            {
                int value = MyGetHashCode(key);
                if (value > storage.Length) { throw new IndexOutOfRangeException("Key does not exist."); }
                if (storage[value] == null) { throw new IndexOutOfRangeException("Key does not exist."); }

                //loop through each kvp at this hash location
                MyKVP<T, K> myKvp = Find(value, key);
                if (myKvp != null)
                {
                    storage[value].Remove(myKvp);
                }
            }
        }

        static void Main(string[] args)
        {
            MyHashMap<string, int> myHashMap = new MyHashMap<string, int>();
            myHashMap.Add("joe", 1);
            myHashMap.Add("mike", 2);
            myHashMap.Add("adam", 3);
            myHashMap.Add("dad", 4);

            Assert.AreEqual(1, myHashMap["joe"]);
            Assert.AreEqual(4, myHashMap["dad"]);
            Assert.AreEqual(2, myHashMap["mike"]);
            Assert.AreEqual(3, myHashMap["adam"]);

            myHashMap.Remove("joe");

            try 
            {
                if (myHashMap["joe"] == 3) { }; //should throw 
            }
            catch (Exception) 
            {
                try { myHashMap.Add("mike",1); }
                catch (Exception) {

                    foreach (MyKVP<string, int> kvp in myHashMap)
                    { 
                        Console.WriteLine(kvp.Key + " " + kvp.Value.ToString());
                    }


                    return;
                }

            }

            throw new Exception("fail");
        }
    }
}

【问题讨论】:

  • 我建议通读 System.Collections.Generic.Dictionary 的源代码并找出 Microsoft 做出这些决定的原因。例如,您使用单个人口稀少的列表,该列表将消耗固定数量的内存,而与其中的项目数量无关。当初始存储耗尽时,您还将看到用于增长数据结构的逻辑。

标签: c# data-structures hashmap


【解决方案1】:

您的哈希方法是固定范围的。这意味着单个项目可能会导致创建 214748 个存储桶(如果它的哈希码重新散列为 214747)。一种更常用(并且几乎总是更好的方法)是从一个已知的初始大小开始(由于对域的了解),对于所有值来说足够大,或者从小开始并让 hashmap 自行调整大小。通过重新探测,需要调整大小的明显衡量标准是需要多少重新探测。使用您在此处尝试的链接,您将希望降低平均和最大链大小。这可以缩短最坏情况的查找时间,从而使您的平均查找时间更接近最佳情况 O(1)。

这种散列(以及初始表大小)的两种最常见的方法是使用素数或 2 的幂。前者被认为(尽管在这一点上有一些争论)提供更好的密钥分布,而后者允许更快的计算(两种情况都对输入哈希进行取模,但已知的数字是 2 的幂, 模可以作为二元与运算快速完成)。链接时使用 2 的幂的另一个优点是可以测试链以查看调整哈希大小是否实际上会导致该链被拆分(如果您有一个 8 值表并且有一个链其哈希值全部为 17、1 或 33,然后将表大小加倍仍将它们留在同一链中,但将它们加倍将重新分配它们)。

您没有提供替换语义的方法,这在 .NET 字典类型中很常见(如果已经存在具有该键的项目,则添加会出错,但分配给索引不会)。

尝试超出存储桶数量的检索错误对用户来说毫无意义,他们不关心存储桶是否存在,只关心密钥(他们不需要知道您的实现是如何工作的完全)。找不到密钥的两种情况都应该引发相同的错误(System.Collections.Generic.KeyNotFoundException 具有完全正确的语义,因此您可以重用它。

在这种情况下使用List 相当繁重。一般来说,我不赞成任何人说 BCL 集合太重,但在滚动你自己的集合时,通常是因为(1)你想从练习中学习,或者(2)BCL 集合不适合你的目的。在情况 (1) 中,您应该学习如何完成您开始的工作,在情况 (2) 中,您需要确保 List 没有您在 Dictionary 中发现的任何失败。

对于不了解实施细节的人来说,您的删除既会引发无意义的错误,也会引发不一致的错误(该存储桶中是否存在其他东西不是他们应该关心的)。由于删除不存在的项目并无害,因此更常见的是仅返回一个指示该项目是否存在的布尔值,并让用户决定这是否表示错误。删除项目后继续搜索整个存储桶也很浪费。

您的实现现在确实允许空键,这很合理(事实上,IDictionary&lt;TKey, TValue&gt; 的文档说实现可能会也可能不会这样做)。但是,您拒绝它们的方式是返回由尝试在 null 上调用 GetHashCode() 引起的 NullReferenceException,而不是检查并抛出 ArgumentNullException。对于接收NullReferenceException 的用户,表明集合本身为空。因此这是一个明显的错误。

【讨论】:

    【解决方案2】:
    1. Remove 方法绝不应引发异常。您正在尝试删除一个项目。如果它已经被移除,则不会造成任何伤害。 .Net 中的所有集合类都使用 bool 作为返回值来指示项目是否真的被删除。

    2. 不要抛出异常,抛出特定的异常。浏览 Collection 命名空间中的所有异常以找到合适的异常。

    3. 添加 TryGetValue

    4. 使用已经是 .Net 一部分的 KeyValuePair,而不是自己创建。

    5. 添加一个可以定义地图大小的构造函数。

    6. 当引发异常时,包括引发异常的详细信息。例如,不要写“This key exists”,而是写 string.Format("Key '{0}' already exists", key)

    【讨论】:

      【解决方案3】:

      很抱歉,这个类不能用作 HashMap 甚至是简单的字典。

      首先,GetHashCode() 返回的值不是唯一的。两个不同的对象,例如两个字符串,可能返回相同的哈希码值。在哈希码冲突的情况下,使用哈希码作为数组索引的想法只会导致记录丢失。我建议阅读有关 GetHashCode() 方法以及如何从 MSDN 实现它的信息。一些明显的例子是,如果你得到从 0 开始的所有可能的 Int64 值的哈希码,那么哈希码肯定会在某个时候发生冲突。

      另一件事是,for 循环查找很慢。您应该考虑使用二进制搜索进行查找。为此,您必须随时维护按键排序的键值对,这意味着您应该为 storage 变量使用 List 而不是数组,因此在添加新的键值对时,您可以将其插入到适当的索引处。

      毕竟,请确保在编写真正的哈希映射时,您意识到不同键的哈希码可以相同,并且永远不要使用 for-loop 从 0 到 len-1 进行查找。

      【讨论】:

      • 链接不就是为了这个吗?
      • 这正是 GetHashCode() 的用途。我建议您自己多阅读一下 GetHashCode() 方法。任意对象的哈希映射(而不是那些可以生成完美哈希的对象,因为它们来自已知集合)总是存在哈希冲突的风险,为此有各种技术,例如重新探测和链接(查询者使用链接这里)。实现 GetHashCode() 的人应该始终避免碰撞,但使用它的人必须处理碰撞仍然可能发生的事实。
      • 是的,我的错,我必须道歉,我浏览了你的代码太轻了。我上面的评论完全不准确。
      猜你喜欢
      • 1970-01-01
      • 2010-10-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多