跳表是什么:
跳表是链表+链表索引
构成的一种数据结构
跳表有什么用:
跳表是优化链表查找效率的一种数据结构。
链表在查找时时间复杂度是O(n),数据量大时,效率不高,要想提升查找效率,非常直接的思路就是要减少需要遍历的元素个数。
优化思路:
-
借鉴数组的二分查找算法,基于原始链表构建一个新的数量大幅减少的链表,称之为子链表或者索引链表,可能有多层,见下图。
-
如图所示,上层链表基于下层链表的每两个节点选取第一个节点,将这些节点首尾相连,构成一个新链表。随着层数的增高,链表的节点数逐渐减少。
当然,也可以每三个(或者更多)节点选取一个节点,视情况而定。 -
优化后,在查找时,从最上层开始查找,最好的情况是在最上层查找成功,最坏的情况是要在最下层查找,但即便是最坏的情况,也会大大缩小查询范围。
查找时间复杂度:
-
优化后的查询复杂度是O(logn),和二分查找的时间复杂度是一样的。
-
要理解这个时间复杂度,关键在于理解,每层上最多需要遍历多少个节点,如果是每两个节点取一个节点构成的链表,
每层只需要遍历3个节点
,看图即可理解。 -
最多有多少层呢?由公式计算而出最多 log(n)层,则整个搜索过程最多需要遍历 3*log(n)次,即算法时间复杂度位log(n)
特点:
- 典型的用空间换时间
- 原链表是有序链表
- 上层链表是下次链表的子集,上层链表的节点一定存在于下层链表
代码:
package skiplist;
/**
* 跳表的一种实现方法。
* 跳表中存储的是正整数,并且存储的是不重复的。
*
* Author:ZHENG
*/
public class SkipList {
private static final float SKIPLIST_P = 0.5f;
private static final int MAX_LEVEL = 16;
private int levelCount = 1;
private Node head = new Node(); // 带头链表
public Node find(int value) {
Node p = head;
for (int i = levelCount - 1; i >= 0; --i) {
while (p.forwards[i] != null && p.forwards[i].data < value) {
p = p.forwards[i];
}
}
if (p.forwards[0] != null && p.forwards[0].data == value) {
return p.forwards[0];
} else {
return null;
}
}
public void insert(int value) {
int level = randomLevel();
Node newNode = new Node();
newNode.data = value;
newNode.maxLevel = level;
Node update[] = new Node[level];
for (int i = 0; i < level; ++i) {
update[i] = head;
}
// record every level largest value which smaller than insert value in update[]
Node p = head;
for (int i = level - 1; i >= 0; --i) {
while (p.forwards[i] != null && p.forwards[i].data < value) {
p = p.forwards[i];
}
update[i] = p;// use update save node in search path
}
// in search path node next node become new node forwords(next)
for (int i = 0; i < level; ++i) {
newNode.forwards[i] = update[i].forwards[i];
update[i].forwards[i] = newNode;
}
// update node hight
if (levelCount < level) levelCount = level;
}
public void delete(int value) {
Node[] update = new Node[levelCount];
Node p = head;
for (int i = levelCount - 1; i >= 0; --i) {
while (p.forwards[i] != null && p.forwards[i].data < value) {
p = p.forwards[i];
}
update[i] = p;
}
if (p.forwards[0] != null && p.forwards[0].data == value) {
for (int i = levelCount - 1; i >= 0; --i) {
if (update[i].forwards[i] != null && update[i].forwards[i].data == value) {
update[i].forwards[i] = update[i].forwards[i].forwards[i];
}
}
}
while (levelCount>1&&head.forwards[levelCount]==null){
levelCount--;
}
}
// 插入元素时,从第几层开始插入,是随机选取的,但也是有一定技巧的,底层的概率高,高层的概率低
// 理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层。
// 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。
// 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 :
// 50%的概率返回 1
// 25%的概率返回 2
// 12.5%的概率返回 3 ...
private int randomLevel() {
int level = 1;
while (Math.random() < SKIPLIST_P && level < MAX_LEVEL)
level += 1;
return level;
}
public void printAll() {
Node p = head;
while (p.forwards[0] != null) {
System.out.print(p.forwards[0] + " ");
p = p.forwards[0];
}
System.out.println();
}
public class Node {
private int data = -1;
private Node forwards[] = new Node[MAX_LEVEL];
private int maxLevel = 0;
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{ data: ");
builder.append(data);
builder.append("; levels: ");
builder.append(maxLevel);
builder.append(" }");
return builder.toString();
}
}
}
代码总结:
- 插入时根据随机函数选择从第几层开始插入,随机函数巧妙
- 链表的层次包含在Node节点的定义中