使用golang简单实现跳跃表SkipList

    有关SkipList的定义,请参考跳跃表(可能需要科学上网)。我们知道,有序链表,无论是单向还是双向,增删改查时间复杂度都是O(n)。跳跃表的存在就是为了解决这个问题,解决思路为空间换时间,不过空间复杂度没有提升,仍然为O(n)(理论上使用的空间变为原来的1-2倍,数学期望为n+log(n)倍),但是增删改查的时间复杂度降低为O(log(n)),其性能可以媲美二叉树家族(AVL树红黑树),但在并发情况下,二叉树在增删操作后进行重新平衡需要加锁的节点要比跳跃表多,所以整体性能表现上,跳跃表要优于二叉树。

    本文将使用go语言实现跳跃表的增、删、改、查操作,考虑到业务上的使用场景,该实现将支持协程安全。

    一个典型的单向四层跳跃表如图1:

图1.单向四层跳跃表示意图

    由图1可以看到,跳跃表具有以下特性:

    1、层数(LEVEL)越高,链上的节点越少,大致呈P=0.5的几何分布

    2、每一层的节点均有序且不重复。

    跳跃表层数(LEVEL)的设置,应当与数据量息息相关,合理的取值应该为log(n),对数底数为2,当然,考虑到数据量增长,大于log(n)也是可以的。比如,数据量为30亿条,则合理的层数取值应该为31<log(3000000000)<32,故LEVEL应取32。

    首先,定义节点结构体:

type SkipListNode struct {

    key int

    data interface{}

    next []*SkipListNode

}

    其中,key为排序使用的字段,对应图1中的数字1~9;data为实际存储的数据,可以为任何类型,图1中并没有体现;next为节点指针切片,比如图1中的"HEAD"节点的next应该为[&5, &3, &1, &1],“3”节点的next应该为[&5, &5, &4],其中&n表示key为n节点的地址。

    定义跳跃表结构体:

type SkipList struct {

    head    *SkipListNode

    tail        *SkipListNode

    length    int

    level      int

    mut       *sync.RWMutex

    rand      *rand.Rand

}

    head:头节点

    tail:尾节点

    length: 数据总量

    level:层数

    mut:互斥锁,用于保证协程安全

    rand:随机数生成器,用于生成随机层数,随机生成的层数要满足P=0.5的几何分布,大致可以理解为:掷硬币连续出现正面的次数,就是我们要的层数。所以随机生成层数的方法可以定义如下:

func (list *SkipList) randomLevel() int {

    level := 1

    for ; level < list.level && list.rand.Uint32() & 0x1 == 1; level ++{}

    return level

}

    补上一个工厂构造方法:

func NewSkipList(level    int) *SkipList {

    list := &SkipList{}

    if level <=0 {

        level =32

    }

    list.level = level

    list.head = &SkipListNode{next:make([]*SkipListNode, level, level)}

    list.tail = &SkipListNode{}

    list.mut = &sync.RWMutex{}

    list.rand = rand.New(rand.NewSource(time.Now().UnixNano()))

    for index :=range list.head.next {

        list.head.next[index] = list.tail

    }

    return list

}

    接下来实现数据插入方法。假设我们目前的跳跃表已经有如下数据:

图2.需要插入数据的跳跃表

    我们需要插入一个3,并且调用randomLevel得到的层数为3,那么插入3需要如下几部:

    1、沿着LEVEL3的链查找第一个比3大的节点或者TAIL节点,记录下该节点的前一个节点——HEAD和层数——3。

    2、沿着LEVEL2的链查找第一个比3大的节点或者TAIL节点,记录下该节点的前一个节点——&1和层数——2。

    3、沿着LEVEL1的链查找第一个比3大的节点或者TAIL节点,记录下该节点的前一个节点——&2和层数——1。

    4、生成一个新的节点newNode,key赋值为3,将newNode插入HEAD、&1、&2之后,即HEAD.next[3]=&3,&1.next[2]=&3,&2.next[1]=&3。

    5、给newNode的next赋值,即步骤4中HEAD.next[3]、&1.next[2]、2.next[1]原本的值。

    注意:为了易于理解,上述步骤中所有索引均从1开始,而代码中则从0开始,所以代码中均有索引=层数-1的关系。

    那么,插入的代码可以写成这样:

func (list *SkipList) Add(key int, data interface{}) {

    list.mut.Lock()

    defer list.mut.Unlock()

    //1.确定插入深度

    level := list.randomLevel()

    //2.查找插入部位

    update :=make([]*SkipListNode, level, level)

    node := list.head

    for index := level -1; index >=0; index -- {

        for {

            node1 := node.next[index]

            if node1 == list.tail || node1.key > key {

                update[index] = node//找到一个插入部位

                break

             }else if node1.key == key{

                node1.data = data

                return

             }else {

                node = node1

            }

        }

    }

    //3.执行插入

    newNode := &SkipListNode{key, data, make([]*SkipListNode, level, level)}

    for index, node :=range update {

        node.next[index],newNode.next[index] = newNode,node.next[index]

    }

    list.length ++

}

    实际上,删除、查找的方法与插入变化不大。

    删除方法:

func (list *SkipList) Remove(key int) bool {

    list.mut.Lock()

    defer list.mut.Unlock()

//1.查找删除节点

    node := list.head

    remove :=make([]*SkipListNode, list.level, list.level)

    var target *SkipListNode

    for index :=len(node.next) -1; index >=0; index -- {

        for {

            node1 := node.next[index]

            if node1 == list.tail ||  node1.key >  key{

                break

             }else if node1.key == key {

                remove[index] = node//找到啦

                target = node1

                break

             }else {

                node = node1

            }

        }

    }

//2.执行删除

    if target != nil {

        for index, node1 :=range remove {

            if node1 != nil {

                node1.next[index] = target.next[index]

            }

        }

        list.length --

        return true

       }

    return false

}

    查找方法:

func (list *SkipList)Find(key int) interface{} {

    list.mut.RLock()

    defer list.mut.RUnlock()

    node := list.head

    for index :=len(node.next) -1; index >=0; index -- {

        for {

            node1 := node.next[index]

            if node1 == list.tail ||  node1.key >  key   {

                break

             }else if node1.key == key {

                return node1.data

            }else {

                node = node1

            }

        }

    }

return nil

}

    最后,是获取数据总量方法:

func (list *SkipList) Length() int {

    list.mut.RLock()

    defer list.mut.RUnlock()

    return list.length

}

    就这样,一个单向的跳跃表就实现了,是不是很简单呢?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容