数据结构 -- 整数集合(Intset)

整数集合

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_INT16INTSET_ENC_INT32INTSET_ENC_INT64
  • lengthintset中元素的个数。
  • contents:存储元素的数组。虽然类型为int8_t,但实际上存储的是int16_tint32_tint64_t类型的元素。

Intset

操作

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_INT16intset(上图)中插入一个大于INT16_MAX的元素过程

  • 重新开辟内存,并将元素数据拷贝到新的内存中
  • 更新原有元素的编码方式和内存位置
  • 给新元素所在内存赋值
  • 更新intset元素个数

在这里插入图片描述

查找元素

intset中查找元素用的是二分查找,时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)。查找过程大致如下:

  • 原始是数据是否为空
    • 为空说明没有找到,返回0,并且记录插入位置为0(即头部)
  • 要查找的数据是否大于最大值
    • 大于最大值说明没有找到,返回0,并且记录插入位置为intset中元素个数(即尾部)
  • 要插入的值是否小于最小值
    • 小于最小值说明没有找到,返回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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虎小黑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值