循环链表

同样的,循环链表本身在 Go 的 SDK 里已经实现了,位于 container/ring 包中。

我们可以观摩下官方实现的循环链表:

type Ring struct {
    next, prev *Ring
    Value      interface{} // for use by client; untouched by this library
}

func (r *Ring) init() *Ring {
    r.next = r
    r.prev = r
    return r
}

// Next returns the next ring element. r must not be empty.
func (r *Ring) Next() *Ring {
    if r.next == nil {
        return r.init()
    }
    return r.next
}
// Prev returns the previous ring element. r must not be empty.
func (r *Ring) Prev() *Ring {
    if r.next == nil {
        return r.init()
    }
    return r.prev
}

其实看上去也没有什么不同,Ring 也是链表的一个节点,它既是一个循环链表,也是一个双向链表,有前后两个指针。

和官方 SDK 定义的双向链表相比,其实非常相似,Ring 这里就可以看成是双向链表里面的 Element,也就是链表中的节点,但是这个 Ring 节点并没有指向一个独立的链表结构体,也就是说,循环链表中并没有定义像双向链表中的 List 的结构体。

  • 循环链表中的的创建:

创建和双向链表相比也显得十分的简洁,其中参数 n 代表链表元素的个数,最后返回的是创建好的链表的链表首元素指针(这里也是比较罕见的用 new 去创建一个结构体,赶紧拿笔记下来)。

  • 获取链表长度

这里其实就是对链表的遍历啦,通过创建一个 n 作为计数器,遍历节点最终计算出链表的长度。那么可以看出来,这个计算链表的时间复杂度是 O(n),如果链表中元素较多的时候,这个操作还是比较耗时的,建议在使用的时候最好在外部把链表的长度先存起来,做修改的时候直接操作外部的值,以免造成额外的开销。

  • 链表中节点的获取(移动)

Move 方法通过传入参数获取到指针移动的次数和参数,这个比较好玩的是,他实现了往前移动和往后移动,取负值就是往后移动。

  • 链表的连接和解连接

这个 Link 方法其实就是把 s 链表接到 r 链表的后面。

在解连接的时候,传入的参数是 n,其实是删除 n % r.Len() 个元素,从 r 的下一个点开始,到 r 往后的第 n 个节点,这里非常巧妙通过 move 指向 r 往后的第 n + 1 个节点,然后将 r 节点和后面的第 n + 1 个节点连接,完成整个删除的操作。

不过,需要注意的是,r 本身不能为空,并且传入参数的 n 如果和链表本身长度相同,那么这个解连接会失效(相当于什么都没做,因为这个时候 n+1 就是自己的下一个节点)。

  • Do 方法(遍历)

其实就是实现对循环链表遍历的操作,你可以通过传入 f 函数实现一些定制化的功能。

总结

通过看 ring 的源码,与双向链表相比,我们还是可以发现 ring 更加精简,实现的复杂性明显降低的很多,实现特别简单,但是在计算链表长度时可能会比较耗时,建议在使用循环链表的时候最好在外部把链表的长度先存起来,做修改的时候直接操作外部的值,以免造成额外的开销。

最后更新于