一.intset特性
1.仅储存整型(根本要求)
2.内部升序排序
3.三种编码节省空间(16/ 32 /64bits)
4.自动升级降级
5.集合最基本的特性:唯一性
二.intset定义
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
class intset {
public:
uint32_t encoding;
uint32_t length;
int8_t contents[];
}
为了最大化的利用有限的空间,intset具有三种整型编码方式,分别使用16bits(-32768~32767), 32bits(-2147483648~2147483647),64bits(~)。可见,每种编码方式都是8bits的整数倍, 所以在intset类中,我们使用int8_t类型的柔性数组content[]作为我们的实际存储空间,容易知道,接下来我们对intset的所有操作其实是用过编码位数和对内存的直接操作。其中length指的是当前intset包含的数据个数。
三.intset的基本操作
任何数据结构几乎都无法避免增删改查,一个良好的存储数据结构必须能够承受海量的数据,以及拥有更快的操作。而我们所讲的intset也无非是增删改查。只不过是对内存和操作速度优化后的“数组”罢了。
intset* intsetNew(void);//创建一个空集合
uint8_t intsetGetAndSetV(intset* is, uint32_t pos, int64_t* value);//获取并传值
intset* intsetAdd(intset* is, int64_t value, uint8_t* success);//添加元素
intset* intsetRemove(intset* is, int64_t value, int* success);//删元素
uint8_t intsetFind(intset* is, int64_t value);//看某个元素是否在集合中
int64_t intsetRandom(intset* is);//随机取数
int64_t intsetGet(intset* is, int pos);//取索引为pos的值
uint32_t intsetLen(intset* is);//集合元素个数
size_t intsetBlobLen(intset* is);//返回内存字节数
1.查
首先是最基本的“查”操作,涉及到一下几个函数,是后面很多操作的基石
int64_t intset::intsetGet(intset* is, int pos);//外部api
int64_t intset::intsetGetEncoded(intset* is, int pos, uint8_t enc);//仅内部
uint8_t intset::intsetFind(intset* is, int64_t value);//外部api
uint8_t intset::intsetSearch(intset* is, int64_t value, uint32_t* pos);//仅内部
还记得我们前面所说吗,intset的绝大多数操作实际上是对于内存空间的直接操作,其中intsetGetEncoded函数就是通过巧妙的类型转换和内存拷贝而获得,代码如下:
int64_t intset::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;
}
}
注:memrev32ifbe等类似函数负责端序转换,视操作系统而定
其中最为重要的是intsetSearch函数,找到返回1,没找到返回0,同时会更新pos的值,找到时是pos是目标值的索引,没找到时是目标值应该插入的位置。已知内部数据按升序排序,所以只需要通过二分查找即可。
//查找,搜索
//成功查找,则设置pos,返回1.
//找不到,pos为插入位置
uint8_t intset::intsetSearch(intset* is, int64_t value, uint32_t* pos)
{
int min = 0, max = intrev32ifbe(is->length) - 1, mid = -1;
int64_t cur = -1;
// 处理 is 为空时的情况
if (intrev32ifbe(is->length) == 0) {
if (pos) *pos = 0;
return 0;
}
else {
//数组有序,
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 = (min + max) / 2;
cur = intsetGet(is, mid);
if (value > cur) {
min = mid + 1;
}
else if (value < cur) {
max = mid - 1;
}
else {
break;
}
}
// 检查是否已经找到了 value
if (value == cur) {
if (pos) *pos = mid;
return 1;
}
else {
if (pos) *pos = min;
return 0;
}
}
注: intrev32ifbe是端序转换函数,视操作系统而定
2.改
集合中的改其实并不真正存在,实际是search and delete, 然后add,虽然我们支持对于指定位置pos的获取,但并不直接支持改操作。读者有兴趣的话可以自行尝试实现。
3.删
删除其实就是利用前面我们所介绍的intsetSearch函数,获取到目标位置后,通过内存移动,对要进行删除操作的数据进行覆盖,然后重新分配空间。内存移动我们会放在最后一部分来讲。
intset* intset::intsetRemove(intset* is, int64_t value, int* success = NULL)
{
// 计算 value 的编码方式
uint8_t valenc = intsetValueEncoding(value);
uint32_t pos;
// 默认设置标识值为删除失败
if (success) *success = 0;
// 当 value 的编码大小小于或等于集合的当前编码方式(说明 value 有可能存在于集合)
// 并且 intsetSearch 的结果为真,那么执行删除
if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is, value, &pos)) {
// 取出集合当前的元素数量
uint32_t len = intrev32ifbe(is->length);
// 设置标识值为删除成功
if (success) *success = 1;
if (pos < (len - 1)) intsetPartialMoveTail(is, pos + 1, pos);
// 缩小数组的大小,移除被删除元素占用的空间
is = intsetResize(is, len - 1);
// 更新集合的元素数量
is->length = intrev32ifbe(len - 1);
}
return is;
}
4.增
intset* intset::intsetAdd(intset* is, int64_t value, uint8_t* success = NULL)
intset添加数据需要经历一下几步操作:
升级并添加
第一步判断value的编码是关键的,因为当value的编码大于当前intset的编码时,意味着所需要添加的新值要么是最大值,要么是最小值,换句话说不是头插就是尾插。所以我们需要做的事也很明确,首先判断是头插还是尾插,然后升级编码(重新分配内存,并将原有的数据按照顺序重新设置),新值。代码如下:
升级并添加情况
intset* 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;
// 更新集合的编码方式
is->encoding = intrev32ifbe(newenc);
// 根据新编码对集合(的底层数组)进行空间调整
is = intsetResize(is, intrev32ifbe(is->length) + 1);
while (length--)//从后往前移动
intsetSet(is, length + prepend, intsetGetEncoded(is, length, curenc));
// 设置新值,根据 prepend 的值来决定是添加到数组头还是数组尾
if (prepend)
intsetSet(is, 0, value);
else
intsetSet(is, intrev32ifbe(is->length), value);
// 更新整数集合的元素数量
is->length = intrev32ifbe(intrev32ifbe(is->length) + 1);
return is;
}
完整的添加函数
//添加,success参数默认为NULL(测试使用)
intset* intset::intsetAdd(intset* is, int64_t value, uint8_t* success = NULL)
{
// 计算编码 value 所需的长度
uint8_t valenc = intsetValueEncoding(value);
uint32_t pos;//插入位置
// 默认设置插入为成功
if (success) *success = 1;
// 如果 value 的编码比整数集合现在的编码要大
// 那么表示 value 必然可以添加到整数集合中,需要升级
if (valenc > intrev32ifbe(is->encoding))
return intsetUpgradeAndAdd(is, value);
else {
if (intsetSearch(is, value, &pos)) {
//已经存在于集合中
if (success) *success = 0;
return is;
}
//重新分配空间(扩容)
is = intsetResize(is, intrev32ifbe(is->length) + 1);
//如不是尾插,移动后缀字节
if (pos < intrev32ifbe(is->length))
intsetPartialMoveTail(is, pos, pos + 1);
}
// 将新值设置到底层数组的指定位置中
intsetSet(is, pos, value);
// 更新集合长度
is->length = intrev32ifbe(intrev32ifbe(is->length) + 1);
// 返回添加新元素后的整数集合
return is;
}
5.尾部空间移动(非升级增和删时使用)
向前或先后移动指定索引范围内的数组元素,函数名中的 MoveTail 其实是一个有误导性的名字,
这个函数可以向前或向后移动元素,而不仅仅是向后在添加新元素到数组时,就需要进行向后移动,
* 如果数组表示如下(?表示一个未设置新值的空间):
* | x | y | z | ? |
* |<----->|
* 而新元素 n 的 pos 为 1 ,那么数组将移动 y 和 z 两个元素
* | x | y | y | z |
* |<----->|
* 接着就可以将新元素 n 设置到 pos 上了:
* | x | n | y | z |
* 当从数组中删除元素时,就需要进行向前移动,
* 如果数组表示如下,并且 b 为要删除的目标:
* | a | b | c | d |
* |<----->|
* 那么程序就会移动 b 后的所有元素向前一个元素的位置,
* 从而覆盖 b 的数据:
* | a | c | d | d |
* |<----->|
* 最后,程序再从数组末尾删除一个元素的空间:
* | a | c | d |
* 这样就完成了删除操作。代码如下:
//移动后缀子字节,用于编码为改变时删除或添加
void intset::intsetPartialMoveTail(intset* is, uint32_t from, uint32_t to)
{
void* src, * dst;
// 要移动的元素个数
uint32_t bytes = intrev32ifbe(is->length) - from;
// 集合的编码方式
uint32_t encoding = intrev32ifbe(is->encoding);
// 根据不同的编码
// src = (Enc_t*)is->contents+from 记录移动开始的位置
// dst = (Enc_t*)is_.contents+to 记录移动结束的位置
// bytes *= sizeof(Enc_t) 计算一共要移动多少字节
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);
}
// 进行移动
// T = O(N)
memmove(dst, src, bytes);
}
四.结语
通过对内存空间和c/c++特性的极致应用,体会到intset设计的精妙了吗。关注我,持续更新redis底层原理。