redis-intset

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/king_qg/article/details/85996203

intset

整数集合,是有序存储的,且不包含重复的元素。

结构

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;
  • encoding:contents中元素类型
  • length:contents中元素个数
  • contents:数据缓冲区,元素从小到大排列,无重复

intset只能处理三种整数类型的值,int16,int32和int64。三种数据类型对应了encoding字段的三个宏的值。

#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

现在看一下这个结构还是比较令人费解的,三种数据类型encoding字段使用一个字节即可存储。而且intset常用在数量比较少而且元素全部都是整数的时候存储,既然数量比较少,length居然用了uint32。难道是因为要考虑内存对齐吗,想想也不合理哦,如果两个数据类型都是uint8_t也会满足内存对齐啊,而且结构体大小会缩减到2。这一点是比较令人费解的地方。

源码

预备知识

对intset的三个字段都使用了大小端的宏来处理,所以源码中看到的intrevXXifbe的宏直接忽略即可,并不影响理解。

判断整数编码类型

static uint8_t _intsetValueEncoding(int64_t v)

返回值是应该使用的编码类型,形参是一个int64类型的变量,因为传参时无须判断使用类型,所以使用最大数据类型传参,在函数中判断到底是使用哪种数据类型去存储。

static uint8_t _intsetValueEncoding(int64_t v) {
    if (v < INT32_MIN || v > INT32_MAX)
        return INTSET_ENC_INT64;
    else if (v < INT16_MIN || v > INT16_MAX)
        return INTSET_ENC_INT32;
    else
        return INTSET_ENC_INT16;
}

函数实现很简单,就是判断数据类型的存储范围确定编码类型。因为是有符号类型,所以判断的时候要考虑的是正负两个边界。

根据类型获得pos位置的元素

这个函数是获取类函数中最根本的函数。

static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) {
    int64_t v64;
    int32_t v32;
    int16_t v16;

    if (enc == INTSET_ENC_INT64) {
        //根据计算得到元素地址,拷贝元素到目标变量中
        memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64));
        //按照设置的大小端格式,正确的变动数据的存储方式
        memrev64ifbe(&v64);
        return v64;
    } else if (enc == INTSET_ENC_INT32) {
        memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32));
        memrev32ifbe(&v32);
        return v32;
    } else {
        memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16));
        memrev16ifbe(&v16);
        return v16;
    }
}

根据传入的类型去获取元素,而不是根据encoding字段的值去获取。首先其它的获取函数可以复用这里的代码,其次这样比较灵活。

static int64_t _intsetGet(intset *is, int pos) {
    return _intsetGetEncoded(is,pos,intrev32ifbe(is->encoding));
}

这个函数就是根据encoding字段获取pos位置处的元素。复用了_intsetGetEncoded的代码。它的灵活性请往下看。

设置函数

static void _intsetSet(intset *is, int pos, int64_t value) {
    //获取编码类型
    uint32_t encoding = intrev32ifbe(is->encoding);

    if (encoding == INTSET_ENC_INT64) {
        //将缓冲区强制转换成编码类型然后设置pos位置的值为value
        ((int64_t*)is->contents)[pos] = value;
        memrev64ifbe(((int64_t*)is->contents)+pos);
    } else if (encoding == INTSET_ENC_INT32) {
        ((int32_t*)is->contents)[pos] = value;
        memrev32ifbe(((int32_t*)is->contents)+pos);
    } else {
        ((int16_t*)is->contents)[pos] = value;
        memrev16ifbe(((int16_t*)is->contents)+pos);
    }
}

查询函数

整数集合,既然是一个集合,必然不能出现重复的元素啊。所以添加前肯定需要先搜索一遍。

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;

    /* The value can never be found when the set is empty */
    if (intrev32ifbe(is->length) == 0) {
        //如果集合中没有元素,那么要查找的值绝对不会出现在集合里
        //pos设置为元素应该插入的位置
        if (pos) *pos = 0;
        return 0;
    } else {
        /* Check for the case where we know we cannot find the value,
         * but do know the insert position. */
         
         //因为整数集合是有序存储的,所以先判断两个特殊位置
         //如果待查找的值大于最大值或小于最小值,那么可以确定元素应该插入的位置
        if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
            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) {
        //while过后value等于查询的值,说明查询的值在整数集合中
        if (pos) *pos = mid;
        return 1;
    } else {
        //二分结束后没有搜索到,那么此时min位置就应该是插入的位置。
        if (pos) *pos = min;
        return 0;
    }
}

