目錄算法
整數集合是 Redis 集合鍵的底層實現之一。當一個集合只包含整數值元素,而且元素數量很少時,Redis 就會使用整數集合做爲集合鍵的底層實現。數組
整數集合是 Redis 用於保存整數值的集合抽象數據結構。它能夠保存類型爲 int16_t、int32_t、int64_t 的整數值,而且保證集合中不會出現重複元素。數據結構
每一個 intset.h/intset
結構表示一個整數集合:ui
typedef struct intset { uint32_t encoding; uint32_t length; int8_t contents[]; } intset;
contents 數組是整數集合的底層實現:整數集合的每一個元素都是 contents 數組的一個數組項,各個項在數組中按值的大小從小到大有序排列,而且數組中不包含重複項。編碼
length 屬性記錄了整數集合記錄的元素數量,也就是 contents 數組的長度。spa
雖然 intset 結構將 contents 屬性聲明爲 int8_t 類型的數組,但實際上 contents 數組並不保存任何 int8_t 類型的值,contents 數組的真正類型取決於 encoding 屬性的值,好比:若是 encoding 屬性的值爲 INTSET_ENC_INT16,那麼 contents 就是一個 int16_t 類型的數組,數組裏的每一個項都是一個 int16_t 類型的整數值,取值範圍爲:[-32768-32767](2^(16-1))。code
與之相似,encoding 的值爲 INTSET_ENC_INT32,那麼數組每項的取值範圍爲:[-2147483648, 2147483647](2^(32-1)。排序
這裏也引起了一個問題,當咱們對一個 encoding 爲 INTSET_ENC_INT8 的 intset,插入 129 時(int8_t 的取值範圍是 [-128, 127]),會出現什麼?索引
這也就引起了 intset 的升級操做。與之對應,也有降級操做。接下來,咱們來詳細認識下 intset 的升降級操做。內存
每當咱們要將一個新元素添加到整數集合時,若是新元素的類型比整數集合的 encoding 類型大,整數集合就須要先進行升級操做(upgrade),而後才能將新元素添加到整數集合中。
整個升級操做源碼以下:
// intset.c/intsetUpgradeAndAdd() /* Upgrades the intset to a larger encoding and inserts the given integer. */ 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; }
升級整數集合並添加新元素,共分爲三步進行:
此外,一旦因插入新元素引起升級操做,就說明新插入的元素比集合中現有的全部元素的長度大,因此這個新元素的值要麼大於全部現有元素(正值),要麼就小於全部現有元素(負值),那麼:
整數集合的升級策略主要有如下兩個好處:
由於 C 語言是靜態類型語言,爲了不類型錯誤,咱們一般不會將兩種不一樣類型的值放在同一個數據結構中。
可是,由於有了升級操做,整數集合能夠經過它來自適應新元素,因此咱們能夠隨意地將 int16_t、int32_t、和 int64_t 類型的整數添加到集合中,而沒必要擔憂出現類型錯誤,大大的提高了整數集合的靈活性。
固然,要讓一個數組能夠同時保存 int16_t、int32_t、和 int64_t 類型的整數值,咱們能夠粗暴的直接使用 int64_t 類型的數組做爲整數集合的底層實現,來保存不一樣類型的值。可是,這樣一來,即便添加到集合中的都是 int16_t、int32_t 類型的值,數組也都是須要使用 int64_t 類型的空間去保存,出現浪費內存的狀況。
而整數集合的升級操做,既能同時保存三種不一樣類型的值,又能夠確保升級操做只會在有須要的時候進行,達到節省內存的目的。
Redis 中的集合實現了交、並、差等操做,相關操做可參加 t_set.c
,其中
sinterGenericCommand()
實現交集,sunionDiffGenericCommand()
實現並集和差集。
它們都能同時對多個集合進行元素。當對多個集合進行差集運算時,會先計算出第一個和第二個集合的差值,而後再與第三個集合作差集,依次類推。
接下來,咱們一塊兒來認識下三個操做的實現思路。
計算交集的過程大概能夠分爲三部分:
須要注意的是,上述第 3 步在集合中進行查找,對於 intset 和 dict 的存儲來講時間複雜度分別是 O(log n) 和 O(1)。但因爲只有小集合才使用 intset,因此能夠粗略地認爲 intset 的查找也是常數時間複雜度的。
並集操做最簡單,只要遍歷全部集合,將每個元素都添加到最後的結果集中便可。向集合中添加元素會自動去重,因此插入的時候無需檢測元素是否已存在。
計算差集有兩種可能的算法,它們的時間複雜度有所區別。
第一種算法
對第一個集合進行遍歷,對於它的每個元素,依次在後面的全部集合中進行查找。只有在全部集合中都找不到的元素,才加入到最後的結果集合中。
這種算法的時間複雜度爲O(N*M),其中N是第一個集合的元素個數,M是集合數目。
第二種算法
在計算差集的開始部分,會先分別估算一下兩種算法預期的時間複雜度,而後選擇複雜度低的算法來進行運算。還有兩點須要注意: