Go语言基础-环形链表

11.3 环形链表

环形链表没有起点与终点,所有元素组成一个封闭的圆环结构。元素指针在环形链表中可以无限循环移动。
如图 11-5 所示,A、B、C、D、E 组成环形链表。
图11-5 包含五个元素的环形链表

假设当前指针位于元素 A 处,指针向前移动,可以访问元素 A、B、C、D、E、A、B、C……由于环形链表没有首尾之分,所以如果指针一直向前移动,它就会循环访问元素,如图 11-6 所示。
图11-6 指针从A开始向前访问元素

假设指针位于元素 D 处,并且向后移动,那么访问元素的顺序为 D、C、B、A、E、D、C、B……如图 11-7 所示。
图11-7 指针从D开始向后访问元素

11.3.1 与环形链表有关的 API

用于构建和操作环形链表的 API 位于 container/ring 包中。此包中只有一个名为 Ring 的结构体,其源代码如下:

type Ring struct {
    next, prev *Ring
    Value      interface{}
}

一个 Ring 实例表示环形链表中的单个元素。next 字段引用当前元素后面的一个元素,prev 字段引用的是当前元素的前一个元素。Value 字段存放当前元素的值。

Ring 结构体还包含以下方法:
(1) Len:返回环形链表中元素的个数。
(2) Next:返回后一个元素的引用。
(3) Prev:返回前一个元素的引用。
(4) Move:滚动环形链表中的元素。滚动的元素个数为 n % r.Len(),即传入参数 n 除以链表元素个数后的余数。这是因为环形链表的元素是循环访问的,取余的目的是排除重复的“圈数”,得到实际移动的元素个数。
(5) Link:将另一个环形链表链接到当前链表中,形成新的链表。
(6) Unlink:从当前链表中解除 n 个元素的链接。n 的实际使用值也是 n % r.Len(),同理也是为了排除重复的“圈数”。
(7) Do:指定一个自定义函数,环形链表会为每个元素调用一次该函数,并把元素的值传递给函数。

Ring 实例是通过 New 函数初始化的,该函数的源代码如下:

func New(n int) *Ring {
    if n <= 0 {
        return nil
    }
    r := new(Ring)
    p := r
    for i := 1; i < n; i++ {
        p.next = &Ring{prev: p}
        p = p.next
    }
    p.next = r
    r.prev = p
    return r
}

参数 n 指定环形链表中的元素个数,返回的 Ring 指针表示链表的当前位置,默认是第一个元素。

New 函数中的核心是这一段 for 循环:

for i := 1; i < n; i++ {
    p.next = &Ring{prev: p}
    p = p.next
}

此循环代码的作用是创建与 n 数目相等的 Ring 实例,并且让它相互链接,即 A 的 next 指针指向 B,B 的 prev 指针指向 A。

还有一步,就是把最后一个元素与第一个元素链接起来,这样才能形成闭环:

p.next = r
r.prev = p

11.3.2 使用环形链表

下面演示一下环形链表的用法。

步骤 1:调用 New 函数,初始化链表

var myring = ring.New(5)

创建一个包含 5 个元素的环形链表,myring 是指向第一个元素的指针。

步骤 2:为链表中的元素设置 Value 字段(即元素的值)

由于环形链表是首尾相接的,所以要先调用 Len 方法得到元素个数,然后通过 for 循环来逐个对元素赋值。

n := myring.Len() // 元素个数  
pt := myring      // 临时指针  
v := 'A'  
for x := 0; x < n; x++ {  
    pt.Value = v  
    pt = pt.Next()  
    v++  
}  

为了保证 myring 指针始终指向第一个元素,定义了一个变量 pt 用于临时存放指向其他元素的指针。每一轮循环结束前都调用元素的 Next 方法获取下一个元素的指针,并重新赋值给变量 pt。

步骤 3:通过循环向屏幕输出链表中的元素

pt = myring  
for n := 0; n < 15; n++ {  
    fmt.Printf("%c ", pt.Value)  
    pt = pt.Next()  
}  

