Aleph1022
2018-10-20 11:19:29
SkipList 的复杂度和红黑树一样,而且实现起来更简单。
在并发环境下 SkipList 有另外一个优势,红黑树在插入和删除的时候可能需要做一些保持平衡的操作,这样的操作可能会涉及到整个树的其他部分,而 SkipList 的操作显然更加局部性一些,所以需要盯住的节点更少,因此在这样的情况下性能好一些。
上文引用自:知乎 - 为啥 redis 使用跳表(skiplist)而不是使用 red-black? - 于康的回答
如果你要在一个有序的序列中查找元素
如果你需要维护一个支持插入操作的有序表,大家又会想到链表。
但是,链表无法随机访问,因此也无法二分。而且在有序的链表中间插入,寻找插入的位置最坏是
我们先来看看这张图:
如果要在这里面找
我们考虑从中抽出一些节点,建立一层索引作用的链表:
这样的话,找
跳表的主要思想就是这样逐渐建立索引,加速查找与插入。
一般来说,如果要做到严格
此外,我们在实现时,一般在插入时就确定数值的层数,而且层数不能简单的用随机数,而是每次以
就像这样:
inline int rand_level()
{
int ret = 1;
while(rand() % 2 && ret < MAX_LEVEL)
++ret;
return ret;
}
其中
我们来看看存储结点的结构体:
struct node
{
int key,value;
int next[MAX_LEVEL + 5];
} sl[N + 10];//N 为常量。
next[i]
表示这个结点在第
我们初始化时,应定义两个虚拟结点 head,tail
,表示链表头与链表尾,其实 tail
可以省去,但是为了易于理解,我没有去掉。
笔者个人有一个癖好:就是用一个栈或是队列保存已经被删除的节点,模拟一个内存池,可以节省很多空间,使空间在
分配新结点:
inline void new_node(int &p,int key,int value = 0)
{
if(top)
p = st[top--];
else
p = ++node_tot;
sl[p].key = key;
sl[p].value = value;
}
回收结点:
inline void free_node(int p)
{
st[++top] = p;
}
初始化:
inline void init()
{
new_node(head,-INF),new_node(tail,INF);
for(register int i = 1;i <= MAX_LEVEL;++i)
sl[head].next[i] = tail;
}
按照定义,链表头尾应分别为负与正无穷。但是有时候是不需要的,不过为避免某些锅还是打上的好。
考虑在一个已经建好的跳表中查找一个值
int find(int key)
{
int p = head;
for(register int i = MAX_LEVEL;i;--i)
while(sl[p].next[i] ^ tail && sl[sl[p].next[i]].key < key)
p = sl[p].next[i];
return sl[p].next[1];
}
注意这个代码并没有判断不存在这个元素的情况。
插入操作的思路是,同样从顶层开始,逐层找到每层需要插入的位置,再生成层数并插入:
void insert(int key,int value)
{
int p = head;
int update[MAX_LEVEL + 5];
int k = rand_level();
for(register int i = MAX_LEVEL;i;--i)
{
while(sl[p].next[i] != tail && sl[sl[p].next[i]].key < key)
p = sl[p].next[i];
update[i] = p;
}
int temp;
new_node(temp,key,value);
for(register int i = k;i;--i)
{
sl[temp].next[i] = sl[update[i]].next[i];
sl[update[i]].next[i] = temp;
}
}
删除操作几乎与插入对称:
void erase(int key)
{
int p = head;
int update[MAX_LEVEL + 5];
for(register int i = MAX_LEVEL;i;--i)
{
while(sl[p].next[i] != tail && sl[sl[p].next[i]].key < key)
p = sl[p].next[i];
update[i] = p;
}
free_node(sl[p].next[1]);
for(register int i = MAX_LEVEL;i;--i)
{
if(sl[sl[update[i]].next[i]].key == key)
sl[update[i]].next[i] = sl[sl[update[i]].next[i]].next[i];
}
}
先来看道水题:CodeVS 1230。
给出
n 个正整数,然后有m 个询问,每个询问一个整数,询问该整数是否在n 个正整数中出现过。
虽然是 Hash 入门题,但是 set 可过。所以我们也可以尝试用跳表来写。
代码:
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;
const int N = 1e5;
const int MAX_LEVEL = 25;
struct node
{
int key;
int next[MAX_LEVEL + 5];
} sl[N + 10];
int node_tot,head,tail;
int st[N + 10],top;
inline int rand_level()
{
int ret = 1;
while(rand() % 2 && ret <= MAX_LEVEL)
++ret;
return ret;
}
inline void new_node(int &p,int key = 0)
{
if(top)
p = st[top--];
else
p = ++node_tot;
sl[p].key = key;
}
inline void free_node(int p)
{
st[++top] = p;
}
inline void init()
{
new_node(head),new_node(tail);
for(register int i = 1;i <= MAX_LEVEL;++i)
sl[head].next[i] = tail;
}
void insert(int key)
{
int p = head;
int update[MAX_LEVEL + 5];
int k = rand_level();
for(register int i = MAX_LEVEL;i;--i)
{
while(sl[p].next[i] ^ tail && sl[sl[p].next[i]].key < key)
p = sl[p].next[i];
update[i] = p;
}
int temp;
new_node(temp,key);
for(register int i = k;i;--i)
{
sl[temp].next[i] = sl[update[i]].next[i];
sl[update[i]].next[i] = temp;
}
}
bool find(int key)
{
int p = head;
for(register int i = MAX_LEVEL;i;--i)
while(sl[p].next[i] ^ tail && sl[sl[p].next[i]].key < key)
p = sl[p].next[i];
if(sl[sl[p].next[1]].key == key)
return 1;//找到了
return 0;//没找到
}
int n,m;
int main()
{
srand(19260817);
scanf("%d%d",&n,&m);
int x;
for(register int i = 1;i <= n;++i)
scanf("%d",&x),insert(x);
while(m--)
scanf("%d",&x),puts(find(x) ? "YES" : "NO");
}
好,现在来看道蓝题:P2286。
此题的思路就是,每次找它的前驱与后继中与它差更小的那个。
但是有一个细节:就是要区分人与宠物。所以在其为空的时候我们记录第一个插入的值的类型,此后遇到同类型的就插入,否则就进行领养操作。
set
版代码:
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <set>
#include <algorithm>
using namespace std;
const int mod = 1e6;
int n,ans;
int type;
set<int> d;
inline void answer(int x)
{
auto l = --d.lower_bound(x);
auto r = d.lower_bound(x);
if(x - *l <= *r - x && *l ^ -0x3f3f3f3f)
{
ans = (ans + x - *l) % mod;
d.erase(l);
}
else
{
ans = (ans + *r - x) % mod;
d.erase(r);
}
}
int main()
{
d.insert(0x3f3f3f3f),d.insert(-0x3f3f3f3f);
int x,y;
scanf("%d",&n);
for(register int i = 1;i <= n;++i)
{
scanf("%d%d",&x,&y);
if(d.size() == 2)
type = x,d.insert(y);
else if(x == type)
d.insert(y);
else
answer(y);
}
printf("%d\n",ans);
}
我们考虑转换为跳表版。但是此时我们还需要维护跳表的结点个数。这个很简单,只需要在新建结点时顺便 ++
,回收内存时 --
。或者是在插入删除时处理也可。
以及 lower_bound
函数,由于我们存储的不是双向链表,模拟对 set
的迭代器 --
的操作有两个方法:
--
的结果最终代码:
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
using namespace std;
const int N = 1e5;
const int mod = 1e6;
int n,ans;
int type;
const int MAX_LEVEL = 30;
struct node
{
int key;
int next[MAX_LEVEL + 5];
} sl[N + 10];
int node_tot,head,tail,size;
int st[N + 10],top;
inline int rand_level()
{
int ret = 1;
while(rand() % 2 && ret < MAX_LEVEL)
++ret;
return ret;
}
inline void new_node(int &p,int key = 0)
{
if(top)
p = st[top--];
else
p = ++node_tot;
sl[p].key = key;
++size;
}
inline void free_node(int p)
{
st[++top] = p;
--size;
}
inline void init()
{
new_node(head,-0x3f3f3f3f),new_node(tail,0x3f3f3f3f);
for(register int i = 1;i <= MAX_LEVEL;++i)
sl[head].next[i] = tail;
}
void insert(int key)
{
int p = head;
int update[MAX_LEVEL + 5];
int k = rand_level();
for(register int i = MAX_LEVEL;i;--i)
{
while(sl[p].next[i] ^ tail && sl[sl[p].next[i]].key < key)
p = sl[p].next[i];
update[i] = p;
}
int temp;
new_node(temp,key);
for(register int i = k;i;--i)
{
sl[temp].next[i] = sl[update[i]].next[i];
sl[update[i]].next[i] = temp;
}
}
void erase(int key)
{
int p = head;
int update[MAX_LEVEL + 5];
for(register int i = MAX_LEVEL;i;--i)
{
while(sl[p].next[i] ^ tail && sl[sl[p].next[i]].key < key)
p = sl[p].next[i];
update[i] = p;
}
free_node(sl[p].next[1]);
for(register int i = MAX_LEVEL;i;--i)
if(sl[sl[update[i]].next[i]].key == key)
sl[update[i]].next[i] = sl[sl[update[i]].next[i]].next[i];
}
int lb(int key)
{
int p = head;
for(register int i = MAX_LEVEL;i;--i)
while(sl[p].next[i] ^ tail && sl[sl[p].next[i]].key < key)
p = sl[p].next[i];
return p;//相当于对 lower_bound 的结果进行 --
}
inline void answer(int x)
{
int l = lb(x);
int r = sl[l].next[1];//相当于对迭代器 ++
if(sl[l].key ^ -0x3f3f3f3f && x - sl[l].key <= sl[r].key - x)
{
ans = (ans + x - sl[l].key) % mod;
erase(sl[l].key);
}
else
{
ans = (ans + sl[r].key - x) % mod;
erase(sl[r].key);
}
}
int main()
{
srand(19260817);
init();
int x,y;
scanf("%d",&n);
while(n--)
{
scanf("%d%d",&x,&y);
if(size == 2)
type = x,insert(y);
else if(x == type)
insert(y);
else
answer(y);
}
printf("%d\n",ans);
}
考虑在跳表的每一个结点上,维护同一层内该结点与后一个结点差了多少个数,这样就可以维护一棵名次树了。
然鹅笔者太鸽就不写了,不如去写平衡树。