SkipList 一种使用概率替代平衡树的数据结构

SkipList 一种使用概率替代平衡树的数据结构 - 月一orange - 博客园

我的博客搬家啦~,来这两个地方看看吧~

月一orange - 博客园

今天没吃橘子

从单链表到跳表

skip list常称为跳表,查找、插入、删除时间都接近logn,是代替平衡树的一种方案。skip list可以看作是从单链演化而来,单链表插入删除很快,但是查找效率很低,我们可以建立一种索引,给链表增加一层。这样在查找时就可以跳过一些值,加快查找速度。

在建立索引之后,查找6只需要从最高层开始,寻找每一层最接近6的值。第3层找到5,第2层找到5,第1层找到6。很明显比依次遍历单链表要快得多。这种查找方式,类似于二分查找,这是因为我们尽量让上一层的节点个数是下一层的1/2,并且尽量每两个节点构建一个索引,这样整体非常规整,保证每次查找都能舍去一半的节点。

然而在实际工程中,我们并不会去实现一个完全规整的skip list(每n个节点提取一个索引)。假设第i层有n个节点,我们只需要让n个节点中一半的节点成为索引,而不是去刻意要求节点之间的间隔。那这n个节点中的哪些节点会成为索引?答案是随机。随机一半的节点会向上爬一层成为索引。当然可能你运气不好,构造出的索引都是紧挨着的,无法实现跳过节点的功能。

如果每个节点向上爬的概率是1/2,假设第0层有100个节点,第1层大概有50个,第2层大概有25个,第3层大概有12个......大概率情况下,我们生成的跳表是近似于规整的跳表的,查找时间也不会出现太多偏差。

那通过什么方式让下一层的节点爬到上一层呢?答案是在插入这个节点的时候就决定好它的高度。对于第0层中的n个数,其中一个数能爬到第1层的概率是1/2,能爬到第2层的概率是1/4,能爬到第3层的概率是1/8......那对于一个空的跳表,新插入一个节点,它有100%的概率在第0层,有50%的概率在第1层,有25%的概率在第2层......

那怎么知道这个节点应该占多少层呢?我们只需要模拟出这个概率即可。生成一个0-1之间的随机小数,这个小数在0-1/2的概率为50%,那么就让节点占有0层和1层;这个小数在0-1/4的概率为25%,那么就让节点占有0层、1层、2层。

这样,我们插入10000个节点,第0层有10000个节点,占0层、1层的节点大概会有10000 * 1/2 = 5000,占0层、1层、2层的节点大概有10000 * 1/8 = 2500个......有可能你花光了下辈子的运气,插入的节点高度为99999,工程中一般会设置一个最大高度,插入的节点最多就占maxlevel层。

跳表的数据结构

上图的跳表完全由单链表构建,当查询下一层时,需要通过down指针移动,我们知道,指针移动要比通过下标查找慢得多。而且索引中的每一列存储的key都是相同的,所以可以使用数组存储索引的一列。

具有相同key的节点算作一列,使用一个数组存储,这一列成为一个skiplistnode,它的数据结构应该是:

typedef SkipListNode struct {
    Key int
    Next []*SkipListNode // next数组长度取决于占多高
}

每次往跳表中加入新的key,就是先算出key应该占几层(高度是多少),然后构造这样一个skiplistnode。之后找这个node应该插入哪两个node的中间,找到后插入即可。

对于整个跳表,它应该包含一个头节点(想象单链表,也包含一个头节点),整个跳表的最大高度,跳表当前最高处于哪个高度:

typedef SkipList struct {
    MaxLevel int // 跳表允许的最大高度
    level int // 当前跳表中最高有多高
    header *SkipListNode // 头节点
}

跳表的api

跳表一般有put、delete、get这三种操作,其中put操作会将具有相同key的node的value覆盖掉。这三种api的实现有多种方式,可以参考skiplist论文中的伪代码,写的很优雅。这里主要简单说说思路(还有很多细节,具体看代码),然后给出我的实现:

put:我们往单链表插值是怎么做的?找前置节点pre,然后new一个node,然后node.next = pre.next, pre.next=node。同样的,在跳表中插入一个skiplistnode,需要找出每一层的前置节点,将前置节点保存到数组中,然后new一个skiplistnode,然后遍历前置节点数组,每一行都进行类似于单链表的插入操作。

delete:同样需要找出前置节点数组,然后更新每一层前置节点的next。

