1.跳表的意义
我们都知道 对于一个有序(指的是元素值是升序或者降序的)的数组而言 二分法查询的最坏时间复杂度是O(logn)
我解释一下logn的由来:
比如现在我们有一个长度为8的数组 最坏情况就是分到最后长度为1的时候查询到元素 其中的过程为8 4 2 1 需要执行3次操作 即为log28 = 3 那么简化成复杂度就是O(logn)
然后对于一个有序的链表来说 我们还是需要从头到尾一个一个查询 那么最坏复杂度还是O(n) 但是跳表的出现优化了链表的查询操作
2.跳表的特点
1.一个跳表由若干层链表组成
2.每一层链表都是有序的
3.跳表最下面一层的链表包含了所有的数据
4.如果一个元素出现在了某一层级 那么该层级下面的所以层级都会包含该元素
5.当前层级的元素和指向下一层级的元素始终相同的
6.跳表最上面一层的头元素为head所指向
3.节点类的实现
private class Node<E>{
// 私有成员
private E val;// 节点值
private Node<E> next;// 水平方向上的下一节点
private Node<E> down;// 竖直方向上的下一节点
// 构造方法
public Node(E val, Node<E> next){
this.val = val;
this.next = next;
}
}
4.层级索引的随机生成
此方法致力于随机生成一个待插入的层级数
保证层级为1的概率为0.5 层级为2的概率为0.25(0.5 x 0.5) 层级为3的概率为0.125(0.5 x 0.5 x 0.5)……每一层级的概率是上一层级生成概率的一半
public int randLevel(){
int level = 1;
while(Math.random() < 0.5f && level < MAX_LEVEL)
level++;
return level;
}
5.插入方法
跳表插入的总体思路为:第一步 检查一下有无未创建的层级 如若有 那么就先创建 第二步 层级创建完毕以后 跳过无需获取前置节点的层级从待插入层级开始从上往下依次获取待插入节点的前置节点 存放入数组中的相应位置 第三步 从待插入层级开始 从上往下依次执行插入操作 最后需要更新记录总层级数的变量
public void add(E val){
// 首先随机生成一个待插入的层级数
int level = randLevel();
// 接着创建一个数组 用于存放前置节点
Node[] preNodes = new Node[level];
if(level > curLevelCounts){
// 保存一下旧的头结点
Node<E> beforeHead = head;
// 更新头结点
head = new Node<>(null, null);
// 接着定义一个遍历节点 初始化为头结点
Node<E> curHead = head;
for(int i = curLevelCounts; i < level - 1; ++i){
// 为每一个新层创建头结点
Node<E> node = new Node<>(null, null);
// 设置down属性
curHead.down = node;
curHead = node;
}
// 最后还要连接一下curHead和beforeHead之间的线
curHead.down = beforeHead;
}
// 定义一个遍历节点 用于遍历前置节点
Node<E> preNode = head;
// 过滤层级
for(int i = curLevelCounts; i > level; --i){
preNode = preNode.down;
}
// 获取必要层级的前置节点
for(int i = level - 1; i >= 0; --i){
while(preNode.next != null && preNode.next.val.compareTo(val) < 0){
preNode = preNode.next;
}
preNodes[i] = preNode;
preNode = preNode.down;
}
Node<E> topNode = null;
for(int i = level - 1; i >= 0; --i){
Node<E> node = new Node<>(val, preNodes[i].next);
preNodes[i].next = node;
if(topNode != null){
topNode.down = node;
}
topNode = node;
}
if(level > curLevelCounts)
curLevelCounts = level;
}
6.查询方法
该方法旨在查找是否存在指定值
总体思路:从跳表的最上层链表开始查询 先在当前层级从前往后查找 如果下一节点为空或者下一节点大于指定值的话 那么就前往下一层级寻找 否则就继续在当前层级查找 如果找到了 就返回true 没找到就返回false
然后确实有可能存在当前节点的节点值为空 然后执行if语句 那么将无法执行if语句中的代码 所以需要或上一个node.val == null才行
public boolean search(int target){
// 从头结点开始查找
Node<E> node = head;
while(node != null){
if(node.val == null || node.val.compareTo(target) < 0){
if(node.next == null){
node = node.down;
}else{
node = node.next.val > target ? node.down : node.next;
}
}else if(node.val.equals(target)){
return true;
}else{
return false;
}
}
return false;
}
7.删除方法
总体思路:首先找到待删除节点所处的最高层级 然后从最高层级开始往下一层一层执行待删除节点的删除操作 全部删除完毕以后 需要检查一下哪些是空链表 如若存在空链表 就需要更新总层级数和头结点
删除方法中的删除操作是从topIndex开始删除的 topIndex储存的是层级数-1
node.next.val.equals(val)必须写在node.next != null后面 如果反之就报错了
public boolean remove(E val){
// 定义一个遍历节点 初始化为头结点
Node<E> node = head;
int topIndex = -1;
for(int i = curLevelCounts - 1; i >= ){
while(node.next != null && node.next.val.compareTo(val) < 0)
node = node.next;
if(node.next != null && node.next.val.equals(val) && topIndex == -1){
topIndex = i;
break;
}
if(node.down == null)
return false;
node = node.down;
}
if(topIndex == -1)
return false;
for(int i = topIndex; i >= 0; --i){
if(node.next != null)
node.next = node.next.next;
// 更新遍历节点为下一层级的前置节点
node = node.down;
if(node != null){
while(node.next != null && !node.next.val.equals(val)){
node = node.next;
}
}
}
// 删除那些已被清空的链表
while(curLevelCounts > 1 && head.next == null){
curLevelCounts--;
head = head.down;
}
// 执行到此 才算真正的删除成功
return true;
}
8.整体代码
由于待会需要进行节点值的大小比较 所以说需要让泛型继承自Comparable类才行 这样才能调用该类中的compareTo方法进行泛型的大小比较
public class SkipLinkedList<E extends Comparable<E>>{
// 私有成员
private Node<E> head;// 头节点
private int MAX_LEVEL = 16;// 最大的待插入层级
private int curLevelCounts;// 当前的总层级数
// 构造方法
public SkipLinkedList(){
// 用于初始化头节点
head = new Node<>(null, null);
}
// 节点类
private class Node<E>{
// 私有成员
private E val;// 节点值
private Node<E> next;// 水平方向上的下一个节点
private Node<E> down;// 竖直方向上的下一个节点
// 构造方法
public Node(E val, Node<E> next){
this.val = val;
this.next = next;
}
}
// 随机获取层级数的方法
private int randLevel(){
// 首先设置初始值
int level = 1;
// 然后设置每一层的概率是上一层的一半 比如获取层级2的概率是获取层级1概率的一半 获取层级3的概率是获取层级2概率的一半 以此类推
while(Math.random() < 0.5f && level < MAX_LEVEL){
level++;
}
return level;
}
// 用于往跳表增加元素的方法
public void add(E val){
// 分成三步 第一步.如果在插入之前有些层级还为创建的话 那么就优先创建这些待创建的层级 第二步.待创建完毕所有的层级以后 从待插入的层级开始至上而下依次获取待插入节点的前置节点 第三步.就是从待插入层级开始自上而下依次将待插入节点插入到指定位置处
// 首先先获取待插入的层级
int level = randLevel();
// 接着创建一个数组 用于存放前置节点
Node[] preNodes = new Node[level];
if(level > curLevelCounts){
Node<E> beforeHead = head;
head = new Node<>(null, null);
Node<E> curHead = head;// 这个变量旨在遍历新创建层级的头节点
for(int i = curLevelCounts; i < level - 1; i++){
Node<E> node = new Node<>(null, null);
curHead.down = node;
curHead = node;
}
// 最后设置curHead.down为beforeHead
curHead.down = beforeHead;
}
// 定义一个遍历节点 初始化为头节点
Node<E> preNode = head;
// 在执行第二步之前 还需要将那些无需执行前置节点获取的层级跳过才行
for(int i = curLevelCounts - 1; i >= level; i--){
preNode = preNode.down;
}
for(int i = level - 1; i >= 0; i--){
while(preNode.next != null && preNode.next.val.compareTo(val) < 0){
preNode = preNode.next;
}
preNodes[i] = preNode;
preNode = preNode.down;
}
// 最后一步就是将待插入节点从待插入层级自上而下逐层进行插入操作 插入其实就是连接三条线 一条是前置节点指向待插入节点的 一条是待插入节点指向后置节点的 一条是上置节点指向待插入节点的
Node<E> topNode = null;
for(int i = level - 1; i >= 0; i--){
Node<E> node = new Node<>(val, preNodes[i].next);
preNodes[i].next = node;
if(topNode != null){
topNode.down = preNodes[i];
}
topNode = node;
}
// 最后 如果有必要的话 还需要更新一下curLevelCounts的值
if(level > curLevelCounts)
curLevelCounts = level;
}
// 用于判断是否存在指定值 如果存在的话 那么就返回true 反之则返回false
public boolean search(E target){
// 首先定义一个遍历节点 初始化为头节点
Node<E> node = head;
while(node != null){
// 如果当前节点的值小于指定值的话
if(node.val == null || node.val.compareTo(target) < 0){
// 如果下一节点的值为空的话 那么就直接更新为down节点
if(node.next == null){
node = node.down;
}else{
node = (node.next.val.compareTo(target) > 0) ? node.down : node.next;
}
}else if(node.val.equals(target)){
return true;
}else{
return false;
}
}
// 如果找寻完所有的节点以后 依然没有返回值的话 那么就说明指定值没有找到 所以直接返回false
return false;
}
// 用于删除指定的节点 并且判断是否删除成功
public boolean remove(E val){
// 首先找到指定值所处的最高层级
Node<E> node = head;
int topIndex = -1;// topIndex记录的是层级数-1
for(int i = curLevelCounts - 1; i >= 0; i--){
// 首先一路向右找
while(node.next != null && node.next.val.compareTo(val) < 0){
node = node.next;
}
// 如果找到的话 那么就更新topIndex 并且退出循环
if(topIndex == -1 && node.next != null && node.next.val.equals(val)){
topIndex = i;
break;
}
// 如果当前层没有找到的话 那么就往下一层继续找 先判断一下是否存在下一层 如果不存在的话 说明找不到指定值 直接返回false
if(node.down == null)
return false;
node = node.down;
}
// 那既然指定值的最高层级已经找打的话 那么现在就开始执行删除操作
// 如果找完以后topIndex==-1的话 那么说明没有找到指定值
if(topIndex == -1)
return false;
// 从topIndex层开始往下进行删除操作
for(int i = topIndex; i >= 0; i--){
if(node.next != null){
node.next = node.next.next;
}
// 接下来更新node为下一层待删除节点的前置节点
// 先将node更新为down节点
node = node.down;
// 因为这是在移动到down元素以后进行的更新next操作 所以说我们需要对node进行判空操作 然后找到下一层的待删除节点的前置节点
if(node != null) {
while (node.next != null && !node.next.val.equals(val)) {
node = node.next;
}
}
}
// 我们要将已经被清空的链表彻底清空 彻底清空靠的是更新head节点和curLevelCounts两个变量 但是需要保证的是最后一层的节点不要被清空就行了
while(curLevelCounts > 1 && head.next == null){
head = head.down;
curLevelCounts--;
}
// 执行到最后肯定是说明该删除的元素已经被我们成功删除了
return true;
}
// 用于打印跳表
@Override
public String toString(){
// 用于打印跳表
StringBuilder sb = new StringBuilder();
// 首先设置一个遍历节点
Node<E> cur = head;
while(cur != null){
// 接着设置一个tmp节点
Node<E> tmp = cur.next;
while(tmp != null){
sb.append(tmp.val).append(", ");
tmp = tmp.next;
}
cur = cur.down;
if(cur != null)
sb.append("\n");
}
return sb.toString();
}
}
9.测试
public static void main(String[] args) {
SkipLinkedList<Integer> sll = new SkipLinkedList<>();
sll.add(1);
sll.add(2);
sll.add(3);
sll.add(4);
sll.add(5);
System.out.println(sll);
sll.remove(3);
System.out.println(sll);
System.out.println(sll.search(6));
}