散列 Hash

Hash

我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数,也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地方。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种解决"冲突"的简便做法。

HashFunction

散列函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。

算法思想:散列的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。

算法流程:

  1. 用给定的散列函数构造散列表
  2. 根据选择的冲突处理方法解决地址冲突
  3. 在散列表的基础上执行散列查找。

总的来说,"直接定址"与"解决冲突"是散列表的两大特点。

HashCode具有如下要求和期望

  • 必要的

    1. 相等的对象具有相等的HashCode,如果 a == b ,则 hash(a) == hash(b)
    2. 在对象的生存期内,对象的HashCode不应发生改变
    3. hash函数不应引发异常
  • 追求的

    1. HashCode应该在值域内均匀分布

    2. hash函数具有良好的性能

    3. 越相近的值,HashCode就应该越远离

      hash(1.00) = 1
      hash(1.01) = 23058430092136961
      hash(1.02) = 46116860184273921
      
    4. 使用者(甚至是攻击者)难以从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;
    }
}
posted @ 2022-10-28 11:23  Violeshnv  阅读(19)  评论(0编辑  收藏  举报