约瑟夫问题
1)设编号为 1,2,3 ... n 的 n 个人围坐一圈。
2)约定编号为 k (1 <= k <= n)的人从 1 开始报数,数到 m 的那个人出列。
3)它的下一位又从 1 开始报数,数到 m 的那个人又出列,依此类推,直到所有人出列为止。
4)由此产生一个出列编号的序列。
解题思路
1)构建一个结构体 Boy
No 为 Boy 的编号。
Next 指针指向下一个 Boy。
代码如下:
// 小孩的结构体
type Boy struct {
No int // 编号
Next *Boy // 指向下一个小孩的指针
}
2)根据编号,创建一个单向环形队列。
假设 n = 5 ,即一共圈内有5个小孩,创建完成的结构体如图所示:
first 指针指向队首。
curBoy 指针指向当前要添加的新节点。

当只有一个小孩时,构成一个自循环,Next 指针指向自身,如图所示:

当不止一个小孩,新节点加入的流程如图所示:
1、构建编号为 n 的新节点 boy
boy := &Boy{
No: i,
}

2、循环加入编号为 n 的新节点:
让 curBoy 的 Next 指针指向新建立的节点 boy
让 curBoy 指针指向新建立的节点 boy
让 curBoy 的 Next 指针指向头节点 first
依此类推,循环加入新节点
如图所示:

循环构建单向环形链表的代码:
// 小孩构成单向环形链表
// num:小孩的个数
// *Boy:返回单向环形链表的头指针
func AddBoy(num int) *Boy {
// 第一个小孩,空节点
first := &Boy{}
// 因为 first 指针固定指向第一个小孩
// 因此 first 指针不能移动,需要一个辅助指针
// 辅助指针 curBoy ,指向当前的小孩
curBoy := &Boy{}
if num < 1 {
fmt.Println("num 的值有误")
return first
}
// 循环地构成单向环形链表
for i := 1; i <= num; i++ {
boy := &Boy{
No: i,
}
// 第一个小孩
if i == 1 {
first = boy
curBoy = boy
// 形成一个自循环
curBoy.Next = first
} else {
curBoy.Next = boy
curBoy = boy
// 最后一个节点指向头节点
// 形成一个单向环形链表
curBoy.Next = first
}
}
return first
}
3)根据编号构建好单向环形队列后,编写一个出列算法:
1、让头指针 first 指向单向环形队列的头部。
2、让尾指针 tail 指向单向环形队列的尾部。
如图所示:

3、当 first 指针移动时,tail 指针也跟着移动相应的次数。
设置 tail 指针的目的是作为辅助指针,用来删除指定的节点。
假设:n = 5, k = 2 , m = 3 (即5个小孩围成一圈,由第2个小孩开始报数,数到3的小孩出列)。
未报数前:
由于是第 2 个小孩开始报数,因此 first 指针移动到节点 2 。
因为 tail 指针移动的次数和 first 指针一致,因此 tail 指针移动到节点 1。
如下图所示:

报数后:
由第2个小孩由1开始报数,数到 3 的小孩出列。
因此,报数后,first 指针移动到节点4。(节点2:报1,节点3:报2,节点4:报3)
tail 指针移动的次数和 first 指针一致,因此 tail 指针移动到节点 3。
如下图所示:

那么,节点4 出列。因此需要在单向环形链表上删除节点4。
因此,需要执行如下流程:
1.把 first 指针后移。
2.把 tail 指针的 Next 指向 first。
如下图所示:

由于 节点4 指向 节点5 ,但是没有任何节点指向 节点4 , 因此 节点4 会自动被系统的垃圾回收机制回收。
因此新构成的单向环形链表如图所示:
依此类推,循环出列节点,直到单向环形队列中只剩下一个节点。
循环出列结束的标志为 first == tail (因为当 first == tail 的时候,说明队列中只剩下了一个节点)
如图所示:

