redis.h 中的 zskiplist 结构和 zskiplistNode 结构, 以及t_zset.c 中所有以 zsl 开头的函数
class Skiplist
{
public:
struct Node
{
Node *right;
Node *down;
int val;
Node(Node* r, Node* d, int v)
: right(r)
, down(d)
, val(v)
{}
};
public:
Skiplist()
{
head = new Node(nullptr, nullptr, -1);
}
bool search(int num)
{
Node *p = head;
while(p)
{
while(p->right && p->right->val < num)
p = p->right;
if(p->right && p->right->val == num)
return true;
p = p->down;
}
return false;
}
void add(int num)
{
vector<Node*> path;
Node *p = head;
while(p)
{
while(p->right && p->right->val < num)
p = p->right;
path.push_back(p);
p = p->down;
}
bool insertUp = true;
Node *downNode = nullptr;
while(insertUp && path.size())
{
auto pos = path.back();
path.pop_back();
pos->right = new Node(pos->right, downNode, num);
downNode = pos->right;
insertUp = rand() & 1;
}
if(insertUp)
{
Node *q = new Node(nullptr, downNode, num);
head = new Node(q, head, -1);
}
}
Node *head;
};
int main()
{
Skiplist list;
list.add(1);
list.add(3);
list.add(7);
list.add(9);
list.add(10);
bool res1 = list.search(7);
bool res2 = list.search(8);
cout << res1 << endl << res2 << endl;
}
对于查找来说,都是从最高层开始查找。请仔细看源码中的查找实现。
假设查找7
这里的实现实际上只存储了key,即val。并没有将value放入。因此,即使我们多构造了很多节点,但是实际value值确只有一份,所以空间的牺牲是可以接受的。
typedef struct zskiplistNode {
// 成员对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
level: 记录目前跳表内,层数最大的那个节点的层数。
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
尾指针指错了
我们来演示下如何插入新节点的。新节点的score = 13
x = zsl->header;
for (int i = zsl->level - 1; i >= 0; i--)
{
rank[i] = i == zsl->level-1 ? 0 : rank[i+1];
}
while(x->level[i].forward && x->level[i].forward.score < score)
{
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;
rank{2, 2}
update{node10->level[0], node10->level[1]}
创建新的节点,并按照power次概率指定level。level = 1
update中存储的都是待插入节点的左边路径。所以新节点要插入在update节点的右边。
for (i = 0; i < level; i++) {
// 将新节点插入到记录的路径右侧
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
span的计算方法如下
7. 新插入节点的level - zsl->level的update节点没有被使用,只用更新跨度即可。
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
if (level > zsl->level) {
// 初始化未使用层
// T = O(1)
for (i = zsl->level; i < level; i++) {
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = zsl->length;
}
// 更新表中节点最大层数
zsl->level = level;
}
现在,我们对Redis的跳表终于有了一定的了解了,我们现在可以对一些属性做出解释了。
对于zskipList来说
对于zskipNode来说
rank值
非常重要。关于基础操作,我不是很关心,在一般的跳表都有,比较关心Redis在跳表上实现的特有操作。
可以以O(logN)的速度获得有序数组中的第K个值。
// redis.h
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o) {
zskiplistNode *x;
unsigned long rank = 0;
int i;
// 遍历整个跳跃表
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
// 遍历节点并对比元素
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
// 比对分值
(x->level[i].forward->score == score &&
// 比对成员对象
compareStringObjects(x->level[i].forward->obj,o) <= 0))) {
// 累积跨越的节点数量
rank += x->level[i].span;
// 沿着前进指针遍历跳跃表
x = x->level[i].forward;
}
/* x might be equal to zsl->header, so test if obj is non-NULL */
// 必须确保不仅分值相等,而且成员对象也要相等
// T = O(N)
if (x->obj && equalStringObjects(x->obj,o)) {
return rank;
}
}
// 没找到
return 0;
}
根据源码,这里很简单,就是利用了span这个字段,在查找的时候累加span,就获得了排位。
zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) {
zskiplistNode *x;
unsigned long traversed = 0;
int i;
// T_wrost = O(N), T_avg = O(log N)
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
// 遍历跳跃表并累积越过的节点数量
while (x->level[i].forward && (traversed + x->level[i].span) <= rank)
{
traversed += x->level[i].span;
x = x->level[i].forward;
}
// 如果越过的节点数量已经等于 rank
// 那么说明已经到达要找的节点
if (traversed == rank) {
return x;
}
}
// 没找到目标节点
return NULL;
}
这里要对跳表的结构有这样一种理解