集合物件的編碼有兩種:intset
和 hashtable
編碼一:intset
intset 的結構
整數集合 intset
是集合底層的實現之一,從名字就可以看出,這是專門為整數提供的集合型別。
其結構定義如下,在 intset.h
:
typedef struct intset {
// 編碼方式
uint32_t encoding;
// 集合包含的元素數量
uint32_t length;
// 儲存元素的陣列
int8_t contents[];
} intset;
contents
中的元素,按照從小到大排序,並且不存在重複項。雖然元素定義是int8_t
型別,但實際上,contents
存的元素型別取決於encoding
encoding
有幾個型別,定義在intset.c
:
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
encoding | 型別 | 位元組 |
---|---|---|
INTSET_ENC_INT16 | int16_t | 2 |
INTSET_ENC_INT32 | int32_t | 4 |
INTSET_ENC_INT64 | int64_t | 8 |
下圖展示了包含了 1、2、3 三個整數元素的集合結構:
常見操作原始碼分析
原始碼在
intset.c
中
1. 建立空集合
建立一個空的 intset
,一開始的編碼是最小的 INTSET_ENC_INT16
intset *intsetNew(void) {
intset *is = zmalloc(sizeof(intset));
is->encoding = intrev32ifbe(INTSET_ENC_INT16);
is->length = 0;
return is;
}
2. 搜尋
因為集合中的整數存的是有序的,所以查詢是用二分查詢,時間複雜度 \(O(nlogn)\)
uint8_t intsetFind(intset *is, int64_t value) {
uint8_t valenc = _intsetValueEncoding(value);
// 如果 value 的編碼大於集合的編碼,那肯定是不存在的
// intsetSearch 是更底層的搜尋,實現原始碼在下面,是個二分查詢
return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
}
// 集合搜尋,是二分查詢。
// 如果找到了,返回1,並且把位置設定到 pos 變數中
// 如果找不到,返回0,可以插入值的位置設定到 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;
// 陣列判空
if (intrev32ifbe(is->length) == 0) {
if (pos) *pos = 0;
return 0;
} else {
// 看是否比最大的大或者比最小的小,這種情況也直接返回不存在
if (value > _intsetGet(is,max)) {
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;
}
}
3. 指定位置獲取
// 如果獲取得到,返回1,找到的值設定進 value 變數
// 如果獲取不到,返回 0
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value) {
if (pos < intrev32ifbe(is->length)) {
*value = _intsetGet(is,pos);
return 1;
}
// 位置如果大於長度,肯定就獲取不到的
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;
// ...
// 根據編碼的長度,從對應的位置後拷貝對應的位元組返回
if (enc == INTSET_ENC_INT64) {
memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64));
memrev64ifbe(&v64);
return v64;
} else if (enc == INTSET_ENC_INT32) {
// ...
return v32;
} else {
// ...
}
}
4. 插入
插入的步驟如下:
- 檢查如果插入的元素的編碼大於集合編碼,進行升級並插入
- 如果不需要升級,檢查元素是否存在,如果存在,則直接返回
- 如果元素不存在,則擴容,在元素對應的位置插入值(它後面的元素則都往後挪)
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 intsetUpgradeAndAdd(is,value);
} else {
// 先查詢看元素已存在,如果存在,則直接返回
if (intsetSearch(is,value,&pos)) {
if (success) *success = 0;
return is;
}
// 擴容
is = intsetResize(is,intrev32ifbe(is->length)+1);
// 將 pos 後的記憶體塊向後挪動一個位置,給新值騰空間
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 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) {
// ...
} else {
// ...
}
memmove(dst,src,bytes);
}
5. 升級
當 intset
插入元素的時候,會先檢測元素的長度,判斷元素應該屬於什麼編碼(encoding
)。
如果當前元素的編碼,大於 intset
的編碼(整個集合最長的編碼),集合將進行升級後,才新增元素。
升級整數集合並新增新元素共分為 3 步進行:
- 根據新元素的編碼,擴充套件整數集合底層陣列的空間大小,併為新元素分配空間。
- 將底層陣列現有的所有元素都轉換成與新元素相同的型別,並將型別轉換後的元素放置到正確的位上,而且在放置元素的過程中,需要繼續維持底層陣列的有序性質不變。
- 將新元素新增到底層陣列裡面。
// 升級並插入新值
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
// 當前編碼
uint8_t curenc = intrev32ifbe(is->encoding);
// 新的編碼
uint8_t newenc = _intsetValueEncoding(value);
// 當前元素個數
int length = intrev32ifbe(is->length);
// value 的編碼比其他的都大,那麼這個 value 不是最大值就是最小值。
// 如果是最大值就放在陣列最後,最小值就放在陣列最前面
int prepend = value < 0 ? 1 : 0;
// 設定 encoding 屬性為新編碼
is->encoding = intrev32ifbe(newenc);
// 根據新編碼給擴充套件集合需要的空間,實現原始碼在下面
is = intsetResize(is,intrev32ifbe(is->length)+1);
// 從尾到頭依次遍歷挪動原來的值。為什麼不從頭到尾呢?因為陣列是同一個,從頭到尾會覆蓋原來的值
while(length--)
// _intsetGetEncoded(is,length,curenc) 表示根據編碼和位置獲取值
// prepend 為了確保如果 value 是最小的值,那麼前面會留一個空位置
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
if (prepend)
// 當 value 是最小值時,放在第一個空位
_intsetSet(is,0,value);
else
// 當 value 是最大值,放在最後一個位置
_intsetSet(is,intrev32ifbe(is->length),value);
// 長度加 1
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
// 整數集合重新分配記憶體
static intset *intsetResize(intset *is, uint32_t len) {
// 根據編碼算出集合需要的空間
uint32_t size = len*intrev32ifbe(is->encoding);
// 分配記憶體
is = zrealloc(is,sizeof(intset)+size);
return is;
}
6. 降級
並沒有降級
7. 刪除
刪除的步驟如下:
- 找到值的位置
pos
- 把
pos
後面的元素向前挪,覆蓋掉pos
上的元素 - 縮容:長度減一
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;
// 把刪除位置後面的元素都挪到前面來,直接覆蓋掉 pos 的元素
if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
// 再縮容
is = intsetResize(is,len-1);
is->length = intrev32ifbe(len-1);
}
return is;
}
編碼二:hashtable
hashtable
編碼用的是字典 dict
作為底層實現,關於 dict
,具體的前文 Redis 設計與實現 4:字典 dict 已經寫了,包括了 dict 基本操作的原始碼解讀。
下圖展示了包含 "a"、"b"、"c"、"d" 四個元素的集合結構:
編碼的轉換
當集合物件滿足以下兩個條件時,採用 intset
編碼:
- 所有元素都是整數
- 元素數量不超過512個(用通過
set-max-intset-entries
配置項配置)
不能同時滿足以上兩個條件,則採用 tablehash
編碼。