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;
}
这个新元素添加的位置为什么是这样的规则,负数插入到头部,否则插入到尾部。因为此时是缓冲区需要升级的函数,说明原有的类型比升级的类型小,那么这个值要么是最小值,要么是最大值。
可见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。微信公众号:后台技术栈。