引入
程序员小灰的公众号(风趣的漫画&&数据结构和算法) https://mp.weixin.qq.com/s/Ok0laJMn4_OzL-LxPTHawQ
学习过程
问题描述
有序数组查找定值=>有序链表查找目标结点
基本思路
类似查字典时用的目录,先用目录确定起始页码,再翻到字典页做最后的查找。
逻辑图解
基本结构
用双向链表来组成链表网络
操作思路
查找结点
(1)从最上层索引查找,找到左侧最接近的索引结点,到下一层结点重复查找步骤,一直定位到结点链表;
(2)最后找到的也许是目标结点;也可能目标结点不存在,找到的是目标结点的前驱结点。
插入结点
在找到前驱结点后,要把新结点插入到前驱结点后;到这还没完事,别忘了维护所涉及的索引层,暂时用Random随机数决定是否晋升为索引结点。
(1)以极端情况——新结点一路运气爆棚,每层索引都晋升,甚至不得不新加一层索引… 为例
删除结点
与插入结点的思路类似,删除结点后要记得维护索引哦~
代码实现
结点定义
/**
* 结点定义
*
* @version
* @Description
* @author xps
* @date: Sep 4, 2020
*/
class Node {
// 上下左右结点
Node up, down, left, right;
// 存的值
int val;
public Node(int val) {
super();
this.val = val;
}
}
跳表定义
/**
* 跳表定义
*
* @version
* @Description
* @author xps
* @date: Sep 4, 2020
*/
class SkipList {
// 跳表的比较头
Node head = new Node(Integer.MIN_VALUE);
// 跳表的比较尾
Node tail = new Node(Integer.MAX_VALUE);
// 理论最高层数
int maxLevel = 0;
// 实际最高层数
int actMaxLevel = 0;
public SkipList() {
super();
head.right = tail;
tail.left = head;
}
/**
* 增加新结点
*
* @param data 添加结点的数值大小
*/
public void add(int data) {
// 先找到前置结点
Node frontNode = find(data);
// 防止结点重复
if (data == frontNode.val) {
return;
}
// 插入新结点到前置结点后面
insert(frontNode, new Node(data));
maxLevel++;
System.out.println("刚插入新结点" + data);
printNode(head);
// 遍历每层索引,随机决定是否在当前层索引添加新结点
Random random = new Random();
// 此次添加索引的最高层数(新增一个及结点,最多新加一层索引)
int highLevel = actMaxLevel >= maxLevel ? maxLevel : (actMaxLevel + 1);
for (int i = 1; i <= highLevel; i++) {
boolean flag = random.nextBoolean();
if (flag) {
// 如果要新加一层索引
if (i - actMaxLevel == 1) {
addNewLevel();
}
// 要找到当前链表(新插入结点)对应索引的左结点
Node leftNode = frontNode;
while (leftNode.up == null) {
leftNode = leftNode.left;
}
// 上层索引的前置结点
Node newFrontNode = leftNode.up;
// 上层的新结点
Node n = new Node(data);
// 上层索引执行同样的插入操作
insert(newFrontNode, n);
// 上层索引新增结点 和 下层结点关联
n.down = frontNode.right;
frontNode.right.up = n;
frontNode = newFrontNode;
} else {
// 当前索引层不添加新结点,则后续索引层也不会添加新索引
break;
}
}
}
/**
* 删除结点
*
* @param data 要删除结点的值
*/
public void delete(int data) {
// 仍然是先找到前置结点
Node frontNode = find(data);
if (data != frontNode.right.val) {
System.out.println("未找到结点" + data);
return;
}
// 删除目标结点
remove(frontNode.right);
System.out.println("单纯删除结点后");
printNode(head);
maxLevel--;
// 最底层索引的head
Node firstHead = head;
while (firstHead.down != null) {
firstHead = firstHead.down;
}
firstHead = firstHead.up;
// 如果没有索引层
if (firstHead == null) {
// actMaxLevel = 0;
return;
}
// 最底层索引上一层的head
Node secondHead = firstHead.up;
// 上两层索引一致的标志
boolean equalFlag = true;
// 如果只剩一层索引,肯定不会索引重复
if (secondHead == null) {
equalFlag = false;
}
while (equalFlag && firstHead.right != null) {
int firstVal = firstHead.right.val;
int secondVal = secondHead.right.val;
if (firstVal == secondVal) {
firstHead = firstHead.right;
secondHead = secondHead.right;
} else {
equalFlag = false;
break;
}
}
// 如果索引一致,则下层索引替换上层索引
if (equalFlag) {
// 此时两层索引都走到了最右(尾)
// 若重复的是顶层索引,直接让次层索引晋升为顶级索引
boolean repeatFlag = false;
if (secondHead.up == null) {
tail = firstHead;
firstHead = firstHead.left;
while (firstHead.left != null) {
firstHead.up = null;
firstHead = firstHead.left;
}
head = firstHead;
repeatFlag = true;
}
if (!repeatFlag) {
while (secondHead != null) {
if (secondHead.up != null) {
secondHead.up.down = firstHead;
firstHead.up = secondHead.up;
}
secondHead = secondHead.left;
firstHead = firstHead.left;
}
}
actMaxLevel--;
}
// 若索引没有存数据结点,则删除这层索引;防止删掉底层链表
while (head.right.val == Integer.MAX_VALUE && head.down != null) {
Node newHead = head.down;
Node newTail = tail.down;
newHead.up = null;
newTail.up = null;
head = newHead;
tail = newTail;
actMaxLevel--;
}
}
/**
* 查找前置结点
*
* @param data 数值
* @return 查找数值的前置结点
*/
public Node find(int data) {
// 从当前索引头开始查找
Node a = head;
if (data <= Integer.MIN_VALUE || data >= Integer.MAX_VALUE) {
System.out.println("越界");
return null;
}
while (true) {
while (data > a.right.val) {
// 结点右移
a = a.right;
}
if (a.down != null) {
a = a.down;
} else {
// 没有下层结点,表示到了链表层
break;
}
}
return a;
}
/**
* 插入结点
*
* @param frontNode 前置结点
* @param newNode 新结点
*/
public void insert(Node frontNode, Node newNode) {
// 右侧结点双向连接
frontNode.right.left = newNode;
newNode.right = frontNode.right;
// 右侧结点双向连接
frontNode.right = newNode;
newNode.left = frontNode;
}
/**
* 打印当前层结点
*
* @param head 一层的头结点
*/
public void printNode(Node head) {
Node a = head;
while (a != null) {
System.out.print(a.val + "\t");
a = a.right;
}
System.out.println();
if (head.down != null) {
printNode(head.down);
}
}
/**
* 删除目标结点
*
* @param node 要删除的目标结点
*/
public void remove(Node node) {
// 前置结点 后置结点直接关联
Node leftNode = node.left;
Node rightNode = node.right;
leftNode.right = rightNode;
rightNode.left = leftNode;
// 如果上层索引有这个结点的索引,也要删除
if (node.up != null) {
remove(node.up);
}
}
/**
* 新增一层索引
*
*/
public void addNewLevel() {
Node newHead = new Node(Integer.MIN_VALUE);
Node newTail = new Node(Integer.MAX_VALUE);
newHead.right = newTail;
newTail.left = newHead;
newHead.down = head;
newTail.down = tail;
head.up = newHead;
tail.up = newTail;
// 重置整个链表的索引头和索引尾
head = newHead;
tail = newTail;
actMaxLevel++;
}
}
测试
public class TestSkipList {
public static void main(String[] args) {
SkipList list = new SkipList();
list.add(50);
list.add(15);
list.add(13);
list.add(20);
System.out.println("删除前:");
list.printNode(list.head);
list.delete(20);
System.out.println("删除20后");
list.printNode(list.head);
list.delete(50);
System.out.println("删除50后");
list.printNode(list.head);
list.delete(15);
System.out.println("删除15后");
list.printNode(list.head);
list.add(100);
System.out.println("添加100后");
list.printNode(list.head);
}
}
打印结果
总结
数组的二分查找是建立在下标已知的基础上,链表本身查找元素困难,为了类比数组的查找,给链表添加索引,目标结点和索引结点比较,缩小范围。(空间换时间)
扩展
据说redis的底层实现也用到了跳表,那有空得学习。
后记
断断续续的两天,才写完这篇博客,幸亏那时的草稿本还在,不然重新画图又要花费不少时间;csdn的markdown编辑器倒是还挺全面的,我一眼注意到了那个数学公式,点进去跳到了可汗学院的项目…看来以后写公式不用粘图片了;最后呢,分享一句伟大科学家牛顿的一句话,“用多重又混杂的方式发现不了真理,简简单单才是正确的打开方式”(Truth is ever to be found in simplicity, and not in the multiplicity and confusion of things)