什么是跳表
跳表SkipList,全称跳跃列表,其实就是一个创建了目录方便查找的有序链表。
引用百度百科的定义:
跳表全称叫做跳跃表,简称跳表。跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。
引用维基百科的定义:
在计算机科学中,跳跃列表是一种数据结构。它使得包含n个元素的有序序列的查找和插入操作的平均时间复杂度都是O(log n),优于数组的 O(n)复杂度。快速的查询效果是通过维护一个多层次的链表实现的,且与前一层(下面一层)链表元素的数量相比,每一层链表中的元素的数量更少(见右下角示意图)。一开始时,算法在最稀疏的层次进行搜索,直至需要查找的元素在该层两个相邻的元素中间。这时,算法将跳转到下一个层次,重复刚才的搜索,直到找到需要查找的元素为止。跳过的元素的方法可以是随机性选择或确定性选择,其中前者更为常见。
跳表的应用和好处
Redis中有序集合(Sorted Set)采用了这种数据结构,不仅提高了搜索能力,同时也提高插入和删除操作的性能。 跳表是一个随机化的数据结构,可以被看做二叉树的一个变种,它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单。
跳表跟平衡树(AVL、红黑树)一样都是有序,而哈希表是无序的,所以哈希表不适合范围查找。查找单个key的时候,跳表的时间复杂度要O(logn),而哈希的时间复杂度为O(1)。
范围查询的时候平衡树会比较复杂,需要通过各种遍历,而跳表比较简单,找到最小值之后,对原始数据进行遍历即可;平衡树的插入删除需要旋转调整树结构,而跳表修改指针即可,简单又快速。
跳表的算法设计比较简单。
下面以普通链表和跳表分析查找元素20的效率对比:
想要定位到链表中的节点20,无法直接定位,需要从头节点开始,顺着next指针,逐个访问下一个节点,属于线性遍历。
下面这种情况就不一样,可以根据索引提升查询效率。
首先从最上层的索引开始查找,找到该层中小于节点20的前置索引15;
接下来,顺着节点15访问下一个层索引,在该层节点中找到20;
顺着第1层索引的节点20向下,找到原始链表的节点20;
通过以上例子,假设原始链表有n个节点,那么索引的层级就是log(n)-1,在每一层的访问次数是常量,因此查找节点的平均时间复杂度是O(logn)。比起常规的查找方式,也就是线性一次访问链表节点的方式,效率要高很多。
跳表的增删
下面通过跳表的源码分析增删:
public class Node {
// 元素值
private int data;
// 元素的上下左右指针
// 为了方便理解 将左侧空节点设置为负无穷大,右侧空节点设置为正无穷大
private Node up;
private Node down;
private Node left;
private Node right;
public Node(int data){
this.data = data;
}
}
public class SkipList {
private Node head;
private Node tail;
private int maxLevel;
// 节点上升索引的概率
private static final double PROMOTE_RATE = 0.5;
public SkipList(){
Node max = new Node(Integer.MAX_VALUE);
Node min = new Node(Integer.MIN_VALUE);
head = min;
tail = max;
head.setRight(tail);
tail.setLeft(head);
maxLevel = 0;
}
/**
* 查找前置节点
* @param data
* @return
*/
public Node findNode(int data){
Node node = head;
while (true){
while (node.getRight().getData() != Integer.MAX_VALUE && node.getRight().getData() <= data){
node = node.getRight();
}
if(node.getDown() == null){
break;
}
node = node.getDown();
}
return node;
}
// ......
// 增删改查方法
}
新增元素
通过在原有跳表中新增元素12,如下过程:
首先找到前置节点11,接下来按照链表的插入方式,把节点12插入到11的下一个节点;
随着原始链表的新节点越来越多,索引会逐渐变得不够用了,因此索引节点也需要相应做出调整;
假如我们在50%的概率上,得出节点需要晋升,那么把节点12作为索引节点,插到第1层索引的对应位置,并且向下指向原始链表的节点12;
如果每层上升都成功,并且超出了最高索引的最高范围,那么新增一层索引。
public void insert(int data){
// 查找前置节点
Node preNode = findNode(data);
if(preNode != null && preNode.getData() == data){
return;
}
// 按照链表的插入方式,把节点插入到前置节点的下一个节点;
Node newNode = new Node(data);
appendNewNode(preNode,newNode);
int currentLevel = 0;
Random random = new Random();
// 晋升成功
while (random.nextDouble() < PROMOTE_RATE){
// 当前层数最大,多添加一层
if(currentLevel == maxLevel){
addLevel();
}
// 找到索引的前置节点
while (preNode.getUp() == null){
preNode = preNode.getLeft();
}
preNode = preNode.getUp();
// 索引插入数据
Node upperNode = new Node(data);
appendNewNode(preNode,upperNode);
upperNode.setDown(newNode);
newNode.setUp(upperNode);
newNode = upperNode;
currentLevel ++;
}
return;
}
public void appendNewNode(Node preNode,Node newNode){
newNode.setLeft(preNode);
preNode.getRight().setLeft(newNode);
newNode.setRight(preNode.getRight());
preNode.setRight(newNode);
}
public void addLevel(){
maxLevel++;
Node p1 = new Node(Integer.MIN_VALUE);
Node p2 = new Node(Integer.MAX_VALUE);
p1.setRight(p2);
p2.setLeft(p1);
p1.setDown(head);
p2.setDown(tail);
head.setUp(p1);
tail.setUp(tail);
head = p1;
tail = p2;
}
删除
跳表删除节点的过程,是相反的思路。
public boolean remove(int data){
// 找到要删除的元素
Node removeNode = search(data);
if(removeNode == null){
return false;
}
int currentLevel = 0;
while (removeNode != null){
removeNode.getRight().setLeft(removeNode.getLeft());
removeNode.getLeft().setRight(removeNode.getRight());
// 如果不是最底层,且只有无穷小和无穷大节点,删除该层
if(currentLevel != 0 && removeNode.getLeft().getData() == Integer.MIN_VALUE && removeNode.getRight().getData() == Integer.MAX_VALUE){
removeLevel(removeNode.getLeft());
}else {
currentLevel++;
}
removeNode = removeNode.getUp();
}
return true;
}
// 删除一层
private void removeLevel(Node leftNode){
Node rightNode = leftNode.getRight();
// 如果删除层是最高层
if(leftNode.getUp() == null){
leftNode.setDown(head);
leftNode.getDown().setUp(null);
rightNode.getDown().setUp(null);
}else {
leftNode.getUp().setDown(leftNode.getDown());
leftNode.getDown().setUp(leftNode.getUp());
rightNode.getUp().setDown(rightNode.getDown());
rightNode.getDown().setUp(rightNode.getUp());
}
maxLevel--;
}