文章目录
4. 跳跃表(skiplist)
跳跃表是一种有序数据结构,通过每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,也能够顺序批量处理节点。
大部分情况下,跳跃表在效率上和平衡树一致,但是跳跃表更为简单。Redis中,当一个有序集合包含的元素数量多,或者元素成员是较长的字符串的时候,会采用跳跃表来作为有序集合键的底层实现。
注:Redis中使用跳跃表只有两个地方
- 实现有序集合键
- 用作集群节点中的内部数据结构
4. 1 . 如何实现跳跃表?
两个结构:redis.h/zskiplistNode(用于表示跳跃表节点)和redis.h/zsklist(用于保存跳跃表节点的信息,例如节点的数量,表头尾节点之类的)。
4.1.1. 跳跃表节点(redis.h/zskiplistNode)
typedef struct zskiplistNode {
// 层
struct zskiplistLevel{
// 前进指针
struct zskipListNode *forward;
// 跨度
unsigned int span;
} level[];
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
}zskiplistNode;
属性解释:
-
层
跳跃表节点的level数组可以包含多个元素,每个元素都有一个指向其他节点的指针,程序就是通过这些层加快访问其他节点的速度。**简而言之,层多,速度快。**当我们创建一个新的跳跃表节点的时候,程序根据幂次定律随机生成一个介于1-32之间的值作为level数组的大小(层 的高度)。
-
前进指针
指向表尾方向的指针(level[i].forwards属性),作用就是表示向表尾方向访问节点。
-
跨度
level[i].sapn 用于记录两个节点之间的距离,
- 跨度越大,距离越远
- 执行NULL的所有前进指针的跨度都为0,因为没有连接任何节点
-
后退指针
backward属性,用于从表尾向表头方向访问节点,每个节点只有一个后退指针,所以一次只能后退到前一个节点。
-
分值和成员
score,跳跃表中的所有节点都按照分值的大小排序;
obj,指针,指向一个字符串对象(保存着一个SDS),同一跳跃表中,每个节点的成员对象唯一,但是分值可以相同,同分值的节点按照成员对象在字典序中的大小进行排序(小(表头)->大(表尾))。
4…1.2. 跳跃表(redis.h/zskiplist)
typedef struct zskiplist{
// 表头节点和表尾节点
struct skiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
}zskiplist;
属性解释:
- header和tail分别执行跳跃表的表头和表尾节点,通过他们,我们要访问表头表尾节点的复杂度是O(1)
- length 用于记录节点的数量
- level 作用是在O(1)复杂度内获取跳跃表中层高最大的那个节点的层数,但是表头节点的层高不计算在内。
4.2. 跳跃表API
函数 | 作用 | 时间复杂度 |
---|---|---|
zslCreate | 创建一个新的跳跃表 | O(1) |
zslFree | 释放给定跳跃表,包括其中的节点 | O(N),N为跳跃表长度 |
zslInsert | 将包含给定成员的和分值的对象添加到跳跃表中 | 平均O(logN),最坏O(N) |
zslDelete | 删除跳跃表中包含给定成员和分值的节点 | 平均O(logN),最坏O(N) |
zslGetRank | 返回包含给定成员和分值的节点在表中的排位 | 平均O(logN),最坏O(N) |
zslGetElementByRank | 返回跳跃表在给定排位上的节点 | 平均O(logN),最坏O(N) |
zslIsInRange | 给定一个分值范围(range),如果跳跃表中至少有一个节点的分值在这个范围内就返回1,否则返回0 | O(1),只需要访问表头和表尾节点就行了 |
zslFirstInRange | 给定一个分值范围,返回表中第一个符合这个范围的节点 | 平均O(logN),最坏O(N) |
zslLastInRange | 给定一个分值范围,返回跳跃表中最后一个符合这个范围的节点 | 平均O(logN),最坏O(N) |
zslDeleteRangeByScore | 给定一个分值范围,删除表中所有在这个范围之内的节点 | O(N),N为被删除的节点的数量 |
zslDeleteRangeByRank | 给定一个排位范围,删除表中所有在这个范围之内的节点 | O(N),N为被删除的节点的数量 |
5. 整数集合
整数集合(intset)是Redis中集合键的底层实现之一,当一个集合只包含整数值袁术,且数量不多时,Redis就会采用整数集合作为集合键的底层实现。
5.1. 如何实现整数集合?
整数集合可以保存的数据类型可以是int32_t、int16_t或者int64_t。
typedef struct inset{
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存的元素的数组
int8_t contents[];
}intset;
解释下;
-
contents数组是整数集合的底层实现,整数集合中的每个元素都是contents的一个数据项,每个项按从小到大的顺序排列,并且不重复。
contents数组声明的类型是int8_t,但是contents的真正类型取决于encoding的值
- 如果encoding的值为INTSET_ENC_INT16,那么content是就是Int16_t数组(值的大小 -32768 ~ 32767)
- 如果encoding的值为INTSET_ENC_INT32,那么content是就是Int32_t数组(值的大小 -2147483648 ~ 2147483647)
- 如果encoding的值为INTSET_ENC_INT64,那么content是就是Int64_t数组(值的大小 -9223372036854775808~ 9223372036854775807)
-
length属性记录整数集合包含的元素数量,也就是contents数组的长度。
5.2. 升级
当我们将新元素插入到整数集合中,而这个新元素的类型比整数集合现有的所有元素的类型都要长的时候,就需要对整数集合升级,才能将新元素插入到整数集合中。
5.2.1. 如何升级?
- 根据新元素类型,扩展整数集合底层数组的空间大小,并且为新元素分配空间。
- 将底层数组现有的所有元素转换成新元素的类型,并将类型转换后的元素放到合适的位置上,在这个过程中 ,要保证底层数组的有序性。
- 将新元素插添加到底层数组中。
5.2.2. 升级的好处
-
提升灵活性
整数集合会自动升级底层数组来适应新元素,所以不会出现类型错误的问题。
-
节约内存
再没有将大类型的元素添加到底层数组中之前,整数集合保存的元素都是小类型的,而不需要预先将所有的元素存为大类型,来保存数据。
注:整数集合不支持降级操作。
5.3. 整数集合API
函数 | 作用 | 时间复杂度 |
---|---|---|
intsetNew | 创建一个新的压缩列表 | O(1) |
intsetAdd | 将给定元素添加到整数集合中 | O(N) |
intsetRemove | 从整数集合中删除给定元素 | O(N) |
intsetFind | 检查给定值是否存在集合中 | 采用二分查找,O(logN) |
intsetRandom | 从整数集合中随机返回一个元素 | O(1) |
intsetGet | 取出底层数组中给定索引上的元素 | O(1) |
intsetLen | 返回整数集合包含的元素个数 | O(1) |
intsetBlobLen | 返回整数集合占用的内存字节数 | O(1) |