上述代码循环输出 15 次,链表中只有 5 个元素,但因为元素是首尾相接的,所以会不断地循环读取元素。运行代码后会发现,链表中的元素被循环输出了三次:

ABCDEABCDEABCDE  

11.3.3 滚动环形链表

调用 Move 方法可以让环形链表滚动指定数量的元素,并且返回目标元素的指针。由于环形链表不区分首尾元素,为了排除重复的循环,实际被滚动的元素个数会变为 n % Len()。请看下⾯示例。

var r = ring.New(4)  
n := r.Len() // 链表长度为 4  
p := r       // 临时指针  

// 元素列表:1、2、3、4  
for i := 0; i < n; i++ {  
    p.Value = i + 1 // 设置元素的值  
    p = p.Next()    // 转到下一个元素  
}  

rx := r.Move(18) // 实际移动 18 % 4 个元素  
fmt.Print(rx.Value)  

环形链表中有 4 个元素,元素值依次为 1、2、3、4。变量 r 指向元素 1,链表向后滚动 18 个元素。由于 18 % 4 的结果为 2,所以链表向后滚动过程中,有 4 次重新回到元素 1,之后再向后滚动两个元素。因此最终返回的是元素 3 的指针,如图 11-8 所示。
请添加图片描述

再看一个向后滚动链表的示例:

var r = ring.New(5) // 初始化链表实例  

// 给链表中的元素赋值  
// 元素列表:item-1、item-2、item-3、item-4、item-5  
n := r.Len()  
p := r  
for i := 0; i < n; i++ {  
    p.Value = fmt.Sprintf("item-%d", i + 1)  
    p = p.Next()  
}  

// 滚动链表  
rx := r.Move(-3)  

在调用 Move 方法时,传递给参数 n 的值是-3,由于是负值,链表会向后滚动 3 个元素。所以 Move 方法返回的是 item-3 元素。其过程可以参考图 11-9。

11.3.4 链接两个环形链表

调用 Ring 实例的 Link 方法可以把当前链表与另一个链表进行链接,类似于把两个链表组合成一个新的链表。可以通过一个例子来理解其链接过程。

步骤 1:初始化链表 r 和 s

其中,r 包含 3 个元素——A、B、C;s 包含两个元素——D、E。

var r = ring.New(3)  
n := r.Len()  
p := r  
c := 'A'  
for i := 0; i < n; i++ {  
    p.Value = c  
    p = p.Next()  
    c++  
}  

var s = ring.New(2)  
n = s.Len()  
p = s  
for i := 0; i < n; i++ {  
    p.Value = c  
    p = p.Next()  
    c++  
}  

变量 c 在初始化时赋值为“A”,属于 rune 类型,而 rune 类型实际上是 int32 类型的别名,因此表达式 c++可以让字符对应的 ASCII 码增加 1。当 c 的值为“A”时,c++就变成了“B”,再执行一次 c++就变成“C”。

步骤 2:把 r 和 s 链表链接起来,返回新的链表对象 nr

nr := r.Link(s)  

链接后得到的新链表为 B、C、A、D、E。
链接前,链表 r 的指针位于元素 A 处,链表 s 的指针位于元素 D 处。调用 r 的 Link 方法就是把元素 D、E 插⼊到元素 A、B 之间。链接完成,指针位于插⼊的最后一个元素的下一个元素处,也就是元素 E 的下一个元素——B。可以使用图 11-10 和图 11-11 来模拟此过程。
请添加图片描述

步骤 3:如果希望链接后的元素次序变为 A、B、C、D、E

那么,链表 r 要先把指针移到元素 C 上,然后与链表 s 链接,使元素 D、E 插⼊到 A 跟 C 之间。链接之后指针指向元素 A。

r = r.Move(2) // 向前移动两个元素,指针指向元素 C  
nr := r.Link(s)  

同样,可以模拟该过程,如图 11-12 和图 11-13 所示。
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值