【2023】HashMap详细源码分析解读

前言

在弄清楚HashMap之前先介绍一下使用到的数据结构,在jdk1.8之后HashMap中为了优化效率加入了红黑树这种数据结构。

在计算机科学中,(英语:tree)是一种抽象数据类型(ADT)或是实作这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

  • 每个节点都只有有限个子节点或无子节点;
  • 没有父节点的节点称为根节点;
  • 每一个非根节点有且只有一个父节点;
  • 除了根节点外,每个子节点可以分为多个不相交的子树;
  • 树里面没有环路(cycle)
    在这里插入图片描述

在分类上包含了二叉树、二叉查找树、红黑树、B树、B+树等。

1、二叉树

  • 每个节点最多含有两个子节点,分别为左子节点和右子节点。
  • 不要求每个节点都有两个子节点,有的只有左,有的只有右子节点
  • 二叉树每个节点的左子树和右子树也分别满足前两条定义
    在这里插入图片描述

2、二叉搜索树

  • 在树种的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值而右子树节点的值都大于这个节点的值
  • 不会出现键值相等的节点
  • 通常情况下二叉树搜索的时间复杂度为O(log n)
    在这里插入图片描述
    因为他不会自旋所以会出现一种最坏的情况,左右子树极度不平衡,
    在这里插入图片描述

3、红黑树

  • 节点要么是红色,要么是黑色
  • 跟节点是黑色
  • 叶子节点都是黑色的空节点
  • 红黑树的红色节点的子节点都是黑色
  • 从任意节点到叶子节点的所以路径都包含相同数目的黑色节点
  • 在进行添加或者删除后,如果不满足上面的五条定义则会发生旋转调整操作
  • 查找、删除、添加的操作时间复杂度都是O(log n)
    在这里插入图片描述

散列表

散列表又名hash表,是根据键(key)直接访问在内存存储位置值(value)的数据结构,它是由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性

将键(key)映射为数组下标的函数叫做散列函数。可以表示为:hashValue = hash(key)
散列函数的基本要求:

  • 散列函数计算得到的散列值必须是大于等于0的正整数,因为hashValue需要作为数组的下标。
  • 如果key1 = key2,那么经过hash后得到的哈希值也必相同即:hash(key1) = hash(key2)
  • 如果key1 != key2,那么经过hash后得到的哈希值也必相同即:hash(key1) != hash(key2)

散列冲突

实际的情况下想找一个散列函数能够做到对于不同的key计算得到的散列值都不同几乎是不可能的。从而就会造成一种现象,就是多个key通过hash运算转换之后映射到同一个数组下标位置,这种情况被称之为散列冲突(或者哈希冲突、哈希碰撞)
在这里插入图片描述

拉链法

为了解决散列冲突,一般都会采用一种叫拉链法的方式解决。
在散列表中,数组的每个下标位置我们可以称之为桶,每个桶会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中,这种方式就叫拉链法

  • 插入操作,通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可,插入的时间复杂度是O(1)
  • 当查找、删除一个元素时,我们同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除
    • 平均情况下基于链表法解决冲突时查询的时间复杂度是O(1)
    • 散列表可能会退化为链表,查询的时间复杂度就从O(1)退化为O(n)
    • 将链表法中的链表改造为其他高效的动态数据结构,比如红黑树,查询的时间复杂度是O(log n)
      在这里插入图片描述
      在这里插入图片描述

而且使用红黑树可以有效的防止DDos 攻击

一、简介

HashMap是Map中的重要实现类,它是一个散列表,存储的内容是键值对(key=>value)映射。HashMap是非线程安全的。HashMap中允许存储null的键和值,键是唯一的。

在JDK1.8以前,HashMap的底层数据结构是纯粹的数组+链表结构。由于数组具有读取快,增删慢的特点,而链表具有读取慢,增删快的特点,HashMap将二者相结合,并且没有使用同步锁进行修饰,它的性能较好。数组是HashMap的主体,链表则是为了解决哈希冲突而引入。此处解决哈希冲突的具体方法为:拉链法

二、源码解析

1、put方法

1.1、常见属性

扩容阈值 = 数组容量 * 加载因子

    //默认的初始容量static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  //默认的加载因子     static final float DEFAULT_LOAD_FACTOR = 0.75f;//存储数据的数组transient Node<K,V>[] table;//容量transient int size;    

