java单链表的实现和原理_跳跃表的原理和实现(Java)

一、高效查找算法

我们在实际开发中经常会有在一堆数据中查找一个指定数据的需求,而常用的支持高效查找算法的实现方式有以下几种:

有序数组:这种方式的存储结构,优点是支持数据的随机访问,并且可以采用二分查找算法降低查找操作的复杂度。缺点同样很明显,插入和删除数据时,为了保持元素的有序性,需要进行大量的移动数据的操作。

二叉查找树:如果需要一个既支持高效的二分查找算法,又能快速的进行插入和删除操作的数据结构,那首先就是二叉查找树莫属了。缺点是在某些极端情况下,二叉查找树有可能变成一个线性链表。

平衡二叉树:二叉树表示不服,于是基于二叉查找树的优点,对其缺点进行改进,引入了平衡的概念。根据平衡算法的不同,具体实现有AVL树、B树(B-Tree)、B+树(B+Tree)、红黑树 等等。但是平衡二叉树的实现多数比较复杂,较难理解。

跳跃表:同样支持对数据进行高效的查找,插入和删除数据操作也比较简单,最重要的就是实现比较平衡二叉树真是轻量几个数量级。缺点就是存在一定数据冗余。

二、跳跃表

跳跃表(SkipList)是一种可以替代平衡树的数据结构。跳跃表让已排序的数据分布在多层次的链表结构中,默认是将 Key 值升序排列的,以 0-1 的随机值决定一个数据是否能够攀升到高层次的链表中。它通过容许一定的数据冗余,达到 “以空间换时间” 的目的。

跳跃表的效率和 AVL 相媲美,查找、添加、插入、删除操作都能够在 O(LogN) 的复杂度内完成。讲了那么多,下面就直接进入主题,详细的看一看跳跃表是怎么实现的。

三、跳跃表的实现

168423ad0ab9

上面这张图就是一个跳跃表的实例,先说一下跳跃表的构造特征:

一个跳跃表应该有若干个层(Level)链表组成;

跳跃表中最底层的链表包含所有数据; 每一层链表中的数据都是有序的;

如果一个元素 X 出现在第i层,那么编号比 i 小的层都包含元素 X;

第 i 层的元素通过一个指针指向下一层拥有相同值的元素;

在每一层中,-∞ 和 +∞ 两个元素都出现(分别表示 INT_MIN 和 INT_MAX);

头指针(head)指向最高一层的第一个元素;

1、定义链表中节点的模型

168423ad0ab9

image.png

Java 代码实现如下:

public class SkipListEntry {

// data

public Integer key;

public T value;

// links

public SkipListEntry up;

public SkipListEntry down;

public SkipListEntry left;

public SkipListEntry right;

// constructor

public SkipListEntry(Integer key, T value) {

this.key = key;

this.value = value;

}

// methods...

}

可以看到节点模型主要分为2个部分。

data 部分包含具体的存储数据,这里为了不引入其他杂乱的问题,使用 Integer 作为 key 的类型,Object 作为value 的类型。

links 部分包含4个指针,分别是 up、down、left、right,单从名字上就能够明白它们的作用。

2、跳跃表本身的模型

public class SkipList {

// 节点数量

public int n;

// 节点最大层数

public int h;

// 第一个节点

SkipListEntry head;

// 最后一个节点

SkipListEntry tail;

public Random r;

}

Note: Random 类的实例对象 r 用来决定新添加的节点是否能够向更高一层的链表攀升。

3、初始化跳跃表的实例

构造函数将初始化一个空的跳跃表看起来像下面这样:

168423ad0ab9

image.png

构造函数的 Java 代码:

public SkipList() {

// 创建 head 节点

this.head = new SkipListEntry(Integer.MIN_VALUE, null);

// 创建 tail 节点

this.tail = new SkipListEntry(Integer.MAX_VALUE, null);

// 将 head 节点的右指针指向 tail 节点

this.head.right = tail;

// 将 tail 节点的左指针指向 head 节点

this.tail.left = head;

this.h = 0;

this.n = 0;

this.r = new Random();

}

4、基本操作

跳跃表需要实现查找、插入、移除这些基本操作:

get(Integer key) : 根据 key 值查找某个元素

put(Integer key, Object value) :插入一个新的元素,元素已存在时为修改操作

remove(Integer key): 根据 key 值删除某个元素

虽然看似是 3 个不同的操作,但是究其本质,要实现这 3 个操作,都得先找到某个元素或是定位到一个元素,好在下一个位子插入新元素。那么,我们就先把这个 findEntry 的方法实现吧。

168423ad0ab9

image.png

上面的图示使用紫色的箭头画出了在一个 SkipList 中查找 key 值 50 的过程。简述如下:

从 head 出发,因为 head 指向最顶层(top level)链表的开始节点,相当于从顶层开始查找;

移动到当前节点的右指针(right)指向的节点,直到右节点的 key 值大于要查找的 key 值时停止;

如果还有更低层次的链表,则移动到当前节点的下一层节点(down),如果已经处于最底层,则退出;

重复第 2 步和第 3 步,直到查找到 key 值所在的节点,或者不存在而退出查找;

Java 代码实现如下:

