跳表java实现(可直接运行)
跳表类
package com.yjz.example.跳表;
/**
* @author: yuanjinzhong
* @date: 2023/1/28 3:00 PM
* @description:
* 跳表类,参考项目:https://github.com/wangzheng0822/algo/blob/b2c1228ff915287ad7ebeae4355fa26854ea1557/java/17_skiplist/SkipList2.java#L119
* <li>跳表里面每个值只有一个节点,每个节点含有该节点在每一层的下一个节点
* <p><img width="850" height="250" src="./image/跳表的最好的表现方式.png">
*/
public class SkipList {
/** 晋升成为索引的概率 ---新插入的节点有{@linkplain SKIP_LIST_P}的概率成为索引; redis设置的是0.25的概率晋升为索引 */
public static final double SKIP_LIST_P = 0.5;
/** 跳表的最大高度,包括原始skipList */
private static final int MAX_LEVEL = 8;
/** 跳表的当前高度 */
private int levelCount = 1;
/** 头节点,每个skipList都需要一个头节点,但是打印的时候不会打印出来 */
public Node head = new Node(MAX_LEVEL);
/**
* 插入元素
*
* @param e
* @return
*/
public void insert(int e) {
int level = head.nextInEachLayer[0] == null ? 1 : randomLevel();
/**
* 一层一层的增加索引,
*
* <p>若随机的层数比当前最大值大(或者大很多),那么只向上增加一层索引;
*
* <p>不然可能出现,其他节点都在同一层,这个新插入的索引,单独七八层
*
* <p>不过可以不加,看个人理解
*/
if (level > levelCount) {
/** 俩种策略 */
level = ++levelCount;
// levelCount=level;
}
Node newNode = new Node(level);
newNode.data = e;
Node p = head;
for (int i = levelCount - 1; i >= 0; i--) {
/**
* 为啥不比较p.data?因为p是头节点啊,头节点没有值;所以比较:p.nextInEachLayer[i].data
*
* <p>p.nextInEachLayer[i].data其实就是第一个真实的节点
*/
while (p.nextInEachLayer[i] != null && p.nextInEachLayer[i].data < e) {
p = p.nextInEachLayer[i];
}
/**
* i=levelCount-1,levelCount会大于当前随机出来的level;也就是i会大于level
*
* <p>新插入的节点只有level层,高于该层的层数肯定不需要维护后继指针
*/
if (level > i) {
/**
* 插入值,就插在{@link p和p的后继 }之间
*
* <p>有的实现还判断p.nextInEachLayer[i]是否为空,其实大可不必
*/
Node temp = p.nextInEachLayer[i];
p.nextInEachLayer[i] = newNode;
newNode.nextInEachLayer[i] = temp;
}
}
}
/**
* 查找
*
* @param e
* @return
*/
public Node find(int e) {
Node p = head;
// 从最高层开始查找
for (int i = levelCount - 1; i >= 0; i--) {
while (p.nextInEachLayer[i] != null && p.nextInEachLayer[i].data < e) {
p = p.nextInEachLayer[i];
System.out.println("查找路径" + p);
}
}
System.out.println("最终确定的节点" + p);
// 肯定是到跳表最下面一层查找
if (p.nextInEachLayer[0] != null && p.nextInEachLayer[0].data == e) {
return p.nextInEachLayer[0];
}
return null;
}
/**
* 删除元素
*
* @param e
*/
public void delete(int e) {
Node p = head;
Node[] preNodes = new Node[levelCount];
for (int i = levelCount - 1; i >= 0; i--) {
while (p.nextInEachLayer[i] != null && p.nextInEachLayer[i].data < e) {
p = p.nextInEachLayer[i];
}
// 将目标节点的每一层前驱节点保存下来(这个层数多了,下面数值相等判断过滤了)
preNodes[i] = p;
}
if (p.nextInEachLayer[0] != null && p.nextInEachLayer[0].data == e) {
for (int i = levelCount - 1 - 1; i >= 0; i--) {
if (preNodes[i].nextInEachLayer[i] != null && preNodes[i].nextInEachLayer[i].data == e) {
preNodes[i].nextInEachLayer[i] = preNodes[i].nextInEachLayer[i].nextInEachLayer[i];
}
}
}
// 悬浮在上方的空引用删除
while (levelCount > 1 && head.nextInEachLayer[levelCount] == null) {
levelCount--;
}
}
/**
* 期望建立的索引结构,
*
* <p>正金字塔结构 每一层是下一层的1/2的元素数量
*
* <p>最下面一层,完整的链表 最下面第二层(1级索引),1/2的节点做索引 最下面第二层(2级索引),1/2的1/2节点做索引
*
* <p>新加入一个节点,则有1/2的可能是1级索引 有1/4的可能是2级索引 有1/8的可能是3级索引 (是2级索引的同时肯定也是1级索引,3级索引的同时肯定也是1级和2级索引)
*
* <p>该方法会随机生成 1~MAX_LEVEL 之间的数(MAX_LEVEL表示索引的最高层数),
*
* <p>且该方法有 1/2 的概率返回 1、 1/4 的概率返回 2、 1/8的概率返回 3,以此类推
*
* <p>1,2,3,4 不是对应 1,2,3,4级索引,需要减1,才等于索引的级别
* <li>randomLevel() 方法返回 1 表示当前插入的该元素不需要建索引,只需要存储数据到原始链表即可(概率 1/2)
* <li>randomLevel() 方法返回 2 表示当前插入的该元素需要建一级索引(概率 1/4)
* <li>randomLevel() 方法返回 3 表示当前插入的该元素需要建二级索引(概率 1/8)
* <li>randomLevel() 方法返回 4 表示当前插入的该元素需要建三级索引(概率 1/16)
* <li>以此类推。。。
*
* <p>
* <li>返回1不建立索引,大于1则必定会建一级索引,大于1的概率为1-1/2=二分之一,
* <li>大于2必然会创建二级索引,大于2的概率为1-1/2-1/4=四分之1
* <li>类推
*
* @return
*/
public int randomLevel() {
int level = 1;
/** 随机数连续2次小于1/2才会使得level+1(第一次加1),即level+1(第一次加1)的概率为1/2乘以1/2= 1/4) */
// 当 level < MAX_LEVEL,且随机数小于设定的晋升概率时,level + 1
while (Math.random() < SKIP_LIST_P && level < MAX_LEVEL) {
level += 1;
}
return level;
}
public void printAll() {
Node p = head;
while (p.nextInEachLayer[0] != null) {
System.out.print(p.nextInEachLayer[0] + " ");
p = p.nextInEachLayer[0];
}
System.out.println();
}
public static void main(String[] args) {
SkipList skipList = new SkipList();
skipList.insert(3);
skipList.insert(1);
skipList.insert(2);
skipList.insert(6);
skipList.insert(9);
skipList.insert(8);
skipList.printAll();
System.out.println("跳表的当前高度:" + skipList.levelCount);
System.out.println("查找到的结果" + skipList.find(6));
System.out.println("++++++++++++++++删除节点之后打印数据++++++++++++++++++");
skipList.delete(6);
skipList.printAll();
}
}
节点类
package com.yjz.example.跳表;
/**
* @author: yuanjinzhong
* @date: 2023/1/28 2:49 PM
* @description:
* <p>跳表节点,存储正整数
* <p>每个值只有一个node节点
* <p>如何表示层级关系呢?
* <p>使用{@linkplain nextInEachLayer[]}来表示该节点在不同层级的后驱节点
*/
public class Node {
// 数据域,默认-1,因为想存储正整数嘛
int data = -1;
/**
* 普通的链表使用next或者pre指针指向前驱和后继
*
* <p>这里使用{@linkplain nextInEachLayer[]}来表示该节点在不同层级的后驱节点
*
* <p>数组下标表示层级
*/
Node nextInEachLayer[];
// 该节点的最大层级
int maxLevelOfThisNode;
/**
* 新键节点的时候,就已经知道该节点有几层,就知道该节点需要维护几层的后驱节点
*
* @param level
*/
Node(int level) {
this.maxLevelOfThisNode = level;
this.nextInEachLayer = new Node[level];
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{ data: ");
builder.append(data);
builder.append("; levels: ");
builder.append(maxLevelOfThisNode);
builder.append(" }");
return builder.toString();
}
}
基础决定深度啊!