千家信息网

.NET中的HashSet原理是什么

发表于:2024-11-26 作者:千家信息网编辑
千家信息网最后更新 2024年11月26日,这篇文章主要介绍".NET中的HashSet原理是什么",在日常操作中,相信很多人在.NET中的HashSet原理是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答".
千家信息网最后更新 2024年11月26日.NET中的HashSet原理是什么

这篇文章主要介绍".NET中的HashSet原理是什么",在日常操作中,相信很多人在.NET中的HashSet原理是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答".NET中的HashSet原理是什么"的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

哈希表原理

HashSet是基于哈希表的原理实现的,学习HashSet首先要了解下哈希表。

哈希表(hash table, 也叫散列表)是根据key直接访问存储位置的数据结构,它通过一个键值的函数,将所需查询的数据映射到表中一个位置来访问,加快了查找速度。

上述函数即为哈希函数,哈希函数应尽量计算简单以提高插入、检索效率;计算得到的地址应尽量分布均匀,以降低哈希冲突;应具有较大的压缩性,以节省内存。常见的哈希函数构造方法有直接定址法、除留余数法、数字分析法等。HashSet采用除留余数法,将元素的HashCode除以某个常数(哈希表Size)的余数作为地址,常数通常选取一个素数。

两个相等的对象的哈希值相同,但两个不等的对象的哈希值是有可能相同的,这就是哈希冲突。处理冲突的方法有开放定址法、链表法、双散列法等。HashSet使用链表法,将冲突元素放在链表中。

哈希表是一种用于高性能集合操作的数据结构,它有如下特点:

  • 无序、不重复;插入、查找时间复杂度为O(1);

  • 不使用索引;

  • 容量不足时自动扩容,但扩容成本高;

  • 可提供很多高性能集合操作,如合并、裁剪等;

HashSet实现

HashSet内置了两个数组,如下。_buckets中存放由哈希函数计算得到的索引值,_buckets中的值从1开始,因此在使用时需要-1。该值即为_entries数组的相对索引,若未发生冲突,指向的值即为待查找元素的相对索引。如果发生了冲突,根据冲突链表也可以快速定位到元素。_entries存放的是Entry对象,Entry类型如下所示。HashCode为元素的哈希值,在查找、插入、删除、扩容等操作时都会用到。Value存储数据。Next在不同时刻有不同的作用,当Entry在列表中时,形成冲突链表,其Next指向冲突链表的下一元素,链表最后一个元素的Next值为-1;若Entry已被列表删除,形成空位链表,其Next指向空位链表的下一元素,空位链表的最后一个元素值为-2。

HashSet还有几个关键成员:_count、_freeList、_freeCount。_count表示添加元素数量,注意它并不是实际存储的元素数量,因为在删除元素时未更新它。_freeList为空位链表头,其值指向被删除的_entries索引,_entries[_freeList].Next指向下一空位的相对位置。_freeCount表示空位数量,列表实际存储的元素数量为_count - _freeCount。

private int[]? _buckets; // _entries索引数组private Entry[]? _entries; // 实体数组private int _count; // 实际存储的元素数量private int _freeList; // 空位链表头索引,初始值-1private int _freeCount; // 空位数量private struct Entry{    public int HashCode;    public int Next;    public T Value;}public int Count => _count - _freeCount; // _count不会记录被删除的元素,因此实际元素数量为_count - _freeCount

HashSet提供了多个构造函数重载,如果不传任何参数,不会初始化_buckets和_entries。当添元素时,会调用Initialize(0)。Initialize方法接受一个int参数,该参数表示需要初始化的列表容量。实际初始化的列表容量为大于等于该值的最小素数。取素数作为列表长度是因为该值作为使用除留余数法构造的哈希函数的除数,对素数求余结果分布更均匀,减少了冲突的发生。

