场景
现在给定一个场景,给定一个有序的链表。如何根据元素的值进行高效查找?
有序,查找这不是二分查找法的要素嘛?
可是注意这里给出的数据结构是链表,而不是数组。所以不能使用二分查找法。
众所周知,链表是一种查慢,但是删除,插入快的数据结构。为了解决这个查询慢这个问题。衍生了今天的主角------跳跃链表。
介绍
这咋一看,看起来好像这个数据结构特别复杂。还有它又是怎么加快查找速率的呢?别急,先来给出他的定义。
定义
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
这又是一种用空间换时间的做法。
跳跃链表实现快速查找的目的,其实归根结底都是建立索引。提到索引就不得不提到目录。这个最好理解索引为何物的例子。
1. 引言 ......................................................................7
1.1 编写目的 .............................................................7
1.2 文档约定 .............................................................7
1.3 术语及缩写说明 ...........................................................7
1.4 预期的读者和阅读建议 ......................................................8
2. 系统程序结构 ................................................................9
2.1 应用架构 ..............................................................9
2.2 功能架构 ..............................................................9
2.3 服务架构 ..............................................................10
2.4 数据库架构 ..............................................................10
2.5 部署架构 ...............................................................11
2.6 方案架构示意图 .............................................................11
2.6.1 接口对接方案 ......................................................11
2.6.2 Data Integration模块架构 .......................................11
2.6.3 KPI Dashboard模块架构 ...........................................12
2.7 模块清单 ...................................................................12
目录其实也是一种对页码进行划分的索引。而且还可以通过划分层多级索引,来加快查找的速率。
跳跃链表其实十分类似于目录。最底层的链表存储着所有的数据,而从第二层开始到顶层代表着用于加速查找的索引。层级越高,对应的索引越高级。
查找
首先来介绍一下查找。看看跳跃链表是如何快速定位到目标元素的。比如现在我们要找到元素23
从顶部的头节点开始遍历。如果节点元素小于目标值,则向左移动。如果大于等于目标值则向下移动。就是同样的操作一步步下探到对应节点。
也就是 head --> 2 --> 2 --> 17 --> 17 --> 17 --> 23
复杂度分析
我们这里是每隔两个节点就建立一层索引。
假设原始链表有n个结点,那么索引的层级就是log(n),在每一层的访问次数是常量,因此查找结点的平均时间复杂度是O(logn)。
增加
插入这里最重要理解的点是概率晋升。我们现在来举例将10插入上图中。
首先进行元素插入,需要先找到待插入元素的前置节点(仅小于待插节点)。然后将该节点插入其中。
这样就完了吗?不是的,随着原始链表的新结点越来越多,索引会渐渐变得不够用了,因此索引结点也需要相应作出调整。
所以,需要新插入的节点按照一定的概率晋升成为索引节点。我们上图的跳跃节点链表中,底层和第二层是按照2:1的进行划分。所以概率晋升的概率也应该为50%。所以新节点有50%的概率晋升为索引节点。同时,如果成功晋升为索引节点,仍然有50%的机会升级为上一级的索引节点。如果晋升失败。则插入操作也随之完成。
注意。这里还有一种特殊情况。就是这个节点存在着一定概率,连续晋升到最顶层的索引链表,同时它又成功晋升,此时需要增加一层索引。
删除
删除操作则相对来说简单很多。
首先同样需要找到待删除元素所在节点。然后如果这个节点上头有索引节点,同样需要删除与之对应的索引节点。
这里同样也存在一种特殊情况。好比删除掉我们新增的节点10.最顶层的索引节点10也会被对应地删除掉。这时,顶层就不存在索引节点,所以需要把这层给清空,将顶层下沉。
代码实现
先要说明跳跃链表采用的是双向链表,节点拥有上下左右四个指针。节点之间彼此互相指向。
//链表结点类
public class Node {
public int data;
//跳表结点的前后和上下都有指针
public Node up, down, left, right;
public Node(int data) {
this.data = data;
}
}
程序中跳表的每一层首位各有一个空结点,左侧的空节点是负无穷大,右侧的空节点是正无穷大。
同时还需要维护一个头节点,尾结点,以及一个最大层数
//结点“晋升”的概率
private static final double PROMOTE_RATE = 0.5;
private Node head,tail;
private int maxLevel;
public SkipList() {
head = new Node(Integer.MIN_VALUE);
tail = new Node(Integer.MAX_VALUE);
head.right = tail;
tail.left = head;
}
查找节点
还记得我们说的查找节点,第一步找到对应前置节点
前置节点在这里指代两种情况,第一种就是直接找到对应元素的节点。第二种是找不到对应元素节点,只能找到它的前一个节点。
//找到值对应的前置结点
private Node findNode(int data){
Node node = head;
while(true){
// 这里属于同层,向左挪动
while (node.right.data!=Integer.MAX_VALUE && node.right.data<=data) {
node = node.right;
}
// node.down == null 表示当前已经是最底层链表,无法下探
if (node.down == null) {
break;
}
// 向下挪动
node = node.down;
}
return node;
}
当然后会存在对应节点的值,与我们查找的目标值并不相同的情况。所以还需进行比较
//查找结点
public Node search(int data){
Node p= findNode(data);
if(p.data == data){
return p;
}
return null;
}
增加节点
在前置结点后面添加新结点
private void appendNode(Node preNode, Node newNode){
newNode.left=preNode;
newNode.right=preNode.right;
preNode.right.left=newNode;
preNode.right=newNode;
}
增加层数
private void addLevel(){
maxLevel++;
Node p1=new Node(Integer.MIN_VALUE);
Node p2=new Node(Integer.MAX_VALUE);
p1.right=p2;
p2.left=p1;
p1.down=head;
head.up=p1;
p2.down=tail;
tail.up=p2;
head=p1;
tail=p2;
}
新增节点代码
public void insert(int data){
// 先找到这个节点对应的前置节点
Node preNode= findNode(data);
//如果data相同,直接返回
if (preNode.data == data) {
return;
}
Node node=new Node(data);
appendNode(preNode, node);
int currentLevel=0;
int currentMaxLevel = maxLevel;
//随机决定结点是否“晋升”
Random random = new Random();
// currentMaxLevel是为了确保 每次insert 理论最多只能 加 1层
while (random.nextDouble() < PROMOTE_RATE && currentLevel <= currentMaxLevel) {
//如果当前层已经是最高层,需要增加一层
if (currentLevel == maxLevel) {
addLevel();
}
//找到上一层的前置节点
while (preNode.up==null) {
preNode=preNode.left;
}
preNode=preNode.up;
//把“晋升”的新结点插入到上一层
Node upperNode = new Node(data);
appendNode(preNode, upperNode);
upperNode.down = node;
node.up = upperNode;
node = upperNode;
currentLevel++;
}
}
删除节点
删除层数代码
private void removeLevel(Node leftNode){
Node rightNode = leftNode.right;
//如果删除层是最高层
if(leftNode.up == null){
leftNode.down.up = null;
rightNode.down.up = null;
}else {
leftNode.up.down = leftNode.down;
leftNode.down.up = leftNode.up;
rightNode.up.down = rightNode.down;
rightNode.down.up = rightNode.up;
}
maxLevel --;
}
删除节点代码
public boolean remove(int data){
Node removedNode = search(data);
if(removedNode == null){
return false;
}
int currentLevel=0;
while (removedNode != null){
removedNode.right.left = removedNode.left;
removedNode.left.right = removedNode.right;
//如果不是最底层,且只有无穷小和无穷大结点,删除该层
if(currentLevel != 0 && removedNode.left.data == Integer.MIN_VALUE && removedNode.right.data == Integer.MAX_VALUE){
removeLevel(removedNode.left);
}else {
currentLevel ++;
}
removedNode = removedNode.up;
}
return true;
}
说明
此文章为查看程序员小灰的什么是 “跳表” 文章后,根据个人理解写的一篇文章。文章的代码以及对于跳跃链表的描述大部分来自于此篇文章, 图片为本人所画。