Redis数据结构系列二:IntSet

Redis 基础数据结构

redis基础数据结构:动态字符串SDS、intSet、Dict、ZipList、QuickList、SkipList、RedisObject。

数据结构——IntSet

IntSet是redis集合的一种实现方式,基于整数数组实现。具备长度可变、有序等特征。

typedef struct intset {
    uint32_t encoding; // 编码方式,支持存放16位,32位和64位的整数
    uint32_t length; // 元素个数
    int8_t contents[]; //整数数组,保持集合数据。有序。该元素仅是有符号8bit, 所有是保持指向数组的第一个元素。
} intset;
  • uint32_t encoding
    编码方式,支持存放16位,32位和64位的整数。编码如下:
    /*INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
    #define INTSET_ENC_INT16 (sizeof(int16_t))//类似short类型,2字节
    #define INTSET_ENC_INT32 (sizeof(int32_t))//类似int类型,4字节
    #define INTSET_ENC_INT64 (sizeof(int64_t))//类似long类型,8字节
    
  • uint32_t length
    元素个数
  • int8_t contents[]
    整数数组,保存集合数据。有序
    该元素仅是有符号8bit, 所以是保存指向数组的第一个元素

在这里插入图片描述

IntSet 升级

若,目前intset A = {1,3,2}, 采用的是INTSET_ENC_INT16编码。 插入一个新数据60000,则超出有符号16bit,需要升级。

在这里插入图片描述
以上述流程来说: 添加60000,这个数字超出int16_t的范围,intset会自动升级到合适的大小。上图则是扩容前后数组的状态
步骤如下:

  1. 升级编码为INTSET_ENC_INT32,每个整数占4个字节,并按照新的编码方式及元素个数扩容数组

  2. 倒序依次将数组中的元素拷贝到正确位置。(由于编码升级,占用字节由小变大,若正序向后拷贝,如intset A[0]=1所占2字节,扩容成4字节,会将原先A[1]=10的数据覆盖,所以,需要倒序拷贝)

  3. 将新增的元素放入数组首或尾(因为是有符号的,负数大在首,正数大在尾)

  4. 最后,将编码encoding改为INTSET_ENC_INT32, length改为4

IntSet 添加元素——源码分析

1、首先先查看定义于intset.h中intset *intsetAdd(intset *is, int64_t value, uint8_t *success);

/* Insert an integer in the intset */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    // 要插入元素的编码
    uint8_t valenc = _intsetValueEncoding(value);
    // 要插入的位置
    uint32_t pos;
    if (success) *success = 1;

    // 1、如果新增数据的编码> 原有encoding,需要升级后添加数据
    if (valenc > intrev32ifbe(is->encoding)) {
        /* This always succeeds, so we don't need to curry *success. */
        return intsetUpgradeAndAdd(is,value);
    } else {
        // 2、如果不需要升级,先二分查找数据是否存在,若不存在,并返回需要插入数据的pos位置
        // 2.1 若数据存在,则返回false
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }
        // 2.2 若数据不存在,重新申请intset内存为length+1
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        // 3、若pos位置小于intset大小,则将pos位置的元素后移一位
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    // 4、将value插入指定pos位置
    _intsetSet(is,pos,value);
    // 5、将intset的length设置为length+1
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

新增数据步骤如下:

  1. 首先获取新数据的编码: _intsetValueEncoding(value)
  2. 如果新增数据的编码 大于原有encoding,需要升级intset后添加数据
  3. 如果不需要升级,先二分查找数据是否存在,若不存在,并返回需要插入数据的pos位置
  4. 将新增数据value插入指定pos位置
  5. 将intset的length设置为length+1

2、紧接着进入升级intset方法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;
    // 1、设置新的编码
    is->encoding = intrev32ifbe(newenc);
    // 2、重新申请数组内存大小, 调整后的大小= (length+1) * encoding, 加1是添加新数据的内存
    is = intsetResize(is,intrev32ifbe(is->length)+1);

    // 3、倒序(从后向前)升级,以至于不会覆盖原有数据
    while(length--) // 首先将length--;改为和下标保持一致
        //3.1 在指定length+prepend位置,插入新元素。即:倒序将原先数组的每一个元素copy到正确的位置
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    //4、若value<0,负数,则在数组头部添加;若value>0,整数,则在数组尾部添加
    //(因为都需要升级编码,那么新增数据的绝对值一定大于原先数组的任意元素,所以,一定是首或尾插入,只需判断正负数即可判断插入位置)
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    // 5、长度改为length+1
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

升级intset步骤如下:

  1. 设置新的编码
  2. 重新申请数组内存大小, 调整后的大小= (length+1) * encoding, 加1是添加新数据的内存
  3. 倒序(从后向前)升级,以至于不会覆盖原有数据
  4. 若新增数据value<0,则在数组头部添加;若value>0,则在数组尾部添加
  5. intset长度改为length+1

3、最后进入查找数据方法intsetSearch——插入时同时保证intset有序和唯一

static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
    int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
    int64_t cur = -1;
    // 1、如果intset为空,返回pos=0
    if (intrev32ifbe(is->length) == 0) {
        if (pos) *pos = 0;
        return 0;
    } else {
        // 2、如果value>max,返回pos=length
        if (value > _intsetGet(is,max)) {
            if (pos) *pos = intrev32ifbe(is->length);
            return 0;
        // 3、如果value<min,返回pos=0
        } else if (value < _intsetGet(is,0)) {
            if (pos) *pos = 0;
            return 0;
        }
    }
    // 4、二分查找value应该在的位置pos
    while(max >= min) {
        // >> 1 = 右移1位,等于/2,即二分查找
        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;
        }
    }
    // 5、若value == cur, 则代表数据存在,返回1
    if (value == cur) {
        if (pos) *pos = mid;
        return 1;
    } else {
        // 5、若value != cur, 则代表数据不存在,返回pos=min
        if (pos) *pos = min;
        return 0;
    }
}

上述方法是典型的二分查找,笔者在此就不做赘述。

注意: intsetSearch是保证intset有序和唯一的逻辑

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值