链表的基本构成
链表是由一个指针域和一个数据域构成,在golang中,数据域可以是各种类型,包括整形,浮点型,切片等等,指针域则是指向下一个节点。
type ListNode struct {
val int//数据域
Next *ListNode//指针域
}
那么接下来介绍一下链表的基本操作
添加节点
func (ll *ListNode) insert(val int) {
//如果为空指针则直接返回
if ll == nil {
return
}
newNode := &ListNode{val: val}
cur := ll
//遍历到末尾
for cur.Next != nil {
cur = cur.Next
}
cur.Next = newNode
}
这里删除节点跟后面的循环链表一起说明,现在我们直接打印这个链表
type ListNode struct {
val int
Next *ListNode
}
func (ll *ListNode) insert(val int) {
if ll == nil {
return
}
newNode := &ListNode{val: val}
cur := ll
for cur.Next != nil {
cur = cur.Next
}
cur.Next = newNode
}
func (ll *ListNode) print() {
if ll == nil {
return
}
cur := ll
for cur != nil {
fmt.Println(cur.val)
cur = cur.Next
}
}
func main() {
head := &ListNode{val: 1}
head.insert(2)
head.insert(3)
head.insert(4)
head.insert(5)
head.print()
}
输出结果为
循环链表
这里重点说一下循环链表,并用循环链表解决约瑟夫问题 循环链表的结构体跟普通链表差不多,本质是最后一个节点的指针域指向的是头节点
type CircleListNode struct {
Val int
Next *CircleListNode
}
func main() {
//创建一个节点
head := &CircleListNode{Val: 1}
//这里是只有一个节点的链表,所以这里直接指向了自己
head.Next = head
}
-
添加和删除节点的操作
添加节点
func (cl *CircleListNode) append(val int) {
if cl == nil {
return
}
newNode := &CircleListNode{Val: val}
cur := cl
//循环结束条件为当前节点的下一个节点为头节点
for cur.Next != cl {
//移动指针的操作
cur = cur.Next
}
//这里不能忘
cur.Next = newNode
newNode.Next = cl
}
删除尾节点
删除尾节点的关键就是要找到倒数第二个节点,让倒数第二个节点指向头节点,这样尾节点就删除了,并且由于golang的垃圾回收机制,尾节点没用被引用,会直接被回收
func (cl *CircleListNode) delete() {
if cl == nil {
return
}
cur := cl
if cur.Next == cl {
return
}
//这里的循环结束条件是cur为倒数第二个节点
for cur.Next.Next != cl {
cur = cur.Next
}
cur.Next = cl
}
完整和代码和打印节点
type CircleListNode struct {
Val int
Next *CircleListNode
}
func (cl *CircleListNode) append(val int) {
if cl == nil {
return
}
newNode := &CircleListNode{Val: val}
cur := cl
for cur.Next != cl {
cur = cur.Next
}
cur.Next = newNode
newNode.Next = cl
}
func (cl *CircleListNode) print() {
if cl == nil {
return
}
cur := cl
if cur.Next == cl {
fmt.Println(cl.Val)
return
}
fmt.Println(cl.Val)
for cur.Next != cl {
cur = cur.Next
fmt.Print(cur.Val)
}
fmt.Println()
}
func (cl *CircleListNode) delete() {
if cl == nil {
return
}
cur := cl
if cur.Next == cl {
return
}
for cur.Next.Next != cl {
cur = cur.Next
}
cur.Next = cl
}
func main() {
//创建一个节点
head := &CircleListNode{Val: 1}
head.Next = head
//添加节点
newNode := &CircleListNode{Val: 2}
head.Next = newNode
newNode.Next = head
tmp := &CircleListNode{Val: 3}
head.Next.Next = tmp
tmp.Next = head
//删除一个节点(中间节点)
head.Next = head.Next.Next
//删除一个节点(末尾节点)
head.Next.Next = head
//由于golang的垃圾回收机制,没用被引用的节点将会被回收
//添加循环链表的末尾节点
root := &CircleListNode{Val: 10}
root.Next = root
root.append(12)
root.append(13)
root.append(14)
root.append(15)
//打印循环链表
root.print()
//删除循环链表的末尾节点
root.delete()
root.print()
}
这里我们可以打断点,看到root节点的情况,发现跟我们预期相同,形成了一个循环链表
循环链表有许多的应用场景,约瑟夫问题就是其中一个,下面简单介绍一下约瑟夫问题:约瑟夫问题(osephus Problem)一个著名的理论问题,源自古罗马历史学家约瑟夫斯(Flavius Josephus)的自传。根据他的描述,在犹太战争期间,约瑟夫和40名士兵被困在一个洞穴中,为了避免被罗马军队俘虏,他们决定自杀,但约瑟夫并不想死。于是,他们围成一圈,每隔一定人数(比如每三个人)处决一个人,直到最后一个人。约瑟夫通过计算找到了安全的位置,从而幸存下来。
这里我们将问题变化为,在循环链表中,一共有n个节点,每个节点从1开始计数,每过m个(报数到m个)就删除那个节点。
我们只需要复用上面的代码,增加两个函数,第一个是remove函数,用来移除第k个节点(k是从头节点的开始的第k个节点,比如1,2,3,4那第2个节点就是头节点数2个,就是3),函数实现如下
remove函数
func Remove(head *CircleListNode, k int) *CircleListNode {
if head == nil || head.Next == head {
//记得返回头节点不要返回nil,不然到删除最后一个节点的时候会报空指针异常
return head
}
//删除头节点的情况
if k == 0 {
cur := head
//遍历到最后一个节点,逻辑跟删除其他节点一样
for cur.Next != head {
cur = cur.Next
}
cur.Next = head.Next
return head
}
cur := head
//要删除某个节点,就要移动到这个节点的上一个节点,比如我要删除第3个节点,我就需要移动到第2个节点,然后让第二个节点的指针域指向第四个节点,就完成了删除
for i := 0; i < k-1; i++ {
cur = cur.Next
}
//这里一定要新声明一个遍量,不要直接返回cur.Next
tmp := cur.Next
cur.Next = cur.Next.Next
return tmp
}
这里其实删除头节点的逻辑跟删除其他节点的逻辑一样,但是为什么要讨论k==0的时候呢,其实如果不这样,我们也要判断这个节点是否为头节点,因为不判断的话删除第1个节点和头节点都会被判断成删除第一个节点,当然不让k==0,进入循环之后再判断也是可以的
josehpus函数如下
func josephus(n, m int) {
head := &CircleListNode{Val: 1}
head.Next = head
for i := 1; i < n; i++ {
head.append(1 + i)
}
fmt.Println("原始链表为")
head.print()
for i := 0; i < n; i++ {
//这里是m-1的原因是因为我们说的第k个节点,实际上报数的是k+1,比如第一个节点报数的是2(头节点报数1),所以这里如果假设删除的是报3,实际上删除的是第2个节点(具体体现为移动次数)
delNode := Remove(head, m-1)
fmt.Printf("被删除的节点是%d\n", delNode.Val)
//记得要重置头节点
head = delNode.Next
}
}
总体逻辑如下
type CircleListNode struct {
Val int
Next *CircleListNode
}
func (cl *CircleListNode) append(val int) {
if cl == nil {
return
}
newNode := &CircleListNode{Val: val}
cur := cl
for cur.Next != cl {
cur = cur.Next
}
cur.Next = newNode
newNode.Next = cl
}
func (cl *CircleListNode) print() {
if cl == nil {
return
}
cur := cl
if cur.Next == cl {
fmt.Print(cl.Val)
return
}
fmt.Print(cl.Val)
for cur.Next != cl {
cur = cur.Next
fmt.Print(cur.Val)
}
fmt.Println()
}
func Remove(head *CircleListNode, k int) *CircleListNode {
if head == nil || head.Next == head {
return head
}
if k == 0 {
cur := head
for cur.Next != head {
cur = cur.Next
}
cur.Next = head.Next
return head
}
cur := head
for i := 0; i < k-1; i++ {
cur = cur.Next
}
tmp := cur.Next
cur.Next = cur.Next.Next
return tmp
}
func josephus(n, m int) {
head := &CircleListNode{Val: 1}
head.Next = head
for i := 1; i < n; i++ {
head.append(1 + i)
}
fmt.Println("原始链表为")
head.print()
for i := 0; i < n; i++ {
delNode := Remove(head, m-1)
fmt.Printf("被删除的节点是%d\n", delNode.Val)
head = delNode.Next
}
}
func main() {
josephus(6, 5)
}
输出结果是
今天的链表学习就到这里咯