散列 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表而言,时间复杂度为
(注意,在查找之前我们需要构建相应的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; } }
本文作者:violeshnv
本文链接:https://www.cnblogs.com/violeshnv/p/16833436.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步