Go的链表实现并解决约瑟夫环问题(1)

164 阅读5分钟

链表的基本构成

链表是由一个指针域和一个数据域构成,在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()
}

输出结果为

image.png

循环链表

这里重点说一下循环链表,并用循环链表解决约瑟夫问题 循环链表的结构体跟普通链表差不多,本质是最后一个节点的指针域指向的是头节点

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节点的情况,发现跟我们预期相同,形成了一个循环链表

image.png 循环链表有许多的应用场景,约瑟夫问题就是其中一个,下面简单介绍一下约瑟夫问题:约瑟夫问题(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)
}

输出结果是

image.png

今天的链表学习就到这里咯