约瑟夫问题(Josephus problem)详解

本文详细介绍了约瑟夫问题的解题思路,通过构建单向环形链表模拟过程,并提供了完整的Golang代码实现,最后展示了执行结果。

约瑟夫问题

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 的小孩出圈

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值