出列的算法如下:
// 解决约瑟夫问题的算法
// k : 约定编号为 k 的人从 1 开始报数
// m : 从1开始报数,数到 m 的人出列
func Josephu(first *Boy, k int, m int) {
fmt.Println("\n小孩出圈的顺序如下:")
// 单独处理空链表
if first.Next == nil {
fmt.Println("empty link list")
return
}
if k > CountBoy(first) {
fmt.Printf("输入的参数 %d 有误\n", k)
return
}
// 定义一个辅助指针,帮助删除小孩
tail := first
// 让 tail 指向环形链表的最后一个节点(小孩)
// 后面 tail 和 first 都往后挪动的时候
// 才能保证 tail 一直在 fist 后面
for {
// 说明已经指向了最后一个节点
// 退出循环
if tail.Next == first {
break
}
tail = tail.Next
}
// 让 first 移动到 k (编号为 k 的人开始报数)
// 移动到编号 k ,只需要移动 k - 1 次
for i := 0; i < k-1; i++ {
first = first.Next
tail = tail.Next
}
// 开始数 m 下(数到 m 的人出列)
// 然后就删除 first 指向的小孩
for {
for i := 0; i < m-1; i++ {
first = first.Next
tail = tail.Next
}
fmt.Printf("编号为 %d 的小孩出圈\n", first.No)
// 删除 first 指向的小孩
first = first.Next
tail.Next = first
// 如果圈中只有一个小孩
if first == tail {
fmt.Printf("编号为 %d 的小孩出圈\n", first.No)
break
}
}
}
完整代码
/*
数据结构
约瑟夫问题(Josephus problem):
1)设编号为 1,2,3 ... n 的 n 个人围坐一圈
2)约定编号为 k (1 <= k <= n)的人从 1 开始报数
数到 m 的那个人出列
3)它的下一位又从 1 开始报数,数到 m 的那个人又出列
依此类推,直到所有人出列为止
4)由此产生一个出列编号的序列
*/
package main
import "fmt"
// 小孩的结构体
type Boy struct {
No int // 编号
Next *Boy // 指向下一个小孩的指针
}
// 小孩构成单向环形链表
// num:小孩的个数
// *Boy:返回单向环形链表的头指针
func AddBoy(num int) *Boy {
// 第一个小孩,空节点
first := &Boy{}
// 因为 first 指针固定指向第一个小孩
// 因此 first 指针不能移动,需要一个辅助指针
// 辅助指针 curBoy ,指向当前的小孩
curBoy := &Boy{}
if num < 1 {
fmt.Println("num 的值有误")
return first
}
// 循环地构成单向环形链表
for i := 1; i <= num; i++ {
boy := &Boy{
No: i,
}
// 第一个小孩
if i == 1 {
first = boy
curBoy = boy
// 形成一个自循环
curBoy.Next = first
} else {
curBoy.Next = boy
curBoy = boy
// 最后一个节点指向头节点
// 形成一个单向环形链表
curBoy.Next = first
}
}
return first
}
// 显示单向的环形链表
func ShowBoy(first *Boy) {
// 单独处理空链表
if first.Next == nil {
fmt.Println("empty link list")
return
}
// 创建一个辅助指针,帮助遍历
curBoy := first
for {
fmt.Printf("小孩编号 = %d -> ", curBoy.No)
// 退出循环的条件
// 单向环形链表的最后一个节点指向头指针
if curBoy.Next == first {
break
}
curBoy = curBoy.Next
}
}
// 统计小孩的数量
func CountBoy(first *Boy) int {
// 单独处理空链表
if first.Next == nil {
return 0
}
// 创建一个辅助指针,帮助遍历
curBoy := first
// 计数
count := 0
for {
count++
// 退出循环的条件
// 单向环形链表的最后一个节点指向头指针
if curBoy.Next == first {
break
}
curBoy = curBoy.Next
}
return count
}
// 解决约瑟夫问题的算法
// k : 约定编号为 k 的人从 1 开始报数
// m : 从1开始报数,数到 m 的人出列
func Josephu(first *Boy, k int, m int) {
fmt.Println("\n小孩出圈的顺序如下:")
// 单独处理空链表
if first.Next == nil {
fmt.Println("empty link list")
return
}
if k > CountBoy(first) {
fmt.Printf("输入的参数 %d 有误\n", k)
return
}
// 定义一个辅助指针,帮助删除小孩
tail := first
// 让 tail 指向环形链表的最后一个节点(小孩)
// 后面 tail 和 first 都往后挪动的时候
// 才能保证 tail 一直在 fist 后面
for {
// 说明已经指向了最后一个节点
// 退出循环
if tail.Next == first {
break
}
tail = tail.Next
}
// 让 first 移动到 k (编号为 k 的人开始报数)
// 移动到编号 k ,只需要移动 k - 1 次
for i := 0; i < k-1; i++ {
first = first.Next
tail = tail.Next
}
// 开始数 m 下(数到 m 的人出列)
// 然后就删除 first 指向的小孩
for {
for i := 0; i < m-1; i++ {
first = first.Next
tail = tail.Next
}
fmt.Printf("编号为 %d 的小孩出圈\n", first.No)
// 删除 first 指向的小孩
first = first.Next
tail.Next = first
// 如果圈中只有一个小孩
if first == tail {
fmt.Printf("编号为 %d 的小孩出圈\n", first.No)
break
}
}
}
func main() {
first := AddBoy(5)
ShowBoy(first)
Josephu(first, 2, 3)
}
执行结果
小孩编号 = 1 -> 小孩编号 = 2 -> 小孩编号 = 3 -> 小孩编号 = 4 -> 小孩编号 = 5 ->
小孩出圈的顺序如下:
编号为 4 的小孩出圈
编号为 2 的小孩出圈
编号为 1 的小孩出圈
编号为 3 的小孩出圈
编号为 5 的小孩出圈
本文详细介绍了约瑟夫问题的解题思路,通过构建单向环形链表模拟过程,并提供了完整的Golang代码实现,最后展示了执行结果。
1421

被折叠的 条评论
为什么被折叠?



