跟着大彬读源码 - Redis 10 - 对象编码之整数集合

整数集合是 Redis 集合键的底层实现之一。当一个集合只包含整数值元素,而且元素数量很少时,Redis 就会使用整数集合做为集合键的底层实现。数组

1 整数集合的实现

整数集合是 Redis 用于保存整数值的集合抽象数据结构。它能够保存类型为 int16_t、int32_t、int64_t 的整数值,而且保证集合中不会出现重复元素。数据结构

每一个 intset.h/intset 结构表示一个整数集合:ui

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;
  • encding:编码方式
  • length:集合包含的元素数量
  • contents[]:保存元素的数组

contents 数组是整数集合的底层实现:整数集合的每一个元素都是 contents 数组的一个数组项,各个项在数组中按值的大小从小到大有序排列,而且数组中不包含重复项。编码

length 属性记录了整数集合记录的元素数量,也就是 contents 数组的长度。spa

虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组,但实际上 contents 数组并不保存任何 int8_t 类型的值,contents 数组的真正类型取决于 encoding 属性的值,好比:若是 encoding 属性的值为 INTSET_ENC_INT16,那么 contents 就是一个 int16_t 类型的数组,数组里的每一个项都是一个 int16_t 类型的整数值,取值范围为:[-32768-32767](2^(16-1))。code

与之相似,encoding 的值为 INTSET_ENC_INT32,那么数组每项的取值范围为:[-2147483648, 2147483647](2^(32-1)。排序

这里也引起了一个问题,当咱们对一个 encoding 为 INTSET_ENC_INT8 的 intset,插入 129 时(int8_t 的取值范围是 [-128, 127]),会出现什么?索引

这也就引起了 intset 的升级操做。与之对应,也有降级操做。接下来,咱们来详细认识下 intset 的升降级操做。内存

2 升级操做

每当咱们要将一个新元素添加到整数集合时,若是新元素的类型比整数集合的 encoding 类型大,整数集合就须要先进行升级操做(upgrade),而后才能将新元素添加到整数集合中。

整个升级操做源码以下:

// intset.c/intsetUpgradeAndAdd()
/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    uint8_t curenc = intrev32ifbe(is->encoding);
    uint8_t newenc = _intsetValueEncoding(value);
    int length = intrev32ifbe(is->length);
    int prepend = value < 0 ? 1 : 0;

    /* First set new encoding and resize */
    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is,intrev32ifbe(is->length)+1);

    /* Upgrade back-to-front so we don't overwrite values.
     * Note that the "prepend" variable is used to make sure we have an empty
     * space at either the beginning or the end of the intset. */
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    /* Set the value at the beginning or the end. */
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

升级整数集合并添加新元素,共分为三步进行:

  1. 扩展底层数组大小。根据新元素的类型,扩展整数集合底层数组的大小,并为新元素分配空间。
  2. 元素转换,并保持原有顺序。将底层数组现有的全部元素,都转换成与新元素相同的类型,并将转换后的元素放在正确的位置上,保证原有顺序不发生改变。
  3. 将新元素添加到底层数组中。

此外,一旦因插入新元素引起升级操做,就说明新插入的元素比集合中现有的全部元素的长度大,因此这个新元素的值要么大于全部现有元素(正值),要么就小于全部现有元素(负值),那么:

  • 在新元素小于全部现有元素时,新元素就会被放在底层数组的最开头的位置,即索引为 0 的位置;
  • 在新元素大于全部现有元素时,新元素就会被放在底层数组的最末尾的位置;

3 升级优点

整数集合的升级策略主要有如下两个好处:

  1. 提示整数集合的灵活性;
  2. 尽量的节约内存;

3.1 提示灵活性

由于 C 语言是静态类型语言,为了不类型错误,咱们一般不会将两种不一样类型的值放在同一个数据结构中。

可是,由于有了升级操做,整数集合能够经过它来自适应新元素,因此咱们能够随意地将 int16_t、int32_t、和 int64_t 类型的整数添加到集合中,而没必要担忧出现类型错误,大大的提高了整数集合的灵活性。

3.2 节约内存

固然,要让一个数组能够同时保存 int16_t、int32_t、和 int64_t 类型的整数值,咱们能够粗暴的直接使用 int64_t 类型的数组做为整数集合的底层实现,来保存不一样类型的值。可是,这样一来,即便添加到集合中的都是 int16_t、int32_t 类型的值,数组也都是须要使用 int64_t 类型的空间去保存,出现浪费内存的状况。

而整数集合的升级操做,既能同时保存三种不一样类型的值,又能够确保升级操做只会在有须要的时候进行,达到节省内存的目的。

4 交、并、差集算法

Redis 中的集合实现了交、并、差等操做,相关操做可参加 t_set.c,其中
sinterGenericCommand() 实现交集,sunionDiffGenericCommand() 实现并集和差集。

它们都能同时对多个集合进行元素。当对多个集合进行差集运算时,会先计算出第一个和第二个集合的差值,而后再与第三个集合作差集,依次类推。

接下来,咱们一块儿来认识下三个操做的实现思路。

4.1 交集

计算交集的过程大概能够分为三部分:

  1. 检查各个集合,对于不存在的集合当作空集来处理。一旦出现空集,则不用继续计算了,最终的交集就是空集。
  2. 对各个集合按照元素个数由少到多进行排序。这个排序有利于后面计算的时候从最小的集合开始,须要处理的元素个数较少。
  3. 对排序后第一个集合(也就是最小集合)进行遍历,对于它的每个元素,依次在后面的全部集合中进行查找。只有在全部集合中都能找到的元素,才加入到最后的结果集合中。

须要注意的是,上述第 3 步在集合中进行查找,对于 intset 和 dict 的存储来讲时间复杂度分别是 O(log n) 和 O(1)。但因为只有小集合才使用 intset,因此能够粗略地认为 intset 的查找也是常数时间复杂度的。

4.2 并集

并集操做最简单,只要遍历全部集合,将每个元素都添加到最后的结果集中便可。向集合中添加元素会自动去重,因此插入的时候无需检测元素是否已存在。

4.3 差集

计算差集有两种可能的算法,它们的时间复杂度有所区别。

第一种算法

对第一个集合进行遍历,对于它的每个元素,依次在后面的全部集合中进行查找。只有在全部集合中都找不到的元素,才加入到最后的结果集合中。

这种算法的时间复杂度为O(N*M),其中N是第一个集合的元素个数,M是集合数目。

第二种算法

  1. 将第一个集合的全部元素都加入到一个中间集合中。
  2. 遍历后面全部的集合,对于碰到的每个元素,从中间集合中删掉它。
  3. 最后中间集合剩下的元素就构成了差集。
  4. 这种算法的时间复杂度为O(N),其中N是全部集合的元素个数总和。

在计算差集的开始部分,会先分别估算一下两种算法预期的时间复杂度,而后选择复杂度低的算法来进行运算。还有两点须要注意:

  • 在必定程度上优先选择第一种算法,由于它涉及到的操做比较少,只用添加,而第二种算法要先添加再删除。
  • 若是选择了第一种算法,那么在执行该算法以前,Redis的实现中对于第二个集合以后的全部集合,按照元素个数由多到少进行了排序。这个排序有利于以更大的几率查找到元素,从而更快地结束查找。

5 总结

  1. 整数集合是集合键的底层实现之一。
  2. 整数集合以有序、无重复的方式保存集合元素。在有须要时,会根据新添加元素的类型,改变底层数组的类型。
  3. 升级操做提高了操做的灵活性,并尽量的节约了内存。
  4. 集合能够进行交、并、差集操做。
相关文章
相关标签/搜索