private SkipListEntry findEntry(Integer key) {

// 从head头节点开始查找

SkipListEntry p = head;

while (true) {

// 从左向右查找,直到右节点的key值大于要查找的key值

while (p.right.key <= key) {

p = p.right;

}

// 如果有更低层的节点,则向低层移动

if (p.down != null) {

p = p.down;

} else {

break;

}

}

// 返回p,!注意这里p的key值是小于等于传入key的值的(p.key <= key)

return p;

}

注意以下几点:

如果传入的 key 值在跳跃表中存在,则 findEntry 返回该对象的底层节点;

如果传入的 key 值在跳跃表中不存在,则 findEntry 返回跳跃表中 key 值小于 key,并且 key 值相差最小的底层节点;

示例,在跳跃表中查找 key=42 的元素节点,将返回 key=39 的节点。如下图所示:

168423ad0ab9

image.png

基于 findEntry 方法,我们就能很容易的实现前面所说的一些操作了。

get方法

public Object get(Integer key) {

SkipListEntry p = findEntry(key);

if (p.key.equals(key)) {

return p.value;

} else {

return null;

}

}

put方法

一些需要注意的步骤:

如果 put 的 key 值在跳跃表中存在,则进行修改操作;

如果 put 的 key 值在跳跃表中不存在,则需要进行新增节点的操作,并且需要由 random 随机数决定新加入的节点的高度(最大level);

当新添加的节点高度达到跳跃表的最大 level,需要添加一个空白层(除了-oo 和 +oo 没有别的节点)

下面我们一步一步的通过图示看一下插入节点的过程:

168423ad0ab9

第一步,查找适合插入的位子

168423ad0ab9

第二步,在查找到的p节点后面插入新增的节点q

168423ad0ab9

image.png

第三步,重复下面的操作,使用随机数决定新增节点的高度

从p节点开始,向左移动,直到找到含有更高level节点的节点;

将p指针向上移动一个level;

创建一个和q节点data一样的节点,插入位子在跳跃表中p的右方和q的上方;

直到随机数不满足向上攀升的条件为止;

图示如下:

168423ad0ab9

168423ad0ab9

168423ad0ab9

只要随机数满足条件,key=42 的节点就会一直向上攀升,直到它的 level 等于跳跃表的高度(height)。这个时候我们需要在跳跃表的最顶层添加一个空白层,同时跳跃表的 height+1,以满足下一次新增节点的操作。

168423ad0ab9

Java代码实现如下:

public Object put(Integer key, Object value) {

SkipListEntry p, q;

int i = 0;

// 查找适合插入的位子

p = findEntry(key);

// 如果跳跃表中存在含有key值的节点,则进行value的修改操作即可完成

if (p.key.equals(key)) {

Object oldValue = p.value;

p.value = value;

return oldValue;

}

// 如果跳跃表中不存在含有key值的节点,则进行新增操作

q = new SkipListEntry(key, value);

q.left = p;

q.right = p.right;

p.right.left = q;

p.right = q;

// 再使用随机数决定是否要向更高level攀升

while (r.nextDouble() < 0.5) {

// 如果新元素的级别已经达到跳跃表的最大高度,则新建空白层

if (i >= h) {

addEmptyLevel();

}

// 从p向左扫描含有高层节点的节点

while (p.up == null) {

p = p.left;

}

p = p.up;

// 新增和q指针指向的节点含有相同key值的节点对象

// 这里需要注意的是除底层节点之外的节点对象是不需要value值的

SkipListEntry z = new SkipListEntry(key, null);

z.left = p;

z.right = p.right;

p.right.left = z;

p.right = z;

z.down = q;

q.up = z;

q = z;

i = i + 1;

}

n = n + 1;

// 返回null,没有旧节点的value值

return null;

}

remove方法

删除节点的操作相对 put 就比较简单了,首先查找到包含 key 值的节点,将节点从链表中移除,接着如果有更高 level 的节点,则 repeat 这个操作即可。

Java代码实现如下:

public Object remove(Integer key) {

SkipListEntry p, q;

p = findEntry(key);

if (!p.key.equals(key)) {

return null;

}

Object oldValue = p.value;

while (p != null) {

q = p.up;

p.left.right = p.right;

p.right.left = p.left;

p = q;

}

return oldValue;

}

跳跃表的原理和实现到这里就结束了。

还有需要说明的一点是:跳跃表每次运行的结果是不一样的,这就是为什么说跳跃表是属于随机化数据结构。(Random的存在导致的)

四、跳跃表在Java中的应用

ConcurrentSkipListMap:在功能上对应HashTable、HashMap、TreeMap;

ConcurrentSkipListSet : 在功能上对应HashSet;

确切的说,SkipList 更像 Java 中的 TreeMap ,TreeMap 基于红黑树(一种自平衡二叉查找树)实现的,时间复杂度平均能达到 O(log n)。

HashMap 是基于散列表实现的,查找时间复杂度平均能达到 O(1)。ConcurrentSkipListMap 是基于跳跃表实现的,查找时间复杂度平均能达到 O(log n)。

ConcurrentSkipListMap 具有 SkipList 的性质 ,并且适用于大规模数据的并发访问。多个线程可以安全地并发执行插入、移除、更新和访问操作。与其他有锁机制的数据结构在巨大的压力下相比有优势。

TreeMap 插入数据时平衡树采用严格的旋转操作(比如平衡二叉树有左旋右旋)来保证平衡,因此 SkipList 比较容易实现,而且相比平衡树有着较高的运行效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值