数据结构之跳表
Posted 前端手艺人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之跳表相关的知识,希望对你有一定的参考价值。
什么是跳表
链表加多级索引。
跳表能够实现二分查找的效率。
代码实现
图1:
注意:
1 节点1虽然看着是有三个,本质上是只有一个。为了便于看三个层级的关系,才多画了两个。(也是很多网上的图示没解释清楚)
2 层级之间的跳跃是通过节点里的数组(也就是forward)存放的指针,forward[0]是1层头指针,forward[1]是2层头指针...(很多书上都会有一个down指针,导致理解偏差)
java:
package skiplist;/**只处理了整数,且无重复元素**/public class SkipList {
private static final int MAX_LEVEL = 16;
// 当前的索引层级
private int levelCount = 1;
// 头链表: forwards分别指向每一个层级的头:插入的时候会自动更新
private Node head = new Node();
private Random r = new Random();
public void printAll() {
Node p =head;
while(p.forwards[0] != null) {
if(i==0) {
System.out.println("头节点的forward指向");
}else {
System.out.println("第"+i+"个节点的forward指向");
}
System.out.print(p.forwards[0] + " ");
if(p.forwards[1] != null) {
System.out.print(p.forwards[1] + " ");
}
if(p.forwards[2]!= null) {
System.out.print(p.forwards[2] + " ");
}
if(p.forwards[3]!= null) {
System.out.print(p.forwards[3] + " ");
}
if(p.forwards[4]!= null) {
System.out.print(p.forwards[4] + " ");
}
if(p.forwards[5]!= null) {
System.out.print(p.forwards[5] + " ");
}
if(p.forwards[6]!= null) {
System.out.print(p.forwards[6] + " ");
}
p = p.forwards[0];
i++;
System.out.println();
}
System.out.println();
}
// 结合图1看
public Node find(int value) {
Node p = head;
// 注意这里的forwards是next p还是指向自己
for(int i =levelCount-1; i>=0; i--){
while(p.forwards[i]!=null && p.forwards[i].data<value) {
p = p.forwards[i];
}
}
// 找得节点一定是在原始链表的那一层(逻辑上有多层,本质上节点只有一层)。所以forwards[0](包含了所有的节点)
if(p.forwards[0]!=null && p.forwards[0].data==value) {
return p.forwards[0];
}else {
return null;
}
}
public void insert(int value) {
// 通过随机函数,插入部分索引层级,维持跳表的平衡性(避免两个节点之间的没有索引,导致查找退化为链表,时间复杂度变为O(n))
int level = randomLevel();
// 注意每一个索引层相同的节点,其实是同一个节点。
// 所以这里新节点只有一个,而不是level个
Node newNode = new Node();
newNode.data = value;
// 记录该节点的所在最大层级
newNode.maxLevel = level;
// 创建了4个node内存,保存了4个节点
Node update[] = new Node[level];
for(int i=0;i<level;i++) {
update[i]=head;
}
Node p = head;
// level必须减1 因为我们的forwards是0对应层级1
for(int i=level-1; i>=0; i--) {
while(p.forwards[i]!=null && p.forwards[i].data < value) {
p = p.forwards[i];
}
// 指向要插入位置的前一个节点(第一次插入的时候,就是head节点)
update[i] = p;
}
// update[i]是要插入位置的上一个节点
// 修改node在每个层级的指针指向
for(int i =0; i<level; i++) {
newNode.forwards[i] = update[i].forwards[i];
// 注意: 当插入该层第一个元素的时候,会更新头结点(update[i]就是头结点)
update[i].forwards[i] = newNode;
}
// 更新当前的索引层级
if (levelCount < level) levelCount = level;
}
public void delete(int value) {
// 每一级一个update
Node[] update = new Node[levelCount];
Node p = head;
// 从最顶索引往下遍历,update指向上一个位置
for(int i=levelCount-1;i>=0;i--) {
while(p.forwards[i] != null && p.forwards[i].data < value) {
p = p.forwards[i];
}
update[i] = p;
}
if(p.forwards[0]!=null && p.forwards[0].data == value) {
for(int i =levelCount-1; i>=0; i--) {
if(update[i].forwards[i] != null && update[i].forwards[i].data == value) {
update[i].forwards[i] = update[i].forwards[i].forwards[i];
}
}
}
}
private int randomLevel() {
int level = 1;
for(int i=1; i < MAX_LEVEL;i++) {
if(r.nextInt()%2 == 1) {
level++;
}
}
return level;
}
public class Node {
private int data = -1;
// 注意几点: 最大层级包含原始链表,原始链表为层级1
// p.forwards[层级值] 当前层级的前向指针:forwards[层级值]相当于next
// p.forwards[层级值-1] 指向下一层级的指针: forwards[层级值-1]相当于down
private Node forwards[] = new Node[MAX_LEVEL];
private int maxLevel = 0;
//用于打印节点
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{ data: ");
builder.append(data);
builder.append("; levels: ");
builder.append(maxLevel);
builder.append(" }");
return builder.toString();
}
}}
const MAX_LEVEL = 4;function Node() {
this.forwards = new Array(MAX_LEVEL);
this.data = null;
this.maxlevel = 0;}function SkipList() {
this.levelCount = 1;
this.head = new Node();}SkipList.prototype.find = function(value) {
var p = this.head;
for (var i = this.levelCount - 1; i >= 0; i--) {
while (p.forwards[i] !== null && p.forwards[i].data < value) {
p = p.forwards[i];
}
}
if (p.forwards[0] != null && p.forwards[0].data == value) {
return p.forwards[0];
} else {
return null;
}};SkipList.prototype.insert = function(value) {
var level = Math.floor(Math.random() * MAX_LEVEL + 1);
var newNode = new Node();
newNode.data = value;
newNode.maxlevel = level;
// js数组是动态创建内存的
var update = [];
for (var i = 0; i < level; i++) {
update[i] = this.head;
}
var p = this.head;
for (var i = level - 1; i >= 0; i--) {
while (p.forwards[i] != null && p.forwards[i].data < value) {
p = p.forwards[i];
}
update[i] = p;
}
// console.log(JSON.stringify(update));
for (var i = 0; i < level; i++) {
newNode.forwards[i] = update[i].forwards[i];
update[i].forwards[i] = newNode;
}
if (this.levelCount < this.level) this.levelCount = this.level;};SkipList.prototype.delete = function(value) {
var update = [];
var p = head;
for (var i = this.levelCount - 1; i >= 0; i--) {
while (p.forwards[i] != null && p.forwards[i].data < value) {
p = p.forwards[i];
}
update[i] = p;
}
if (p.forwards[0] != null && p.forwards[0].data == value) {
for (var i = this.levelCount - 1; i >= 0; i--) {
if (
update[i].forwards[i] != null &&
update[i].forwards[i].data == value
) {
update[i].forwards[i] = update[i].forwards[i].forwards[i];
}
}
}};SkipList.prototype.printAll = function() {
var p = this.head;
console.log("print all....");
while (p.forwards[0] != null) {
console.log(JSON.stringify(p.forwards[0], null, 4) + " ");
p = p.forwards[0];
}};var a = new SkipList();a.insert(123);a.insert("aaa");a.printAll();var result = a.find(123);console.log("打印结果");JSON.stringify(result, null, 4);
查询时间复杂度
O(logn)
每两个节点抽出一个节点作为上一层索引的节点,假设最高级的索引有两个节点,n/2^k=2 => 推出k=logn-1 索引的层数,加上链表这一层,高度为logn,而每一层最多遍历三个节点,则时间复杂度为3*logn
空间复杂度
n/2+n/4+n/8+...+2=n-2 =》 O(n)
如果每三个抽出一个节点,则空间复杂度会更少。 在实际情况中, 链表中可能存的是一个大对象,而索引通常只存储关键值和几个指针。
支持快速插入和删除
很显然,查找的时间复杂度是O(logn) 链表中删除和插入操作时间复杂度都是O(1)
根据乘法法则,时间复杂度为O(logn)
跳表索引动态更新
如果不断的插入,而不更新索引,则跳表可能退化成链表
通过随机函数,决定节点同时插入到哪几级索引中
作业
为什么Redis要用跳表而不是红黑树来实现有序集合?
首先我们要看有序集合支持哪几个操作
1 插入
2 删除
3 查找
4 根据区间查找数据->红黑树的效率没有跳表高
5 迭代输出有序区间
很多编程语言中的Map都是红黑树实现,跳表需要自己实现
作业
如果每三个抽出一个节点,时间复杂度是多少?
点击阅读原文,关注作者可以获得持续的后续文章更新。
以上是关于数据结构之跳表的主要内容,如果未能解决你的问题,请参考以下文章