【澈丹,我想要个钻戒。】【小北,等等吧,等我再修行两年,你把我烧了,舍利子比钻戒值钱。】
                                ——自扯自蛋

  无论开发一个程序还是谈一场恋爱,都差不多要经历这么4个阶段:
  1)从零开始。没有束缚的轻松感。似乎拥有无限的可能性,也有相当多的不确定,兴奋、紧张和恐惧。
  2)从无到有。无从下手的感觉。一步一坎,进展缓慢。走弯路,犯错,投入很多产出很少。目标和现实之间产生强大的张力。疑惑、挫败、焦急和不甘心。
  3)渐入佳境。快速成长。创新,充实,满足。但是在解决问题的同时往往会不可避免地引入更多的问题和遗憾。
  4)接近成功。已经没有那么多的新鲜感和成就感,几乎是惯性般地努力前行。感觉成功在望,但又好像永远也不能100%搞定。有时,一心想要完成的欲望甚至超越了原本的目标。

  经过前面2篇,我们也来到了第4阶段。让我们深吸一口气,把遗留下来的这几个问题全部搞定吧。
  1)能不能支持所有的对象而不仅限于整数?
  2)如何支持所有整数而不只是正整数?
  3)被删除了的槽仍然占用查找时间。
  4)随着时间的推移,被标记为碰撞的槽越来越多,怎么办?
  5)必须在创建容器的时候指定大小,不能自动扩张。
  6)只是一个 HashSet,而不是HashTable。

  继续改造上一篇最后给出的 IntSet4。

支持所有对象而不仅限于整数

  要想支持所有对象而不只是整数,就需要一个能把各种类型的对象变换成整数的方法。这一点得到了 .net 特别的优待,Object 类的 GetHashCode() 就是专门干这个的。它提供的默认实现是:对于引用类型,每创建一个新对象都会把Object里的一个内部的计数器增加1,并把计数器的值作为这个对象的 HashCode;对于 struct 对象,将基于每个字段的 HashCode 计算得出一个整型值作为对象的 HashCode。Object 的子类型可以 override GetHashCode() 函数,对于整数类型的变量,GetHashCode() 的返回值与变量的值相同;小数、字符串等都有自己的变换规则(至于具体的规则限于篇幅将不再详细介绍)。总之,我们只要调用对象的 GetHashCode() 函数,把得到的整型值作为 k 的值就行了。另外还需要一个 Object 类型的变量 Key 保存添加的对象,我们把这两个变量封装到一个名为 Bucket 的 struct 里,Add()、Remove()、Contains() 函数也要做相应的修改:

public class HashSet1
{[DebuggerDisplay("Key = {Key}  k = {k}")]private struct Bucket{public Object Key;public int k;   // Store hash code;}private Bucket[] _buckets;private readonly int DELETED = -1;private int GetHashCode(Object key){return key.GetHashCode();}public HashSet1(int capacity){int size = GetPrime(capacity);_buckets = new Bucket[size];}public void Add(Object item){int i = 0; // 已经探查过的槽的数量Bucket bucket = new Bucket { Key = item, k = GetHashCode(item) };do{int j = DH(bucket.k, i); // 想要探查的地址if (_buckets[j].Key == null || _buckets[j].k == DELETED){_buckets[j] = bucket;return;}else{i += 1;}} while (i <= _buckets.Length);throw new Exception("集合溢出");}public bool Contains(Object item){int i = 0; // 已经探查过的槽的数量int j = 0; // 想要探查的地址int hashCode = GetHashCode(item);do{j = DH(hashCode, i);if (_buckets[j].Key == null)return false;if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item))return true;elsei += 1;} while (i <= _buckets.Length);return false;}public void Remove(Object item){int i = 0; // 已经探查过的槽的数量int j = 0; // 想要探查的地址int hashCode = GetHashCode(item);do{j = DH(hashCode, i);if (_buckets[j].Key == null)return;if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item)){_buckets[j].k = DELETED;return;}else{i += 1;}} while (i <= _buckets.Length);}// 其它部分与 IntSet4 相同
}

让 HashSet 支持所有整数