1.2、构造函数

    //默认无参构造public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // 指定加载因子为默认加载因子 0.75}
  • HashMap是懒惰创建数组的,在创建对象的时候并没有初始化数组
  • 在无参的构造函数中,设置了默认的加载因子

1.3、put方法

  • 流程图
    在这里插入图片描述
  • 具体源码
    public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}/** *  计算hash值的方法*/static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}/** *  具体执行put添加方法*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//判断数组是否初始化(数组初始化是在第一次put的时候)if ((tab = table) == null || (n = tab.length) == 0)//如果未初始化,调用resize()进行初始化n = (tab = resize()).length;//通过 & 运算符计算求出该数据(key)的数组下标并且判断该下标位置是否有数据if ((p = tab[i = (n - 1) & hash]) == null)//如果没有,直接将数据放在该下标位置tab[i] = newNode(hash, key, value, null);else {  //该下标位置有数据的情况Node<K,V> e; K k;//判断该下标位置的数据是否和当前新put的数据一样if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//如果一样,则直接覆盖valuee = p;//判断是不是红黑树else if (p instanceof TreeNode)  //如果是红黑树的话,进行红黑树的具体添加操作e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//如果都不是代表是链表else {  //遍历链表for (int binCount = 0; ; ++binCount) {//判断next节点是否为null,是null代表遍历到链表尾部了if ((e = p.next) == null) {//把新值插入到尾部p.next = newNode(hash, key, value, null);//插入数据后,判断链表长度有大于等于8了没if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//如果是则进行红黑树转换treeifyBin(tab, hash);break; //退出}//如果在链表中找到相同数据的值,则进行修改操作if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;//把下一个节点赋值为当前节点p = e;}}//判断e是否为null(e值为前面修改操作存放原数据的变量)if (e != null) { // existing mapping for key//不为null的话证明是修改操作,取出老值V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)//把新值赋值给当前节点e.value = value;afterNodeAccess(e);//返回老值return oldValue;}}//计算当前节点的修改次数++modCount;//判断当前数组中的数据量是否大于扩容阈值if (++size > threshold)//进行扩容resize();afterNodeInsertion(evict);return null;}
  • 具体流程
  1. 判断键值对数组table是否为ull,复杂执行resize()进行扩容(初始化)
  2. 根据键值对key计算hash值得到数组索引
  3. 判断table[i]==hash值得到数组索引
  4. 如果taale[i] ==null ,条件成立,直接新建节点添加
    i. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
    ii. 判断table[i]是否为treeNode,即table[i]是否为红黑树,如果红黑树,则直接在数中插入键值对
    iii. 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于的话把链表转换为红黑树操作,遍历过程中若发现key已经存在执行覆盖value

1.4 resize方法(扩容)

  • 流程图
    在这里插入图片描述
  • 具体源码
    final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;//如果当前数组为null的时候,把oldCap 老数组容量设置为0int oldCap = (oldTab == null) ? 0 : oldTab.length;//老的扩容阈值int oldThr = threshold;int newCap, newThr = 0;//判断数组容量是否大于0,大于0说明数组已经初始化if (oldCap > 0) {//判断当前数组长度是否大于最大数组长度if (oldCap >= MAXIMUM_CAPACITY) {//如果是,将扩容阈值直接设置为int类型的最大数值并且直接返回threshold = Integer.MAX_VALUE;return oldTab;}//如果在最大长度访问内,则需要扩容oldCap << 1 == oldCap * 2//并且判断是否大于16,else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold  等价于 oldCap * 2}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;//数组初始化的情况,将阈值和扩容因子设置为默认值else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//初始化容量小于16的时候,扩容阈值没用阈值的if (newThr == 0) {//创建阈值float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}//计算出来的阈值赋值threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})//根据上边计算得出的容量 创建新的数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;//扩容操作,判断不为null证明不是初始化数组if (oldTab != null) {//	遍历数组for (int j = 0; j < oldCap; ++j) {Node<K,V> e;//判断当前下标为j的数组如果不为null的话赋值给eif ((e = oldTab[j]) != null) {//将数组的位置设置为nulloldTab[j] = null;//判断是否有下一个节点if (e.next == null)//如果没有,就查询计算在新数组中的下标并放进去newTab[e.hash & (newCap - 1)] = e;//有下个节点的情况,并且判断是否已经树化else if (e instanceof TreeNode)//进行红黑树的操作((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//有下个节点的情况,并且判还没有树化  else { // preserve orderNode<K,V> loHead = null, loTail = null;  //低位数组Node<K,V> hiHead = null, hiTail = null;  //高位数组Node<K,V> next;遍历循环do {//取出next节点next = e.next;//通过 & 操作计算出结果为0if ((e.hash & oldCap) == 0) {//如果低位为null,则把e值放入低位2头if (loTail == null)loHead = e;//低位尾不是null,else//将数据放入next节点loTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);//低位如果记录的有数据,是链表if (loTail != null) {//将下一个元素置空loTail.next = null;//将低位头放入新数组的newTab[j] = loHead;}//高位尾如果记录有数据,是链表if (hiTail != null) {//将下个元素置空hiTail.next = null;//将高位头放入新数组的(原下标+原数组容量)位置newTab[j + oldCap] = hiHead;}}}}}return newTab;}
  • 执行原理
  1. 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度*0.75)
  2. 每次扩容的时候,都是扩容之前容量的2倍;
  3. 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
    i.没有hash冲突的节点,则直接使用e.hash&(newCap-1)计算新数组的索引位置
    ii.如果是红黑树,走红黑树的添加
    iii.如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash&oldCap)是否为0,该元素的位詈要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

扩容时候怎么重新确定元素在数组中的位置,我们看到是由 if ((e.hash & oldCap) == 0) 确定的。

hash HEX(97)  = 0110 0001‬ 
n    HEX(16)  = 0001 0000
--------------------------结果  = 0000 0000
# e.hash & oldCap = 0 计算得到位置还是扩容前位置hash HEX(17)  = 0001 0001‬ n    HEX(16)  = 0001 0000
--------------------------结果  = 0001 0000
#  e.hash & oldCap != 0 计算得到位置是扩容前位置+扩容前容量

get方法

public V get(Object key) {// 定义一个Node结点Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 数组中元素相等的情况if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;// bucket中不止一个结点if ((e = first.next) != null) {//判断是否为TreeNode树结点if (first instanceof TreeNode)//通过树的方法获取结点return ((TreeNode<K,V>)first).getTreeNode(hash, key);do {//通过链表遍历获取结点if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}// 找不到就返回nullreturn null;
}

常见问题

1、索引如何计算?hashCode都有了,为何还要使用hash()方法?数组容量为何是2的n次幂?

  • 先计算key的hashCode(),再进行调用Hash()方法使用异或的方式扰动运算进行二次哈希,最后再通过(n - 1) & hash 与运算得到索引。(使用该方式相当于hash % n取模运算)

  • 二次hash()是为了综合二进制的高位数据,让哈希分布更加均匀,减少哈希碰撞的概率,计算公式:(h = key.hashCode()) ^ (h >>> 16)

  • 计算索引是,如果是2的n次幂可以使用位与运算代替取模,效率更高;而且再扩容时hash & lodCap == 0 的元素留在原来位置,为1的则到到扩容后的新位置,新位置=旧位置+lodCap

    • 计算方式hash & length 使用二次hash值和原始容量做运算,如果结果是0则位置不变,如果不是0则移动到新的位置,
    • 新的位置计算方式:原始数组容量+原始下标=新的位置
  • 使用2的n次幂主要也是为了可以更好的配合优化效率,使得下标分布得更加的均匀

2、HashMap的put方法流程,1.7和1.8有何不同?

  1. HashMap是懒惰创建数组的,首次使用才创建数组
  2. 计算索引(桶下标)
    1. 首先得到key的hash值,在经过一次hash()方法计算出二次hash的值,**计算方式为:(h = key.hashCode()) ^ (h >>> 16)**把hash值通过无符号右移,然后再和原本的hash值进行异或计算,这样的作用主要是为了打乱真正参与计算的低16位,可以有效的做到扰乱运算的效果,减少了哈希碰撞的概率
    2. 然后再拿整个二次hash的值和数组的容量进行除留余数法,取得的余数就是最终的桶下标,计算方式为:(n-1)&hash 把数组长度-1再通过和hash值进行与运算。相当于使用hash值去和数组的长度n做取余% % 运算,使用&做运算主要也是为了可以有效的提高运算的效率(1.7没有该优化)
  3. 如果该桶下标还没人占用,则创建Node节点返回
  4. 如果该桶下标已经被占用:则会去逐个的和各个节点进行比较看hash值和equals()是否都相对,如果都相等,代表是同一个key,则进行覆盖修改,如果不相同则添加
    1. 当已经TreeNode走红黑树的添加或更新逻辑
    2. 如果是普通的Node走链表的添加或更新逻辑,如果链表长度超过树化阈值8时,走树化逻辑,执行树化操作(前提条件是数组长度达到64)
  5. 返回前还会检查容量是否超过了扩容的阈值(数组长度/加载因子),一但超过则会进行扩容
  6. 不同:
    1. 链表插入时,1.7使用的是头插法(从链表头部插入),1.8使用的是尾插法(从链表尾部插入)
      1. 1.7是大于等于阈值且没有空位才扩容(如果有空位则不会扩容而是继续放在计算出来的桶下标的位置上),而1.8是大于等于阈值就扩容
      2. 1.8在扩容计算Node节点时,会优化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/7912.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【洛谷】P4414 [COCI2006-2007#2] ABC

[COCI2006-2007#2] ABC 题面翻译 【题目描述】 三个整数分别为 A , B , C A,B,C A,B,C。这三个数字不会按照这样的顺序给你&#xff0c;但它们始终满足条件&#xff1a; A < B < C A < B < C A<B<C。为了看起来更加简洁明了&#xff0c;我们希望你可以按…

MySQL学习笔记 ------ 分组查询

#进阶5&#xff1a;分组查询 /* 语法&#xff1a; select 分组函数&#xff0c;列&#xff08;要求出现在group by的后面&#xff09; from 表 【where 筛选条件】 group by 分组的列表 【order by 排序的字段】; 注意&#xff1a;查询列表必须特殊&#xff0c;要求是分组函…

ChatGPT在智能电子设备中的应用如何?

ChatGPT在智能电子设备中有着广泛的应用潜力&#xff0c;可以为电子设备提供更智能、更个性化的用户体验&#xff0c;并为用户提供更多便利和高效的功能和服务。智能电子设备是指通过集成计算机、传感器、网络和人工智能等技术&#xff0c;实现智能化的功能和交互的设备。ChatG…

【C#】Lock关键字

一、概述 Lock关键字&#xff0c;确保当一个线程位于代码的临界区时&#xff0c;另一个线程不进入临界区。如果其他线程试图进入锁定的代码&#xff0c;则它将一直等待&#xff08;即被阻止&#xff09;&#xff0c;直到该对象被释放。 Lock关键字属于语法糖&#xff0c;其本…

数据结构【栈和队列】

第三章 栈与队列 一、栈 1.定义&#xff1a;只允许一端进行插入和删除的线性表&#xff0c;结构与手枪的弹夹差不多&#xff0c;可以作为实现递归函数&#xff08;调用和返回都是后进先出&#xff09;调用的一种数据结构&#xff1b; 栈顶&#xff1a;允许插入删除的那端&…

网络知识点之-BGP协议

边界网关协议&#xff08;BGP&#xff09;是运行于 TCP 上的一种自治系统的路由协议。 BGP 是唯一一个用来处理像因特网大小的网络的协议&#xff0c;也是唯一能够妥善处理好不相关路由域间的多路连接的协议。 BGP 构建在 EGP 的经验之上。 BGP 系统的主要功能是和其他的 BGP 系…

How to Use your mac to Read a Word and Repeat it more times

Using the say Command on a Mac 在 Mac 上使用 say 命令 The say command is a fun and useful feature on Mac computers that allows you to convert text to speech using the command line. With this command, you can make your Mac speak anything you type after it…

特征选择策略:为检测乳腺癌生物标志物寻找新出口

内容一览&#xff1a;microRNA&#xff08;小分子核糖核酸&#xff09;是一类短小的单链非编码 RNA 转录体。这些分子在多种恶性肿瘤中呈现失控性生长&#xff0c;因此近年来被诸多研究确定为确诊癌症的可靠的生物标志物 (biomarker)。在多种病理分析中&#xff0c;差异表达分析…

vue3下的uniapp跨域踩坑

uniapp vue3 H5跨域踩坑 开发移动端H5的时候由于后端接口没有做跨域处理&#xff0c;因此需要做下服务器代理&#xff0c;于是百度搜索了uniapp下h5的跨域配置 在manifest下的h5配置proxy&#xff0c;大概是这样: "h5": {"devServer": {"https"…

安全—01day

文章目录 1. 编码1.1 ASCLL编码1.2 URL编码1.3 Unicode编码1.4 HTML编码1.5 Base64编码 2. form表单2.1 php接收form表单2.2 python接收form表单 1. 编码 1.1 ASCLL编码 ASCII 是基于拉丁字母的一套电脑编码系统&#xff0c;主要用于显示现代英语和其他西欧语言。它是最通用的…

对话天壤创始人薛贵荣:AIGC正在成为新的“水煤电”

AIGC正在悄无声息地成为各行各业的必需品。 数科星球原创 作者丨苑晶 编辑丨大兔 国内的大模型混战半年有余&#xff0c;传统互联网巨头和人工智能公司纷纷入场。在“百模大战”的关键时刻&#xff0c;行业悄然发生分化。一些更具前瞻性的企业开始眺望远方&#xff0c;准备打…

ajax/axios访问后端测试方法

文章目录 1、浏览器执行javascript方法GET请求POST请求 2、Postman测试工具GET请求POST请求 3、idea IDE提供的httpclient4、Apache JMeter 1、浏览器执行javascript方法 GET请求 http://localhost:6060/admin/get/123 POST请求 技巧&#xff1a;打开谷歌浏览器&#xff0c…

zookeeper学习(二) 集群模式安装

前置环境 三台centos7服务器 192.168.2.201 192.168.2.202 192.168.2.150三台服务器都需要安装jdk1.8以上zookeeper安装包 安装jdk 在单机模式已经描述过&#xff0c;这里略过&#xff0c;有需要可以去看单机模式中的这部分&#xff0c;注意的是三台服务器都需要安装 安装…

C数据结构与算法——队列 应用(C语言纯享版 迷宫)

实验任务 (1) 掌握顺序循环队列及其C语言的表示&#xff1b; (2) 掌握入队、出队等基本算法的实现&#xff1b; (3) 掌握顺序循环队列的基本应用&#xff08;求解迷宫通路&#xff09;。 实验内容 使用C语言实现顺序循环队列的类型定义与算法函数&#xff1b;编写main()函数…

算法与数据结构(三)--栈

一.栈的基本概念 栈是一种特殊的表&#xff0c;这种表只在表首进行插入和删除操作。 因此&#xff0c;表首对于栈来说具有特殊的意义&#xff0c;称为栈顶。相应的&#xff0c;表尾称为栈底。不含任何元素的栈称为空栈。 栈的修改遵循后进先出的原则&#xff0c;Last In First…

Java前后端交互long类型溢出的解决方案

问题描述&#xff1a; 前端根据id发起请求查找对象的时候一直返回找不到对象&#xff0c;然后查看了请求报文&#xff0c;发现前端传给后台的数据id不对&#xff0c;原本的id是1435421253099634623&#xff0c;可前端传过来的id是 1435421253099634700&#xff0c;后三位变成了…

Zabbix邮件报警(163网易邮箱)

目录 一、电脑登录网易邮箱配置 二、Server端安装配置邮件服务器 邮箱查看 三、编辑zabbix_server.conf 引用邮件脚本 查看邮件 五、配置zabbix web监控项邮件报警 操作思路 Server.zabbix.com web操作 确认报警媒介信息 配置zabbix中的用户所使用的报警媒介类型以及接收邮…

【网络】HTTPS协议

目录 一、概念 1、HTTPS 2、加密解密 3、加密的必要性 4、常见的加密方式 4.1、对称加密 4.2、非对称加密 5、数据摘要 && 数据指纹 6、数字签名 二、HTTPS的工作过程 1、只使用对称加密 2、只使用非对称加密 3、双方都使用非对称加密 4、非对称加密 对…

rust gtk 桌面应用 demo

《精通Rust》里介绍了 GTK框架的开发&#xff0c;这篇博客记录并扩展一下。rust 可以用于桌面应用开发&#xff0c;我还挺惊讶的&#xff0c;大学的时候也有学习过 VC&#xff0c;对桌面编程一直都很感兴趣&#xff0c;而且一直有一种妄念&#xff0c;总觉得自己能开发一款很好…

深入学习 Redis - 深挖经典数据类型之 set

目录 前言 一、Set 类型 1.1、操作命令 sadd / smembers&#xff08;添加&#xff09; sismember&#xff08;判断存在&#xff09; scard&#xff08;获取元素个数&#xff09; spop&#xff08;删除元素&#xff09; smove&#xff08;移动&#xff09; srem&#x…