c# - 批评这个 C# Hashmap 实现?

标签 c# data-structures hashmap

我用 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");
        }
    }
}

最佳答案

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

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

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

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

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

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

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

关于c# - 批评这个 C# Hashmap 实现?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3652917/

相关文章:

c# - C#中如何使用SqlDataAdapter更新多行

c# - 使用c#从mysql返回两个值

c# - 资源静态时从不同的线程访问相同的资源

c++ - 结构与对象 C++

c++ - 在 C++ 中为 HashMap 编写有效的复制构造函数

c# - 如果返回将在里面,将使用销毁数据

data-structures - 存储字符串的最佳数据结构是什么

algorithm - 为什么在这两个不同的图上应用广度优先搜索时会出现时间差异?

java - 使用 HashMap 通过迭代更新值来计算 map 是一种好习惯吗?

rust - 如何将 Entry API 与仅在 Entry 为空时才构建的昂贵 key 一起使用?