这个函数如果在golang中那么会使用多返回值,因为C中最多有一个返回值,所以传入一个指针,这个指针指向的变量也是返回值的作用。返回的0,1用来说明值是否出现在集合中,如果在的话,pos记录的是值在集合中的位置,如果不在的话,pos记录的是应该插入的位置。

升级函数

创建intset时,encoding字段初始化为INTSET_ENC_INT16,此时是最小的类型,每次添加操作,都先调用_intsetValueEncoding判断需要使用的编码类型,一旦新的类型比encoding大,那么intset会扩容,然后将encoding字段设置为新的类型,将原先的元素从后向前搬移到正确的位置,最后将新元素插入到缓冲区中。

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;
}

这个新元素添加的位置为什么是这样的规则,负数插入到头部,否则插入到尾部。因为此时是缓冲区需要升级的函数,说明原有的类型比升级的类型小,那么这个值要么是最小值,要么是最大值。

image
image

可见intset的编码类型不是一成不变的,可以说intset是动态变化类型的数组,不过这个时间复杂度是O(N)的,为什么这样设计呢?因为无法预料传入的值需要什么类型,同时这样设计最节省内存,比如都是比较小的证书时使用int16的数组类型,当一个数不能使用int16保存时,自动调整数据类型使其满足。

移动函数

static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {
    void *src, *dst;
    //需要移动的元素个数
    uint32_t bytes = intrev32ifbe(is->length)-from;
    uint32_t encoding = intrev32ifbe(is->encoding);

    if (encoding == INTSET_ENC_INT64) {
        src = (int64_t*)is->contents+from;
        dst = (int64_t*)is->contents+to;
        //需要拷贝的字节数
        bytes *= sizeof(int64_t);
    } else if (encoding == INTSET_ENC_INT32) {
        src = (int32_t*)is->contents+from;
        dst = (int32_t*)is->contents+to;
        bytes *= sizeof(int32_t);
    } else {
        src = (int16_t*)is->contents+from;
        dst = (int16_t*)is->contents+to;
        bytes *= sizeof(int16_t);
    }
    
    //重点是这个函数,就算src缓冲区和dst缓冲区有重叠,也会正确拷贝
    memmove(dst,src,bytes);
}

看完函数实现可以知道,这个函数是移动了一系列元素,而不是一个值。当时没有看到bytes的初始化,一直在这纠结,哈哈。

添加函数

这里的success也是当做返回值使用的。

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    if (success) *success = 1;

    /* Upgrade encoding if necessary. If we need to upgrade, we know that
     * this value should be either appended (if > 0) or prepended (if < 0),
     * because it lies outside the range of existing values. */
    if (valenc > intrev32ifbe(is->encoding)) {
        /* This always succeeds, so we don't need to curry *success. */
        //如果新的编码类型比原先大大,那么久升级intset
        //升级过后值位于缓冲区的0位置或legnth-1位置
        return intsetUpgradeAndAdd(is,value);
    } else {
        /* Abort if the value is already present in the set.
         * This call will populate "pos" with the right position to insert
         * the value when it cannot be found. */
        if (intsetSearch(is,value,&pos)) {
            //如果在intset搜索到要插入的值,那么不需要添加,直接退出
            if (success) *success = 0;
            return is;
        }

        //需要添加,首先扩容
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        
        //需要插入的位置不是最后,那么闲移动元素把需要插入的位置空出来
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }

    //设置元素并更新长度信息
    _intsetSet(is,pos,value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

移除函数

找到正确的位置,然后移动元素,并且把空间缩容就行。

intset *intsetRemove(intset *is, int64_t value, int *success) {
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    if (success) *success = 0;

    //搜索不到说明不在集合,不需要删除
    if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
    
        //进入if后pos已经保存了value的位置
    
        uint32_t len = intrev32ifbe(is->length);

        /* We know we can delete */
        if (success) *success = 1;

        /* Overwrite value with tail and update length */
        
        //将一系列元素前移直接覆盖需要删除的元素
        if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
        //缩容且更新长度
        is = intsetResize(is,len-1);
        is->length = intrev32ifbe(len-1);
    }
    return is;
}

总结

看过源码后,觉得intset就是一个可以自动向上调整数据类型的数组,没有向下调整的过程,同时要求是一个集合。

关于作者

大四学生一枚,分析数据结构,面试题,golang,C语言等知识。QQ交流群:521625004。微信公众号:后台技术栈。
image

猜你喜欢

转载自blog.csdn.net/king_qg/article/details/85996203