散列 Hash
Hash
我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数,也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地方。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种解决"冲突"的简便做法。
HashFunction
散列函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。
算法思想:散列的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
算法流程:
- 用给定的散列函数构造散列表
- 根据选择的冲突处理方法解决地址冲突
- 在散列表的基础上执行散列查找。
总的来说,"直接定址"与"解决冲突"是散列表的两大特点。
HashCode具有如下要求和期望:
-
必要的
- 相等的对象具有相等的HashCode,如果
a == b
,则hash(a) == hash(b)
- 在对象的生存期内,对象的HashCode不应发生改变
- hash函数不应引发异常
- 相等的对象具有相等的HashCode,如果
-
追求的
-
HashCode应该在值域内均匀分布
-
hash函数具有良好的性能
-
越相近的值,HashCode就应该越远离
hash(1.00) = 1 hash(1.01) = 23058430092136961 hash(1.02) = 46116860184273921
-
使用者(甚至是攻击者)难以从HashCode推出原值
-
public override int GetHashCode()
{
return (RealPart, ImagPart).GetHashCode();
} // 使用元组的Hash算法
我想到了一个绝妙的具体的Hash算法,但是这里地方太小了,写不下
HashTable
散列表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。散列表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整散列函数算法即可在时间和空间上做出取舍。
复杂度分析:
- 时间复杂度:对于无冲突的Hash表而言,时间复杂度为\(O(1)\)(注意,在查找之前我们需要构建相应的Hash表)。
- 空间复杂度:Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。
实现
基于拉链法散列表
每个散列值对应一个数组,当 N/M
大于一定值的时候需要加长并重新散列所有值
public class HashTable<Key, Value> {
private int N;
private int M { get => _List.Length; }
private List<Node>[] _List;
public HashTable() : this(997) {}
public HashTable(int m) {
_List = new List<Node>[m];
for (int i = 0; i < M; ++i)
_List[i] = new List<Node>();
}
private int Hash(Key key);
public Value Get(Key key) {
return _List[Hash(Key)].Find(key);
}
public void Put(Key key, Value value) {
_List[Hash(key)].Add(new Node(key, value));
}
}
public class Node
{
public readonly TKey Key;
public TValue Value;
public virtual bool IsNull { get => false; }
public Node(TKey key, TValue value)
{
Key = key;
Value = value;
}
}
public class NullNode : Node // 秉承不返回null的原则
{
public override bool IsNull { get => true; }
internal NullNode() : base(default(TKey), default(TValue)) { }
}
基于开放地址的散列表
开放地址中最简单的是线性探测法,当碰撞发生时我们直接检查下一个位置,这会产生三种结果
- 命中
- 未命中
- 继续查找
public class HashTable<Key, Value> {
private int N;
private int M { get => _Keys.Length; }
private Key[] _Keys;
private Value[] _Values;
public HashTable() : this(997) {}
public HashTable(int m) {
_Keys = new Key[m];
_Value = new Value[m];
}
private void Resize();
private int Hash(Key key);
public Value Get(Key key) {
for (int i = Hash(key); !_Keys[i].Equals(default(Key)); i = (i + 1) % M)
if (_Keys[i].Equals(key)) {
return _Values[i];
}
return default(Value);
}
public void Put(Key key, Value value) {
int i;
for (i = Hash(key); !_Keys[i].Equals(default(Key)); i = (i + 1) % M)
if (_Keys[i].Equals(key)) {
_Values[i] = value;
return;
}
_Keys[i] = key;
_Values[i] = value;
++N;
}
}
基于多散列函数的散列表
如果一个散列函数有冲突,则使用第二个,如果依然不行则使用第三个,直到成功
散列表代码如下
public class HashTable<TKey, TValue>
{
private readonly NullNode Nil = new NullNode();
public int M { get => _St.Length; }
public int N { get; private set; }
private SeqList<Node>[] _St;
public HashTable() : this(97) { }
public HashTable(int capacity)
{
_St = new SeqList<Node>[(uint)capacity];
N = 0;
for (int i = 0; i < capacity; i++)
_St[i] = new SeqList<Node>();
}
public void Insert(TKey key, TValue value)
{
if (N / M > 5)
Extend(M + 97);
Node t = Select(key);
if (t.IsNull)
{
Put(_St, new Node(key, value));
++N;
}
else
t.Value = value;
}
public bool Contains(TKey key)
{
return !Select(key).IsNull;
}
public TValue Get(TKey key, TValue defaultValue = default(TValue))
{
Node t = Select(key);
return t.IsNull ? defaultValue : t.Value;
}
public Node Select(TKey key)
{
foreach (var item in _St[Hash(key, M)])
if (item.Key.Equals(key))
return item;
return Nil;
}
public TValue this[TKey key]
{
get
{
Node t = Select(key);
if (t.IsNull)
throw new ArgumentException("Not Existed Key");
return t.Value;
}
set
{
Node t = Select(key);
if (t.IsNull)
Insert(key, value);
else
t.Value = value;
}
}
public bool Remove(TKey key)
{
var list = _St[Hash(key, M)];
for (int i = 0; i < list.Count; ++i)
if (list[i].Key.Equals(key))
{
list.RemoveAt(i);
return true;
}
return false;
}
private int Hash(TKey key, int capacity)
{
return (key.GetHashCode() & 0x7FFFFFFF) % capacity;
}
private void Extend(int minCapacity)
{
int capacity = Math.Max(minCapacity, MinPrime(2 * M, 4 * M + 1));
var newSt = new SeqList<Node>[capacity];
for (int i = 0; i < capacity; i++)
newSt[i] = new SeqList<Node>();
foreach (var list in _St)
foreach (var item in list)
Put(newSt, item);
_St = newSt;
}
private void Put(SeqList<Node>[] st, Node item)
{
st[Hash(item.Key, st.Length)].Add(item);
}
private int MinPrime(int i, int maxLimit)
{
while (i < maxLimit)
{
int s = (int)Math.Sqrt(++i) + 1;
int j = 1;
while (j != s)
if (i % j == 0)
break;
if (j == s)
break;
}
return i;
}
public System.Collections.Generic.IEnumerator<Node> GetEnumerator()
{
foreach (var list in _St)
foreach (var node in list)
yield return node;
}
}