private int Initialize(int capacity){    int size = HashHelpers.GetPrime(capacity); // 获取>=capacity的最小素数    var buckets = new int[size];    var entries = new Entry[size];    // ...    return size;}

查找元素时,首先调用元素的GetHashCode方法计算哈希值,然后调用GetBucketRef方法执行哈希函数运算,获得索引。GetBucketRef的返回值-1为真实索引i,若i为-1,则未找到元素。若i>=0,表示列表中存在与待查找元素哈希值相同的元素,但相等的哈希值并不一定表示元素相等,还要进一步判断HashCode,若HashCode相等,再判断元素是否相等,满足则查找到元素,返回_entries的索引i。

private int FindItemIndex(T item){    // ...    int hashCode = item != null ? item.GetHashCode() : 0;    if (typeof(T).IsValueType)    {        int i = GetBucketRef(hashCode) - 1; // _buckets元素从1开始        while (i >= 0) // 存在与item相同的哈希值,不一定存在item        {            ref Entry entry = ref entries[i];            if (entry.HashCode == hashCode && EqualityComparer.Default.Equals(entry.Value, item))            {                return i; // HashCode相等且元素相等,则查找到元素,返回_entries索引            }            i = entry.Next;            collisionCount++;            // ...        }    }    // ...    return -1;}private ref int GetBucketRef(int hashCode){    int[] buckets = _buckets!;    return ref buckets[(uint)hashCode % (uint)buckets.Length]; // 使用除留余数法构造哈希函数}

插入元素时,首先会查找待插入的元素是否存在,HashSet是不重复的,因此若插入元素已存在会直接返回false。若不存在元素,则会寻找存放元素的index。如果存在删除后的空位,则会将元素放到_freeList指向的空位上;如果不存在空位,则按_entries顺序插入元素。找到index后,即可将元素的HashCode及元素赋值到_entries[index]对应字段,当没有冲突时,Next值为-1;若存在冲突,则形成链表,将其添加到链表头,Next指向冲突的下一位置。

private bool AddIfNotPresent(T value, out int location){    bucket = ref GetBucketRef(hashCode); // bucket为ref int类型,若不存在冲突,bucket应为0,因为int默认值为0    // ...    int index;    if (_freeCount > 0) // 存在删除后的空位,将元素放在空位上    {        index = _freeList;        _freeCount--; // 更新删除后空位数量        _freeList = StartOfFreeList - entries[_freeList].Next; // 更新空位索引    }    else // 按_entries顺序存储元素    {        int count = _count;        if (count == entries.Length) // 容量不足,扩容        {            Resize();            bucket = ref GetBucketRef(hashCode);        }        index = count;        _count = count + 1;        entries = _entries;    }    {   // 赋值        ref Entry entry = ref entries![index];        entry.HashCode = hashCode;        entry.Next = bucket - 1; // 若不存在冲突则为-1,否则形成链表,指向冲突的下一元素索引        entry.Value = value;        bucket = index + 1; // 此处对bucket赋值,即改变_buckets对应元素,即将以插入元素哈希值为索引的_buckets值指向存放插入元素的_entries的索引+1        _version++;        location = index;    }    // ...    return true;}

插入时若列表容量不足,会调用Resize方法进行扩容。扩容后的大小为大于等于原大小2倍的最小素数。获取待扩容的大小后,以新大小重新分配entries内存,并调用Array.Copy方法将原内容拷贝到新位置。由于列表长度变了,因此哈希值会变,因此需要更新_buckets的内容(_entries索引),同理entry.Next的值也要更新。

private void Resize() => Resize(HashHelpers.ExpandPrime(_count), forceNewHashCodes: false);public static int ExpandPrime(int oldSize){    int num = 2 * oldSize;    if ((uint)num > 2146435069u && 2146435069 > oldSize)    {        return 2146435069;    }    return GetPrime(num); // 返回原大小2倍的最小素数}private void Resize(int newSize, bool forceNewHashCodes){    var entries = new Entry[newSize];    Array.Copy(_entries, entries, count);    // ...    _buckets = new int[newSize];    for (int i = 0; i < count; i++)    {        ref Entry entry = ref entries[i];        if (entry.Next >= -1) // 更新_buckets内容        {            ref int bucket = ref GetBucketRef(entry.HashCode); // 获取以新大小作为除数的哈希函数运算得到的哈希值            entry.Next = bucket - 1; // 更新Next            bucket = i + 1; // 更新_buckets的元素,指向重新计算的_entry索引+1        }    }    _entries = entries;}

当删除元素时,首先查找待删除元素是否存在。若哈希值存在冲突,会记录冲突链表的上一索引。查找到元素后,需要更新冲突链表的指针。删除元素后,会更新_freeCount空位数量,并将删除元素索引赋值给_freeList,记录删除空位,添加到空位链表头,其Next指向下一空位的相对位置。插入元素时,会将元素插入到_freeList记录的空位索引处,并根据该空位的Next更新_freeList的值。

public bool Remove(T item){    int last = -1;    int hashCode = item != null ? (_comparer?.GetHashCode(item) ?? item.GetHashCode()) : 0;    ref int bucket = ref GetBucketRef(hashCode);    int i = bucket - 1;    while (i >= 0)    {        ref Entry entry = ref entries[i];        if (entry.HashCode == hashCode && (_comparer?.Equals(entry.Value, item) ?? EqualityComparer.Default.Equals(entry.Value, item)))        {            // 找到待删除元素            if (last < 0) // 待删除元素位于链表头部,更新_buckets元素值指向链表下一位置            {                bucket = entry.Next + 1;            }            else // 待删除元素非链表头,需更新链表上一元素Next值                entries[last].Next = entry.Next;            entry.Next = StartOfFreeList - _freeList; // 空位链表,记录下一空位索引相对位置,插入时根据该值更新_freeList            if (RuntimeHelpers.IsReferenceOrContainsReferences())                entry.Value = default!;            _freeList = i; // 记录删除元素位置,下次插入元素时,会插入在此            _freeCount++;  // 更新删除后空位数量            return true;        }        last = i; // 存在冲突,记录链表上一位置        i = entry.Next;    }    return false;}

到此,关于".NET中的HashSet原理是什么"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!

元素 哈希 空位 索引 冲突 更新 指向 函数 位置 数量 方法 素数 原理 大小 存储 余数 实际 容量 表头 学习 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 江西警察学院网络安全与执法 国家推动的网络技术 提高网络安全意识是关键 网络安全工程师李华飚 福建app软件开发需要多少钱 网络管理与网络安全试题 委托软件开发如何入账 莆田市巨讯网络技术有限公司 南京创网网络技术有限公司介绍 魔兽世界的服务器是什么规格 易语言导出资源表数据库 军事理论什么是网络安全 长沙软件开发培训学校怎么样 魔兽怎么在维护的时候切换服务器 计算机网络技术毕业论文范畴 通州区网络技术咨询 企业核心交换机如何提高网络安全 关于工业网络技术 金山wps云文档无法连接服务器 数据库检索最基本技术 网络安全常用法规资料大全 服务器安全狗 vps 大连华信软件开发管培生面试 宁夏党建设计软件开发公司 网络安全答辩题 互联网工作科技含量高吗 网络安全发展对策 中信建投软件开发职位怎么样 南京创网网络技术有限公司介绍 供电公司网络安全自查报告
0