  GetHashCode() 的返回值是 int 类型,也就是说可能为负数。这时,因为数组的大小 m 是正数,所以由 k mod m 计算得出的数组下标也为负数,这可不行。解决方法是先把GetHashCode() 的返回值变换成正数再赋值给 k,最直接的做法是:
  int t = key.GetHashCode();
  uint k = (uint)t;
由于负数在计算机里以补码的形式保存,所以当 t 是负数时,相当于 k = uint.MaxValue - |t| + 1。这个方案的好处是能够利用 0 ~ uint.MaxValue 范围内的所有整数,更不容易发生碰撞。
不过 .net framework 的 HashTable 却是:
  int t = key.GetHashCode();
  int k = t & 0x7FFFFFFF;  // 把 t 的最高位设为0
由于负数在计算机里以补码的形式保存,所以当 t 是负数时,相当于 k = int.MaxValue - |t| + 1。这个方案的缺点是如果添加 -9 和 2147483639 这两个数字就会发生碰撞,但是因为实际的输入总是比较扎堆的,所以这个缺点也不太容易造成太大的性能问题。它的优点是省下了最高的一个比特位。因为我们在解决问题(3)的时候需要增加一个布尔类型的变量记录是否发生了碰撞,到时候如果利用这个节省下来的比特位,就不用增加一个布尔类型的变量了。按照这个方法修改一下代码:

public class HashSet2
{private int GetHashCode(Object key){return key.GetHashCode() & 0x7FFFFFFF;}// ...
}

另外,由于最高的那个比特位有别的用处了,所以不能再把删除的槽的 k 设置成 -1 了,那要设置成什么值好呢? 设置成什么值都不好,因为输入的 k 可能是任何正整数,所以总会有冲突的可能。所以我们改成把 Key 赋值为 _buckets 作为被删除的标志,完整的代码如下:

public class HashSet2
{[DebuggerDisplay("Key = {Key}  k = {k}")]private struct Bucket{public Object Key;public int k;   // Store hash code; sign bit means there was a collision.}private Bucket[] _buckets;private int GetHashCode(Object key){return key.GetHashCode() & 0x7FFFFFFF;}// 将 bucket 标记为已删除private void MarkDeleted(ref Bucket bucket){bucket.Key = _buckets;bucket.k = 0;}// 判断 bucket 是否是空槽或者是已被删除的槽private bool IsEmputyOrDeleted(ref Bucket bucket){return bucket.Key == null || bucket.Key.Equals(_buckets);}public HashSet2(int capacity){int size = GetPrime(capacity);_buckets = new Bucket[size];}public void Add(Object item){int i = 0; // 已经探查过的槽的数量Bucket bucket = new Bucket { Key = item, k = GetHashCode(item) };do{int j = DH(bucket.k, i); // 想要探查的地址if (IsEmputyOrDeleted(ref _buckets[j])){_buckets[j] = bucket;return;}else{i += 1;}} while (i <= _buckets.Length);throw new Exception("集合溢出");}public bool Contains(Object item){int i = 0; // 已经探查过的槽的数量int j = 0; // 想要探查的地址int hashCode = GetHashCode(item);do{j = DH(hashCode, i);if (_buckets[j].Key == null)return false;if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item))return true;elsei += 1;} while (i <= _buckets.Length);return false;}public void Remove(Object item){int i = 0; // 已经探查过的槽的数量int j = 0; // 想要探查的地址int hashCode = GetHashCode(item);do{j = DH(hashCode, i);if (_buckets[j].Key == null)return;if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item)){MarkDeleted(ref _buckets[j]);return;}else{i += 1;}} while (i <= _buckets.Length);}
}

减少已删除的槽对查找时间的影响

  我们在上一篇举过这样一个例子:假设有一个容量 m 为10 的 HashSet,先依次添加 0、1、2、3、4、5、6、7、8、9,然后再删除 0、1、2、3、4、5、6、7、8,这时调用 Contains(0),此函数会依次检查 _values[0]、_values[1]..._values[9],也就是把整个数组遍历了一遍!为什么会这样呢?因为我们在删除一个数字的时候,由于不知道这个数字的后面是否还有一个因与它碰撞而被安排到下一个空槽的数字,所以我们不敢直接把它设为 null。为了解决这个问题,需要一种方法可以指出每个槽是否发生过碰撞。最直接的方法是增加一个 bool 类型的变量,不过为了节省空间,我们将利用在解决问题(2)的时候预留出来的 k 的最高位。如果新添加的项与某个槽发生了碰撞,就把那个槽的碰撞位设为1。有了碰撞位,就可以知道:
  1)如果碰撞位为0,说明要么没发生过碰撞,要么它是碰撞链的最后一个槽。
  2)如果碰撞位为1,说明它不是碰撞链的最后一个槽。
