数据结构-集合
集合作为redis中的一种常用的数据结构,根据其集合内的元素特点,底层采用数组
和哈希表
两种方式进行实现。集合初始化的时候,会先根据的第一个元素是否能够转化成整形,然后决定使用数组还是哈希表来实现。哈希表是通用的实现方式,数组只是针对所有的元素都是整数的情况。在一定的情况下,会发生转换,转换是单向的,只能从数组转到到哈希表。触发的条件有两个,在数组实现的基础上,一是往集合内加入非整数元素,二是集合的大小超过了限制(默认是512,可以通过参数set-max-intset-entries
修改)。这是因为数组的实现的集合,在每次新增、删除操作的时候,都会重新去申请内存,当涉及到的内存比较大的时候,效率会有所降低。
概述
在集合的实现里,重点是在集合操作上,求交集、并集、差集、随机删除以及随机获取。根据集合的数组组成不同,不同的算法带来的效率是不一样的。
交集
求交集的时候,会先对所有集合按照其元素数量大小进行升序排序,以第一个集合(元素数量最小的)为基准,依次便利第一个集合内的每一个元素,查看在其他集合中是否都存在
并集
求并集的时候,先建立一个新的空集合,依次把每个集合的每个元素放入新集合
差集
求差集的时候,是有一个基准,即求第一个集合和其他集合的差集。根据集合的元素数量组成,有两种算法。
- 依次遍历基准集合的每一个元素,判断是否在其他集合中都不存在。算法的时间复杂度为O(N*M),N为基准集合元素的数量,M为所有集合的数量
- 复制一个基准集合,然后依次遍历其余集合的每一个元素,从复制的基准集合中删除该元素。算法的时间复杂度为O(N),N为所有集合的元素数量和
第一种下,要求从第二个集合开始按照其元素数量大小进行降序排序,因为数量大的集合出现相同元素的概率更大,避免后续的对比
随机删除
随机删除操作,根据集合元素的数量和要删除数量大小的关系,有三种情况
- 要删除的数量大于等于集合的元素数量:直接删除整个集合
- 要删除的数量小于集合的元素数量,且占集合元素数量比例小:随机选择要删除的元素
- 要删除的数量小于集合的元素数量,且占集合元素数量比例大:随机选择要留下来的元素,反向操作是,因为哈希表在使用率很低的时候,随机获取数据的成本很高
随机获取
随机获取的操作,根据数据允不允许多次出现以及集合元素的数量和要获取数量大小的关系,分为四种情况
- 允许重复出现:每次都随机获取一个,直到数量满足
- 不允许重复出现,且要获取的数量大于等于集合的元素数量:直接返回整个集合
- 不允许重复出现,要获取的数量小于集合的元素数量,且占集合元素数量比例小:每随机获取一个元素后,把该元素从集合内删除,直到数量满足
- 不允许重复出现,要获取的数量小于集合的元素数量,且占集合元素数量比例大:每次都随机获取一个,如果重复忽略,直到数量满足
源码分析
集合实现(t_set.c)
setTypeCreate
根据数据能否转换成整数,返回空集合(数组实现/哈希实现)
1 | robj *setTypeCreate(sds value) { |
setTypeAdd
新增元素
1 | int setTypeAdd(robj *subject, sds value) { |
setTypeRemove
删除元素
1 | int setTypeRemove(robj *setobj, sds value) { |
setTypeIsMember
查找元素
1 | int setTypeIsMember(robj *subject, sds value) { |
setTypeInitIterator
获取迭代器
1 | setTypeIterator *setTypeInitIterator(robj *subject) { |
setTypeReleaseIterator
释放迭代器
1 | void setTypeReleaseIterator(setTypeIterator *si) { |
setTypeNext
通过迭代器获取下一个元素的内容,返回实现类型(哈希表/数组),根据返回类型数据存放在sdsele
或者llele
1 | int setTypeNext(setTypeIterator *si, sds *sdsele, int64_t *llele) { |
setTypeNextObject
通过迭代器获取下一个元素,返回字符串
1 | sds setTypeNextObject(setTypeIterator *si) { |
setTypeRandomElement
通过迭代器随机获取一个元素的内容,返回实现类型(哈希表/数组),根据返回类型数据存放在sdsele
或者llele
1 | int setTypeRandomElement(robj *setobj, sds *sdsele, int64_t *llele) { |
setTypeSize
获取集合大小
1 | unsigned long setTypeSize(const robj *subject) { |
setTypeConvert
转换集合底层实现方式,只能从数组到哈希表
1 | void setTypeConvert(robj *setobj, int enc) { |
saddCommand
响应sadd命令,添加数据
1 | void saddCommand(client *c) { |
sremCommand
响应srem命令,删除元素
1 | void sremCommand(client *c) { |
smoveCommand
响应smove命令,把一个元素从一个集合移动到另一个集合
1 | void smoveCommand(client *c) { |
sismemberCommand
响应sismember命令,确定元素是否在集合中
1 | void sismemberCommand(client *c) { |
scardCommand
响应scard命令,获取集合大小
1 | void scardCommand(client *c) { |
spopWithCountCommand
随机删除一个或多个元素
1 |
|
spopCommand
响应spop命令,随机返回并删除一个或多个元素
1 | void spopCommand(client *c) { |
srandmemberWithCountCommand
随机获取一个或多个元素,当参数为负数时,表示返回的结果内,同一元素可以重复出现
1 |
|
srandmemberCommand
响应srandmember命令,随机获取一个或多个元素
1 | void srandmemberCommand(client *c) { |
qsortCompareSetsByCardinality
集合快速排序比较方法,根据集合大小
1 | int qsortCompareSetsByCardinality(const void *s1, const void *s2) { |
qsortCompareSetsByRevCardinality
集合快速排序比较方法,根据集合大小倒序排,用于计算集合差集时用
1 | int qsortCompareSetsByRevCardinality(const void *s1, const void *s2) { |
sinterGenericCommand
计算集合交集通用方法
1 | void sinterGenericCommand(client *c, robj **setkeys, |
sinterCommand
响应sinter命令,获取集合的交集
1 | void sinterCommand(client *c) { |
sinterstoreCommand
响应sinterstore命令,获取集合的交集,并把结果存起来
1 | void sinterstoreCommand(client *c) { |
sunionDiffGenericCommand
获取集合并集和差集,通用操作
1 |
|
sunionCommand
响应sunion命令,获取集合的并集
1 | void sunionCommand(client *c) { |
sunionstoreCommand
响应sunionstore命令,获取集合的并集,并把结果存储到新的key中
1 | void sunionstoreCommand(client *c) { |
sdiffCommand
响应sdiff命令,获取集合的差集
1 | void sdiffCommand(client *c) { |
sdiffstoreCommand
响应sdiffstore命令,获取集合的差集
1 | void sdiffstoreCommand(client *c) { |
sscanCommand
响应sscan命令,迭代获取集合内的元素
1 | void sscanCommand(client *c) { |
数组实现
数据结构
数组实现集合的在inset.h
和intset.c
文件中。数据结构很简单,只有三个属性,且集合的每一次新增、删除都是通过remalloc
实现,不涉及其他的更高级用法。且在新增的元素的时候,数组始终是从小到大有序的。查询的时候采用二分查找法。
1 | typedef struct intset { |
编码类型
用来标识集合内元素的最大值的范围
1 |
_intsetValueEncoding
获取一个整数的编码类型
1 | static uint8_t _intsetValueEncoding(int64_t v) { |
_intsetGetEncoded
获取集合内具体索引位置的数值,需要指定编码类型
1 | static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) { |
_intsetGet
获取集合内具体索引位置的数值,采用集合的自己的编码类型
1 | static int64_t _intsetGet(intset *is, int pos) { |
_intsetSet
写入集合内具体索引位置的值
1 | static void _intsetSet(intset *is, int pos, int64_t value) { |
intsetNew
创建新的集合
1 | intset *intsetNew(void) { |
intsetResize
调整集合的大小,在新增和删除元素的时候会用到
1 | static intset *intsetResize(intset *is, uint32_t len) { |
intsetSearch
二分查找数据,返回值表示查询结果1找到 0没找到,pos表示值在数组中的索引,如果没有找到的话,pos表示需要再此位置进行插入
1 | static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) { |
intsetUpgradeAndAdd
升级编码类型并插入数据,用于新插入数据编码类型大于集合的编码类型的情况
1 | static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { |
intsetMoveTail
移动数据,新增的时候调用此方法,给新数据腾位置
1 | static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) { |
intsetAdd
插入新元素,success表示成功与否
1 | intset *intsetAdd(intset *is, int64_t value, uint8_t *success) { |
intsetRemove
删除元素
1 | intset *intsetRemove(intset *is, int64_t value, int *success) { |
intsetFind
查找元素
1 | uint8_t intsetFind(intset *is, int64_t value) { |
intsetRandom
随机获取元素
1 | int64_t intsetRandom(intset *is) { |
intsetGet
获取指定位置的数据
1 | uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value) { |
intsetLen
获取集合大小
1 | uint32_t intsetLen(const intset *is) { |
intsetBlobLen
获取集合所用的内容大小
1 | size_t intsetBlobLen(intset *is) { |
哈希表实现
参照哈希表文章