整数集合

[TOC]

简介

整数集合,从名称即可得知是用来存放整数的集合,它相较于其他集合的区别在于:

  • 自动判断数据类型
  • 压缩内存占用
  • 保存的整数是有序的

因为存放的始终只有整数,所以在存储数据时,会判断该数据的大小,并使用最小的尺寸来存放对应的数据,以达到节省内存的目的。 如集合 Array = [ 1, 2, 3 ] 可以只使用 8 位的 char 来保存。

简而言之即是,集合中的元素大小,以集合中最大元素的大小为准。

PS: 其实不大想写这个,觉得很无意义。

实现

intset 的定义

intset 的定义很简单,只有三个字段


// encoding 的类型定义
#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 用来表示该结构的编码,也就是上文说的元素大小,编码决定了 intset 中每个元素的大小,最后一个 contents 是一个变长的数组,根据保存数据的总数进行扩充,而第二个 length 则表示了 contents 的长度。

创建一个 intset

创建一个空的 intset 用于保存将要加入的整数

intset *intsetNew(void) {
    intset *is = zmalloc(sizeof(intset));
    is->encoding = intrev32ifbe(INTSET_ENC_INT16);
    is->length = 0;
    return is;
}

上面那一段唯一令人迷惑的,就只有 intrev32ifbe(INTSET_ENC_INT16), 从源码可以了解到,这里其实只是为了将所有数字转换为 little endian。一系列的函数如 memrev32ifbeintrev32ifbe 等都是 revert 32 bit if big endian 的意思。

#if (BYTE_ORDER == LITTLE_ENDIAN)
#define memrev16ifbe(p)
...
#define intrev16ifbe(v) (v)
...
#else
#define memrev16ifbe(p) memrev16(p)
...
#endif

添加一个整数

整数集合内部是有序的,所以每次添加都要为其保持集合的顺序,所以在添加的时候遵循以下步骤:

  • 判断新的数字是否超过了当前编码,是的话则先扩充
  • 判断新的数字是否已存在当前集合,是的话直接返回,
    • 具体的查找方式是使用二分法
  • 否则查找新的数字应该存在集合中的那个位置
  • 然后插入对应的位置,并移动集合中受影响的元素的位置
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)) {
        return intsetUpdradeAndAdd(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))
            intsetMoveTail(is, pos, pos+1);
    }

    // 在指定的 pos 填充新整数
    _intsetSet(is, pos, value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

确定添加整数的编码

大致的步骤跟上面总结的一致,接下来应该往细节看了。 获取编码的方式,直接使用对应类型的最大最小值来确定编码。

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

判断新整数是否已存在

然后是查找对应的整数是否存在集合中,判断的逻辑为:

  • 先检查集合是否为空(废话。。。)
  • 然后检查是否在最大值跟最小值的区间外,满足任一个条件都说明新整数不在集合中。
  • 否则则使用二分查找法在集合中查找对应的元素。
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;

    // 如果集合为空,则新整数可以作为集合的第一个元素存在
    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 = ((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;
    }
}

获取指定索引的值

上面在做检查的时候需要获取指定索引的值来进行比较,以判断新的整数是否已存在集合中

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

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;
    }
    ele {
        memcpy(&v16, ((int16_t *)is->contents) + pos, sizeof(v16));
        memrev16ifbe(&v16);
        return v16;
    }
}

上面还有一些 memrev32ifbe 之类的调用,就不一一细说了,具体做的就是根据系统平台的定义,把所有 little endian 的转换为 big endian 的表示,具体的做法就是交换内存位置,如 abc -> cba

调整 intset 的大小

在确定需要把新的整数加入集合后, intset 即调整现有的集合大小,以便容纳新的元素,具体的操作都在 intsetResize 中。

static intset *inisetResize(intset *s, uint32_t len) {
    uint32_t size = len * intrev32ifbde(is->encoding);
    is = zrealloc(is, sizeof(intset) + size);
    return is;
}

重排位置

调整完大小后,则根据新元素应该所处的位置,挪动其他元素对应的位置,如要在一个长度为 10 的集合中的 2 这个位置插入新元素,则需要把原本的 2-10 的元素移到 3-11, 然后才能往 2 中插入新元素。

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

    memmove(dst, src, bytes);
}

插入新元素

终于到了最后一步了,经过上面那么多的准备之后,现在有了空间来让我们放新元素了,也确定了新的整数应该放到哪个位置,同时也把相关的内存空间都挪好了,最后一步就是把元素放到对应的位置。

static void _intsetSet(intset *is, int pos, int63_t value) {
    uint32_t encoding intrev32ifbe(is->encoding);

    if (encoding == INTSET_ENC_INT64) {
        ((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);
    }
}

升级 intset

在插入新数据的时候我们已经见到了这行代码 intsetUpgradeAndAdd,在新的整数大小超过了现有编码时,自动对当前的数据进行扩容,调整存放在集合中的所有元素,将其全部转换为新元素对应的编码。

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    // 获取新的跟旧的元素编码
    uint8_t curenc = intrev32ifbe(s->encoding);
    uint8_t newenc = _intsetValueEncoding(value);
    int length = intrev32ifbe(is->length);

    //
    // 进入扩充的情况只有:
    // 1. value 只能比当前集合中编码的最大值大
    // 2. value 只能比当前集合中编码的最小值小
    // value < 0 说明他是因为第 2 种情况需要扩充,所以添加到最前
    // value > 9 说明他是因为第 1 种情况需要扩充,所以添加到最末
    int prepend = value < 0 ? 1 : 0;

    // 重新分配空间
    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is, intrev32ifbe(is->length) + 1);

    // 使用旧编码从末尾开始获取旧的元素,然后使用新的编码填充
    // 这样就不用担心覆盖了其他元素了,prepend 则用来保证有一个空位来存放新元素
    while (length--)
        _intsetSet(is, length + prepend, _intsetGetEncoded(is, length, curenc));

    if (prepend)
        _intsetSet(is, 0, value);
    else
        _intsetSet(is, intrev32ifbe(is->length), value);

    if->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)) {
        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;
}

其他

获取集合长度

uint32_t intsetLen(intset *is) {
    return intrev32ifbe(is->length);
}