对于碰撞位为0的槽,删除时可以直接把 Key 设为 null;对于碰撞位为1的槽,因为它不是碰撞链的最后一个槽,所以在删除时还是不能把它的 Key 设为null,而是设为 _buckets 表示已删除,并且要保留 k 的最高位为 1,把 k 的其它位设为0。由于我们其实是把一个 int 类型的变量 _k 当成了一个 bool 类型的变量和一个正整数类型的变量来用,所以要先改造一下 Bucket。(ps:用了很多位操作,要是 C# 也有类似于 C++ 的共用体那种东东的话就不用这么麻烦了)

public class HashSet3
{[DebuggerDisplay("Key = {Key}  k = {k}  IsCollided = {IsCollided}")]private struct Bucket{public Object Key;private int _k;   // Store hash code; sign bit means there was a collision.public int k{get { return _k & 0x7FFFFFFF; } // 返回去掉最高的碰撞位之后的 _kset {_k &= unchecked((int)0x80000000); // 将 _k 除了最高的碰撞位之外的其它位全部设为0_k |= value; // 保持 _k 的最高的碰撞位不变,将 value 的值放到 _k 的后面几位中去} }// 是否发生过碰撞public bool IsCollided{get { return (_k & unchecked((int)0x80000000)) != 0; } // _k 的最高位如果为1表示发生过碰撞}// 将槽标记为发生过碰撞public void MarkCollided(){_k |= unchecked((int)0x80000000); //  将 _k 的最高位设为1}}// ...
}

不需要修改 Contains() 函数,把 Add() 和 Remove() 函数按上面的讨论进行修改:

public class HashSet3
{public void Add(Object item){int i = 0; // 已经探查过的槽的数量int k = GetHashCode(item);do{int j = DH(k, i); // 想要探查的地址if (IsEmputyOrDeleted(ref _buckets[j])){_buckets[j].Key = item;_buckets[j].k = k; // 仍然保留 _k 的最高位不变return;}else{_buckets[j].MarkCollided();i += 1;}} while (i <= _buckets.Length);throw new Exception("集合溢出");}public void Remove(Object item){int i = 0; // 已经探查过的槽的数量int j = 0; // 想要探查的地址int hashCode = GetHashCode(item);do{j = DH(hashCode, i);if (_buckets[j].Key == null)return;if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item)) // 找到了想要删除的项{if (_buckets[j].IsCollided){   // 如果不是碰撞链的最后一个槽,要把槽标记为已删除MarkDeleted(ref _buckets[j]);}else{   // 如果是碰撞链的最后一个槽,直接将 Key 设为 null_buckets[j].Key = null;_buckets[j].k = 0;}return;}else{i += 1;}} while (i <= _buckets.Length);}// ...
}

  如您所见,一旦被扣上了“发生过碰撞的槽”的帽子,可就一辈子摘不掉了,即使碰撞链的最后一个槽已经被删除了,删除碰撞链的倒数第二个槽时也不会把碰撞位设为0,随着时间的推移,被标记为碰撞过的槽只会越来越多。如果频繁地删完了添、添完了删,发展到极致就可能产生每个槽都被标记为碰撞的情况,这时候 Contains() 函数的复杂度又变成 O(n) 了。解决这个问题的方法是发现被标记为碰撞的槽的超过一定的比例之后,重新排列整个数组,详细的方法会在后面实现容器的自动扩张时一并给出。
  我们可以很自然地想到,如果不是使用一个比特来位标记是否发生过碰撞,而是为每个槽增加一个整型变量精确记录发生过多少次碰撞,并且在删除碰撞链的最后一个槽时,把整条碰撞链的每一个槽的碰撞次数减少1,这样就可以完美解决上面那个问题了。这跟垃圾回收机制很像。之所以没有采用这种方法,一是因为这样做会使耗费的存储空间增加1倍,二是因为使用了双重散列法之后,不是很容易产生一长串碰撞链,如果不是特别频繁地删除、添加的话,问题不会很严重。

HashSet 的自动扩张

  在数组满了之后,再要添加新项,就得先把数组扩张得大一些。显然,让新数组只比老数组多一个槽是不恰当的,因为那样岂不是以后每添加一个新项都得去扩张数组?那么应该创建多大的新数组才合适呢?目前通常的做法是让新数组比老数组大1倍。
  另外,我们在前一篇已经提到过,开放寻址法在座位全部坐满的情况下性能并不好。上座率越低性能越好,但是也越浪费空间。这个上座率——也就是装载因子(_loadFactor)设为多少能达到性能与空间的平衡呢?.net framework 使用的是 0.72 这个经验值。数组允许添加的项的数量上限 _loadSize = _buckets.Length * _loadFactor。我们会添加一个整型的变量 _count 记录添加到数组中的项的个数(Add() 时 _count 增加1;Remove() 时 _count 减少1),当检测到 _count >= _loadSize 时就要扩张数组,而不会等到数组满了之后。
  还需要添加一个整型变量 _occupancy 用于记录被标记为碰撞的槽的数量。当 _occupancy > _loadSize 时,有可能造成 O(n) 复杂度的查找,所以这时也应该重新排列数组。
  首先,添加上面提到那几个变量,并在构造函数里初始化它们:

public class HashSet4
{private int _count = 0; // 添加到数组中的项的总数private int _occupancy = 0; // 被标记为碰撞的槽的总数private float _loadFactor = 0.72f; // 装载因子private int _loadsize = 0; // 数组允许放置的项的数量上限,_loadsize = _bucket.Length * _loadFactor,在确定数组的大小时被初始化。public HashSet4(int capacity){int size = GetPrime((int)(capacity / _loadFactor));if (size < 11)size = 11; // 避免过小的size_buckets = new Bucket[size];_loadsize = (int)(size * _loadFactor);}// ...
}

  然后实现用于扩张容器的 Expand() 函数和重新排列数组的 Rehash() 函数。因为 Rehash() 函数需要使用新数组的长度计算下标,所以需要改造一下 H1()、H2() 和 DH(),让它们可以接收数组的大小 m 做为参数。Rehash() 与 Add() 函数很像:

public class HashSet4
{private int H1(int k, int m){return k % m;}private int H2(int k, int m){return 1 + (k % (m - 1));}private int DH(int k, int i, int m){return (H1(k, m) + i * H2(k, m)) % m;}// 扩张容器private void Expand(){int newSize = GetPrime(_buckets.Length * 2);    // buckets.Length*2 will not overflowRehash(newSize);}// 将老数组中的项在大小为 newSize 的新数组中重新排列private void Rehash(int newSize){_occupancy = 0; // 将标记为碰撞的槽的数量重新设为0Bucket[] newBuckets = new Bucket[newSize]; // 新数组// 将老数组中的项添加到新数组中for(int oldIndex = 0; oldIndex < _buckets.Length; oldIndex++){if (IsEmputyOrDeleted(ref _buckets[oldIndex]))continue; // 跳过已经删除的槽好空槽// 向新数组添加项int i = 0; // 已经探查过的槽的数量do{int j = DH(_buckets[oldIndex].k, i, newBuckets.Length); // 想要探查的地址if (IsEmputyOrDeleted(ref newBuckets[j])){newBuckets[j].Key = _buckets[oldIndex].Key;newBuckets[j].k = _buckets[oldIndex].k;break;}else{if (newBuckets[j].IsCollided == false){newBuckets[j].MarkCollided();_occupancy++;}i += 1;}} while (true);}// 用新数组取代老数组_buckets = newBuckets;_loadsize = (int)(newSize * _loadFactor);}// ...
}

  Add() 和 Remove() 需要稍稍修改一下,加上统计 _count 和 _occupancy 的代码,并在需要的时候扩张或重排数组:

public class HashSet4
{public void Add(Object item){if (_count >= _loadsize)Expand(); // 如果添加到数组中的项数已经到达了上限,要先扩张容器else if (_occupancy > _loadsize && _count > 100)Rehash(_buckets.Length); // 如果被标记为碰撞的槽的数量和 _loadsize 一般多,就要重新排列所有的项int i = 0; // 已经探查过的槽的数量int k = GetHashCode(item);do{int j = DH(k, i, _buckets.Length); // 想要探查的地址if (IsEmputyOrDeleted(ref _buckets[j])){_buckets[j].Key = item;_buckets[j].k = k; // 仍然保留 _k 的最高位不变_count++;return;}else{if (_buckets[j].IsCollided == false){_buckets[j].MarkCollided();_occupancy++;}i += 1;}} while (i <= _buckets.Length);throw new Exception("集合溢出");}public void Remove(Object item){int i = 0; // 已经探查过的槽的数量int j = 0; // 想要探查的地址int hashCode = GetHashCode(item);do{j = DH(hashCode, i, _buckets.Length);if (_buckets[j].Key == null)return;if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item)) // 找到了想要删除的项{if (_buckets[j].IsCollided){   // 如果不是碰撞链的最后一个槽,要把槽标记为已删除MarkDeleted(ref _buckets[j]);}else{   // 如果是碰撞链的最后一个槽,直接将 Key 设为 null_buckets[j].Key = null;_buckets[j].k = 0;}_count--;return;}else{i += 1;}} while (i <= _buckets.Length);}// ...
}

  终于完成了一个比较实用的 HashSet 了!附上完整代码:

public class HashSet4
{[DebuggerDisplay("Key = {Key}  k = {k}  IsCollided = {IsCollided}")]private struct Bucket{public Object Key;private int _k;   // Store hash code; sign bit means there was a collision.public int k{get { return _k & 0x7FFFFFFF; } // 返回去掉最高的碰撞位之后的 _kset{_k &= unchecked((int)0x80000000); // 将 _k 除了最高的碰撞位之外的其它位全部设为0_k |= value; // 保持 _k 的最高的碰撞位不变,将 value 的值放到 _k 的后面几位中去}}// 是否发生过碰撞public bool IsCollided{get { return (_k & unchecked((int)0x80000000)) != 0; } // _k 的最高位如果为1表示发生过碰撞}// 将槽标记为发生过碰撞public void MarkCollided(){_k |= unchecked((int)0x80000000); //  将 _k 的最高位设为1}}private Bucket[] _buckets;private int GetHashCode(Object key){return key.GetHashCode() & 0x7FFFFFFF;}// 将 bucket 标记为已删除private void MarkDeleted(ref Bucket bucket){bucket.Key = _buckets;bucket.k = 0;}// 判断 bucket 是否是空槽或者是已被删除的槽private bool IsEmputyOrDeleted(ref Bucket bucket){return bucket.Key == null || bucket.Key.Equals(_buckets);}private int _count = 0; // 添加到数组中的项的总数private int _occupancy = 0; // 被标记为碰撞的槽的总数private float _loadFactor = 0.72f; // 装载因子private int _loadsize = 0; // 数组允许放置的项的数量上限,_loadsize = _bucket.Length * _loadFactor,在确定数组的大小时被初始化。public HashSet4(int capacity){int size = GetPrime((int)(capacity / _loadFactor));if (size < 11)size = 11; // 避免过小的size_buckets = new Bucket[size];_loadsize = (int)(size * _loadFactor);}// 扩张容器private void Expand(){int newSize = GetPrime(_buckets.Length * 2);    // buckets.Length*2 will not overflowRehash(newSize);}// 将老数组中的项在大小为 newSize 的新数组中重新排列private void Rehash(int newSize){_occupancy = 0; // 将标记为碰撞的槽的数量重新设为0Bucket[] newBuckets = new Bucket[newSize]; // 新数组// 将老数组中的项添加到新数组中for(int oldIndex = 0; oldIndex < _buckets.Length; oldIndex++){if (IsEmputyOrDeleted(ref _buckets[oldIndex]))continue; // 跳过已经删除的槽好空槽// 向新数组添加项int i = 0; // 已经探查过的槽的数量do{int j = DH(_buckets[oldIndex].k, i, newBuckets.Length); // 想要探查的地址if (IsEmputyOrDeleted(ref newBuckets[j])){newBuckets[j].Key = _buckets[oldIndex].Key;newBuckets[j].k = _buckets[oldIndex].k;break;}else{if (newBuckets[j].IsCollided == false){newBuckets[j].MarkCollided();_occupancy++;}i += 1;}} while (true);}// 用新数组取代老数组_buckets = newBuckets;_loadsize = (int)(newSize * _loadFactor);}// 质数表private readonly int[] primes = {3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369};// 判断 candidate 是否是质数private bool IsPrime(int candidate){if ((candidate & 1) != 0) // 是奇数{int limit = (int)Math.Sqrt(candidate);for (int divisor = 3; divisor <= limit; divisor += 2) // divisor = 3、5、7...candidate的平方根{if ((candidate % divisor) == 0)return false;}return true;}return (candidate == 2); // 除了2,其它偶是全都不是质数}// 如果 min 是质数,返回 min;否则返回比 min 稍大的那个质数private int GetPrime(int min){// 从质数表中查找比 min 稍大的质数for (int i = 0; i < primes.Length; i++){int prime = primes[i];if (prime >= min) return prime;}// min 超过了质数表的范围时,探查 min 之后的每一个奇数,直到发现下一个质数for (int i = (min | 1); i < Int32.MaxValue; i += 2){if (IsPrime(i))return i;}return min;}private int H1(int k, int m){return k % m;}private int H2(int k, int m){return 1 + (k % (m - 1));}private int DH(int k, int i, int m){return (H1(k, m) + i * H2(k, m)) % m;}public void Add(Object item){if (_count >= _loadsize)Expand(); // 如果添加到数组中的项数已经到达了上限,要先扩张容器else if (_occupancy > _loadsize && _count > 100)Rehash(_buckets.Length); // 如果被标记为碰撞的槽的数量和 _loadsize 一般多,就要重新排列所有的项int i = 0; // 已经探查过的槽的数量int k = GetHashCode(item);do{int j = DH(k, i, _buckets.Length); // 想要探查的地址if (IsEmputyOrDeleted(ref _buckets[j])){_buckets[j].Key = item;_buckets[j].k = k; // 仍然保留 _k 的最高位不变_count++;return;}else{if (_buckets[j].IsCollided == false){_buckets[j].MarkCollided();_occupancy++;}i += 1;}} while (i <= _buckets.Length);throw new Exception("集合溢出");}public bool Contains(Object item){int i = 0; // 已经探查过的槽的数量int j = 0; // 想要探查的地址int hashCode = GetHashCode(item);do{j = DH(hashCode, i, _buckets.Length);if (_buckets[j].Key == null)return false;if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item))return true;elsei += 1;} while (i <= _buckets.Length);return false;}public void Remove(Object item){int i = 0; // 已经探查过的槽的数量int j = 0; // 想要探查的地址int hashCode = GetHashCode(item);do{j = DH(hashCode, i, _buckets.Length);if (_buckets[j].Key == null)return;if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item)) // 找到了想要删除的项{if (_buckets[j].IsCollided){   // 如果不是碰撞链的最后一个槽,要把槽标记为已删除MarkDeleted(ref _buckets[j]);}else{   // 如果是碰撞链的最后一个槽,直接将 Key 设为 null_buckets[j].Key = null;_buckets[j].k = 0;}_count--;return;}else{i += 1;}} while (i <= _buckets.Length);}
}

HashSet 到 HashTable

  前面为了简单起见,一直都是只有 Key 而没有 Value。现在我们若想添加一个 Value 简直就跟玩儿似的:

public class HashTable
{[DebuggerDisplay("Key = {Key}  k = {k}  IsCollided = {IsCollided}")]private struct Bucket{public Object Key;public Object Value;// ...}// ...
}

当然,Add() 函数也要加上一个 Value 参数,还有按 Key 查找 Value 等等功能限于篇幅就不再啰嗦了。

HashTable 和泛型 Dictionary

  在 .net framework 的 HashTable 的源代码的注释里写着“泛型 Dictionary 是 Copy 自 HashTable(The generic Dictionary was copied from Hashtable's source - any bug fixes here probably need to be made to the generic Dictionary as well.)”,但是瞎了我的极品高清三角眼,怎么看了半天也没找着双重散列的身影呢?除了是泛型的以外,Dictionary 的实现与 HashTable 还有多少不同之处呢?也许下次我们可以一起研究下 Dictionary 的源代码。

  

转载于:https://www.cnblogs.com/1-2-3/archive/2010/10/18/hash-table-part3.html

白话算法(6) 散列表(Hash Table) 从理论到实用(下)相关推荐

  1. 白话算法(6) 散列表(Hash Table)从理论到实用(中)

    不用链接法,还有别的方法能处理碰撞吗?扪心自问,我不敢问这个问题.链接法如此的自然.直接,以至于我不敢相信还有别的(甚至是更好的)方法.推动科技进步的人,永远是那些敢于问出比外行更天真.更外行的问题, ...

  2. 散列表(Hash Table)

    散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度.这个映射函数叫做散列 ...

  3. 什么是散列表(Hash Table)

    散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构.也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度 ...

  4. 数据结构--散列表 Hash Table

    文章目录 1.线性探测 哈希表代码 2.拉链法 哈希表代码 1. 散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来.可以说,如果没有数组,就没有散列表. ...

  5. 【数据结构与算法】散列表

    一.散列表的由来? 1.散列表来源于数组,它借助散列函数对数组这种数据结构进行扩展,利用的是数组支持按照下标随机访问元素的特性. 2.需要存储在散列表中的数据我们称为键,将键转化为数组下标的方法称为散 ...

  6. Redis散列表(hash)使用

    Redis有5种常用的数据结构,分别为:string(字符串),list(列表),hash(哈希表或散列表),set(集合)和zset(有序集合).5种数据结构指的是作为键值对的值存在于Redis库中 ...

  7. 【常用算法】散列(hash)

    散列(hash)定义 将元素通过一个函数(H(key))转换为整数,使得该整数可以尽量唯一的代表这个元素 散列最基本的对应关系就是对应其本身H(key)=key(很常用) 先看一个简单的问题 随机给一 ...

  8. 散列表(上):Word文档中的单词拼写检查功能是如何实现的?

    [思考题]:在Word里面输入一个错误的英文单词,它会用标红的方式提示"拼写错误".Word的这个单词拼写检查功能,虽然很小但是却非常实用.你有没有想过,这个功能是如何实现的? 1 ...

  9. 散列表查找为何如此之快

    一.散列函数 散列是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key).建立了关键字与存储位置的映射关系,公式如下:  存储位置 =f(关键字) ...

最新文章

  1. 怎么不让html网页自动跳转,javascript怎么禁止跳转页面?
  2. 日期格式转换成时间戳格式php,php日期转时间戳,指定日期转换成时间戳
  3. R语言里面的循环变量
  4. OMEGA3-补充注意事项
  5. java 怎么调试到第三方库的内部,在有源码的情况下
  6. php时间转分钟前,PHP把时间转换成几分钟前几小时前几天前
  7. STM32之外部中断例程
  8. ubuntu18.04安装wireshark3.x与tshark3.x
  9. eclipse Filter web.xml 问题解决 异步请求@WebServlet
  10. 学习node.js的一些笔记
  11. 关于我的家乡介绍网站设计—— 大连介绍(6页) 网页设计作业 / 家乡网页设计作业,网页设计作业 / 家乡网页设计成品,网页设计作业 / 我的家乡网页设计成品模板下载
  12. HTML个人网站设计(源码)
  13. Java实现九宫格游戏
  14. 关于个人网站的盈利模式,可能你还不知道?
  15. 信息学奥赛一本通1003:对齐输出
  16. 还在用收费的工具处理PDF?用Python助力冲破会员牢笼
  17. MIT视频新研究:让7000多人看了48个广告,发现「眼见为实」并不成立
  18. linux线程篇,linux线程篇 (二) 线程的基本操作
  19. php取FBOX数据,云平台制作(1)-OPC Client取数模块的制作
  20. MySQL执行-SQL执行顺序

热门文章

  1. Spring基于注解的方式二
  2. Git使用手册:HTTPS和SSH方式的区别和使用
  3. 关于Bus的几个问题
  4. Zookeeper数据一致性原理
  5. tomcat基础架构剖析
  6. HanLP的自定义词典使用方式与注意事项介绍
  7. vue苹果浏览器微信公众号底部回退栏如何固定或隐藏
  8. 【Windows Server 2019】文件共享,應該不支持 Everyone 訪問
  9. poj1163 数字三角形 (动态规划)
  10. 【Keras】从两个实际任务掌握图像分类