11.3 环形链表
环形链表没有起点与终点,所有元素组成一个封闭的圆环结构。元素指针在环形链表中可以无限循环移动。
如图 11-5 所示,A、B、C、D、E 组成环形链表。
假设当前指针位于元素 A 处,指针向前移动,可以访问元素 A、B、C、D、E、A、B、C……由于环形链表没有首尾之分,所以如果指针一直向前移动,它就会循环访问元素,如图 11-6 所示。
假设指针位于元素 D 处,并且向后移动,那么访问元素的顺序为 D、C、B、A、E、D、C、B……如图 11-7 所示。
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 所示。