我在C#中写了一个hashmap作为自学练习。我想将链接实现为碰撞处理技术。起初我以为我只是使用GetHashCode作为我的哈希算法,但我很快发现使用GetHashCode返回的数字并不总是可行的(如果你想索引和数组,那么int的大小会导致内存不足。数字和数字可以是负数:()。所以,我想出了一种缩小数字的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");
}
}
}
答案 0 :(得分:5)
删除方法永远不应该抛出异常。您正在尝试删除项目。如果已经删除,则不会造成任何伤害。 .Net中的所有集合类都使用bool作为返回值来指示项目是否真的被删除。
不要抛出异常,抛出特定的异常。浏览Collection命名空间中的所有异常以查找合适的异常。
添加TryGetValue
使用KeyValuePair,它已经是.Net的一部分,而不是创建自己的。
添加一个可以定义地图大小的构造函数。
抛出异常时,请详细说明抛出异常的原因。例如,不要写“此键存在”,而是写 string.Format(“键'{0}'已存在”,键)
答案 1 :(得分:5)
您的哈希方法属于固定范围。这意味着单个项目可能会导致创建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
找到的任何失败。
对于不了解实现细节的人,以及不一致的错误(该桶中是否存在其他东西不是他们应该关心的东西),您的删除都会引发无意义的错误。由于删除不存在的项目是无害的,因此更常见的是仅返回指示项目是否存在的bool,并让用户决定是否表示错误。在物品被移除后继续搜索整个桶也是浪费。
您的实现现在允许空键,这是合理的(实际上,IDictionary<TKey, TValue>
的文档说实现可能会也可能不会这样做)。但是,拒绝它们的方式是通过尝试在null上调用NullReferenceException
而导致GetHashCode()
返回,而不是检查并抛出ArgumentNullException
。对于用户来说,收到NullReferenceException
表示集合本身为空。因此这是一个明显的错误。
答案 2 :(得分:-1)
很抱歉这样说,但是这个类不会用作HashMap甚至是简单的字典。
首先,从GetHashCode()返回的值不是唯一的。两个不同的对象,例如两个字符串,可能返回相同的哈希码值。使用哈希代码作为数组索引的想法然后简单地导致在哈希代码冲突的情况下记录丢失。我建议阅读 GetHashCode()方法以及如何从MSDN实现它。一些明显的例子是如果你得到从0开始的所有可能的Int64值的哈希码,那么哈希码肯定会在某个时候发生冲突。
另一件事是,for循环查找很慢。您应该考虑使用二进制搜索进行查找。为此,您必须随时维护按键排序的键值对,这意味着您应该使用List而不是 storage 变量的数组,因此在添加新的键值对时可以将其插入适当的索引。
毕竟,确保在编写真实哈希映射时,您意识到哈希码对于不同的键可以是相同的,并且永远不会使用for-loop从0到len-1进行查找。