get:逐层寻找,先从最高层开始往右找,找到最接近目标值的节点,但是仍小于目标,然后开始下一层寻找。形象上是右下右下这样移动。

go语言具体实现

package index

import (
	"bytes"
	"math"
	"math/rand"
	"time"
)

const (
	MaxLevel    int     = 32 // 从第1层开始算
	Probability float64 = 1 / math.E
)

type SkipList struct {
	header *SkipListNode // 跳表的头节点
	rand   rand.Source   // 用于生成随机数
	level  int           // 当前跳表的最高level
	size   int           // 底层存储的节点个数
}

type SkipListNode struct {
	key   []byte          // key用字节数组存储
	value interface{}     // value是任意类型
	next  []*SkipListNode // 指向后面一列由相同key组成的列表
}

// 构造跳表
func NewSkipList() *SkipList {
	return &SkipList{
		header: NewSkipListNode(nil, nil, MaxLevel),
		rand:   rand.NewSource(time.Now().UnixNano()),
		level:  1,
		size:   0,
	}
}

// 构造跳表节点
func NewSkipListNode(key []byte, value interface{}, level int) *SkipListNode {
	return &SkipListNode{
		key:   key,
		value: value,
		next:  make([]*SkipListNode, level),
	}
}

// 寻找节点
func (sl *SkipList) Get(key []byte) *SkipListNode {
	x := sl.header

	for i := sl.level - 1; i >= 0; i-- {
		for x.next[i] != nil {

			// 在第i层,找到相同的key直接返回
			res := bytes.Compare(x.next[i].key, key)
			if res < 0 {
				x = x.next[i]
			} else if res == 0 {
				return x.next[i]
			}
		}
	}

	return nil
}

// 插入新节点,相同的node替换value
func (sl *SkipList) Put(key []byte, value interface{}) {

	// 将新节点每一层的前置节点存入update数组
	update := make([]*SkipListNode, MaxLevel)
	x := sl.header
	for i := sl.level - 1; i >= 0; i-- {
		for x.next[i] != nil && bytes.Compare(x.next[i].key, key) < 0 {
			x = x.next[i]
		}
		update[i] = x
	}

	// 查看要插入的位置是否已经有相同key
	x = x.next[0]
	if x != nil && bytes.Compare(x.key, key) == 0 {
		x.value = value
		return
	}

	// 新插入节点的level过高,前置节点就是header,将header保存至update
	lvl := sl.RandomLevel()
	if lvl > sl.level {
		for i := lvl - 1; i >= sl.level-1; i-- {
			update[i] = sl.header
		}
		sl.level = lvl
	}

	// 将新节点插入
	newNode := NewSkipListNode(key, value, lvl)
	for i := sl.level - 1; i >= 0; i-- {
		newNode.next[i] = update[i].next[i]
		update[i].next[i] = newNode
	}

	sl.size++
}

// 删除节点
func (sl *SkipList) Remove(key []byte) {

	// 将新节点每一层的前置节点存入update数组
	update := make([]*SkipListNode, MaxLevel)
	x := sl.header
	for i := sl.level - 1; i >= 0; i-- {
		for x.next[i] != nil && bytes.Compare(x.next[i].key, key) < 0 {
			x = x.next[i]
		}
		update[i] = x
	}

	// 判断新节点插入的位置是否已经有相同的key
	x = x.next[0]
	if x != nil && bytes.Compare(x.key, key) == 0 {

		// 从最底层往上依次删除,直到超过被删除node的level
		for i := 0; i < sl.level; i++ {
			if update[i].next[i] != x {
				break
			}
			update[i].next[i] = x.next[i]
		}
	}
}

// 获取新node的前置node
func (sl *SkipList) UpdateNodes(key []byte) []*SkipListNode {

	// 保存前置node
	update := make([]*SkipListNode, MaxLevel)

	// 找每一层的前置node
	x := sl.header

	for i := sl.level - 1; i >= 0; i-- {
		for x.next[i] != nil && bytes.Compare(x.next[i].key, key) < 0 {
			x = x.next[i]
		}
		update[i] = x
	}

	return update
}

// 返回随机level
func (sl *SkipList) RandomLevel() int {
	level := 1

	// 每次循环,概率累乘1/2
	for level < MaxLevel && sl.Random() < Probability {
		level++
	}

	return level
}

// 生成0-1的随机数
func (sl *SkipList) Random() float64 {
	return float64(sl.rand.Int63() / 1 << 63)
}

おすすめ

転載: blog.csdn.net/weixin_43237362/article/details/121428804
おすすめ