热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

.NET中的HashSet原理是什么

这篇文章主要介绍“.NET中的HashSet原理是什么”,在日常操作中,相信很多人在.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; // 空位链表头索引,初始值-1
private 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原理是什么”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程笔记网站,小编会继续努力为大家带来更多实用的文章!


推荐阅读
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 本文介绍了Oracle数据库中tnsnames.ora文件的作用和配置方法。tnsnames.ora文件在数据库启动过程中会被读取,用于解析LOCAL_LISTENER,并且与侦听无关。文章还提供了配置LOCAL_LISTENER和1522端口的示例,并展示了listener.ora文件的内容。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 推荐系统遇上深度学习(十七)详解推荐系统中的常用评测指标
    原创:石晓文小小挖掘机2018-06-18笔者是一个痴迷于挖掘数据中的价值的学习人,希望在平日的工作学习中,挖掘数据的价值, ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 展开全部下面的代码是创建一个立方体Thisexamplescreatesanddisplaysasimplebox.#Thefirstlineloadstheinit_disp ... [详细]
author-avatar
皇家突然回家_390
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有