整数集合
在Redis
中如果一个集合(set
)类型中的元素都是整数,并且元素个数不多的话,Redis
会使用整数集合(intset
)来存储这个集合的元素。
整数集合的查找的时间复杂度为
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)、插入和删除的时间复杂度都是
O
(
n
)
O(n)
O(n)。因为在存储元素相对较少的时候
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)和
O
(
n
)
O(n)
O(n)差距并不大,但intset
相比红黑树和哈希表来说,可以大大减少内存。并且intset
是内存连续的,具备很好的空间局部性,更适合现代操作系统的流水线工作,缓存命中率也会更高。
定义
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
encoding
:编码方式,其实就是inset
中存储的单个元素所占字节数,这里可以为INTSET_ENC_INT16
、INTSET_ENC_INT32
、INTSET_ENC_INT64
。length
:intset
中元素的个数。contents
:存储元素的数组。虽然类型为int8_t
,但实际上存储的是int16_t
、int32_t
、int64_t
类型的元素。
操作
intset
相关操作比较多,这里只介绍一些常用的操作。
创建
创建一个空的intset
,默认编码方式为INTSET_ENC_INT16
。
intset *intsetNew(void) {
intset *is = zmalloc(sizeof(intset)); // 分配内存
is->encoding = intrev32ifbe(INTSET_ENC_INT16); // 设置编码方式 -- 默认为int16_t
is->length = 0; // 设置元素个数为0
return is;
}
编码升级
intset
原始的编码方式可能是INTSET_ENC_INT16
,但是当添加一个元素值大于INT16_MAX
时,intset
会将编码方式升级为INTSET_ENC_INT32
,同时原有元素的编码方式也需同步发生转变,保证intset
中所有的元素都使用统一的编码方式(因为在intset
中,获取元素都是通过拷贝一块内存获取[contents
起始地址+(位置*编码长度), 编码长度])。
static intset *intsetResize(intset *is, uint32_t len) {
uint64_t size = (uint64_t)len*intrev32ifbe(is->encoding); // 计算新的内存大小,不含头部大小
assert(size <= SIZE_MAX - sizeof(intset)); // 防止溢出
// 重新分配内存, zrealloc会将元素数据拷贝到新的内存中
is = zrealloc(is,sizeof(intset)+size);
return is;
}
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intrev32ifbe(is->encoding); // 当前编码类型
uint8_t newenc = _intsetValueEncoding(value); // 通过要添加的值确定新的编码类型
int length = intrev32ifbe(is->length); // 当前数据数据个数
// 因为value的加入导致intset需要升级编码
// 如果value是整数,说明value比原有数据的最大值还要大
// 如果value是负数,说明value比原有数据的最小值还要小
int prepend = value < 0 ? 1 : 0; // 如果是整数就加到尾部,如果是负数就加到头部
is->encoding = intrev32ifbe(newenc); // 设置新的编码
// 重新分配内存后,原来is指向的内存已经被free,因此需要重新赋值
is = intsetResize(is,intrev32ifbe(is->length)+1);
// 从尾部开始将数据逐一迁移到新的位置
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
// 将新的值添加到指定位置
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
如下图向已经包含已经包含两个元素的编码为INTSET_ENC_INT16
的intset
(上图)中插入一个大于INT16_MAX
的元素过程
- 重新开辟内存,并将元素数据拷贝到新的内存中
- 更新原有元素的编码方式和内存位置
- 给新元素所在内存赋值
- 更新
intset
元素个数
查找元素
在intset
中查找元素用的是二分查找,时间复杂度为
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)。查找过程大致如下:
- 原始是数据是否为空
- 为空说明没有找到,返回0,并且记录插入位置为0(即头部)
- 要查找的数据是否大于最大值
- 大于最大值说明没有找到,返回0,并且记录插入位置为
intset
中元素个数(即尾部)
- 大于最大值说明没有找到,返回0,并且记录插入位置为
- 要插入的值是否小于最小值
- 小于最小值说明没有找到,返回0,并且记录插入位置为0(即头部)
- 二分查找
- 如果找到,返回1,并且记录插入位置为找到的位置
- 如果没有找到,返回0。根据二分查找最后一次比较,因此插入位置为
mid
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
// min max 其实是下表index
int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
int64_t cur = -1;
if (intrev32ifbe(is->length) == 0) { // 判断是否为空
if (pos) *pos = 0;
return 0;
} else {
// 因为在intset中元素是生序排列的。因此可以通过比较最大值和最小值来判断是否存在
if (value > _intsetGet(is,max)) { // 大于最大值
if (pos) *pos = intrev32ifbe(is->length);
return 0;
} else if (value < _intsetGet(is,0)) { // 小于最小值
if (pos) *pos = 0;
return 0;
}
}
// 二分查找
while(max >= min) {
mid = ((unsigned int)min + (unsigned int)max) >> 1; // 对除法进行的优化
cur = _intsetGet(is,mid);
if (value > cur) {
min = mid+1;
} else if (value < cur) {
max = mid-1;
} else {
break;
}
}
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
添加元素
在了解编码升级和查找之后,添加元素就简单了。
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
uint8_t valenc = _intsetValueEncoding(value); // 获取新元素的编码格式
uint32_t pos;
if (success) *success = 1;
if (valenc > intrev32ifbe(is->encoding)) { // 编码格式大于当前intset编码格式,需要升级编码
return intsetUpgradeAndAdd(is,value);
} else {
if (intsetSearch(is,value,&pos)) { // 查找元素,如果不存将pos设置为要插入的位置
if (success) *success = 0;
return is;
}
is = intsetResize(is,intrev32ifbe(is->length)+1); // 重新分配空间
// 将pos后面的数据往后移动一位
if (pos < intrev32ifbe(is->length))
intsetMoveTail(is,pos,pos+1); // 将pos后面的数据整体移动一次,只有一次内存操作
}
_intsetSet(is,pos,value); // 设置值
is->length = intrev32ifbe(intrev32ifbe(is->length)+1); // 更新元素个数
return is;
}
删除元素
删除元素的过程和添加元素的过程类似,只是删除元素时需要将后面的元素往前移动一位。值得注意的是intset并不会做编码降级处理
intset *intsetRemove(intset *is, int64_t value, int *success) {
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
if (success) *success = 0;
// 如果这个数的编码大于inset的编码,说明这个数大于inset中最大的元素,肯定不存在
if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
uint32_t len = intrev32ifbe(is->length);
if (success) *success = 1;
if (pos < (len-1))
intsetMoveTail(is,pos+1,pos); // 如果删除的数不是放在尾部,那么就需要移动数据
is = intsetResize(is,len-1); // 重新分配内存,这里是压缩内存
is->length = intrev32ifbe(len-1); // 更新元素个数
}
return is;
}