前言
单向环形链表是一种特殊的数据结构,它结合了链表的线性存储特点与环形结构的循环特性。这种结构特别适用于需要模拟循环行为且元素间关系非连续存储的应用场景。约瑟夫问题(Josephus Problem)是一个经典的数学和计算机科学问题,巧妙地运用单向环形链表可以简洁高效地解决这个问题。下面我们将详细阐述单向环形链表的构造方法,并展示如何使用Java语言来解决约瑟夫问题。
单向环形链表的构造
单向环形链表由一系列节点组成,每个节点包含两部分:数据域(通常存放元素值)和指针域(指向下一个节点)。与普通单链表不同的是,单向环形链表的最后一个节点的指针不再为null,而是指向链表的第一个节点,形成了一个封闭的环。
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
class CircularLinkedList {
private Node head;
private int size;
// ... 其他方法(如添加节点、遍历等)
}
为便于理解我们一小孩的约瑟夫问题新建建立类
class Boy {
private int no;
private Boy next;
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}
class CircleSingleLinkedList {
// 创建一个first节点,当前没有编号(也可以为空)
private Boy first = null;
}
构造单向环形链表的基本步骤如下:
1.创建第一个节点
初始化链表时,创建一个节点作为头节点,并将其next指针指向自身,形成初始的空环。
2. 添加节点
新节点被添加到环中时,将其next指针指向当前环中的最后一个节点,同时更新最后一个节点的next指针指向新节点,从而保持环形结构。
约瑟夫问题及其解决
约瑟夫问题描述如下:假设n个小孩围成一个圈,从某个小孩开始报数,每次数到第m个小孩时,该小孩被淘汰出局,然后从下一个小孩继续从1开始报数,如此反复,直到只剩最后一个小孩。要求找出最后留下的那个小孩的编号。
使用单向环形链表解决约瑟夫问题的优势在于能够直观地模拟“围成一圈”的情况,并通过节点的移动来模拟报数和淘汰过程。以下是使用Java实现的解决方案:
1.先进行小孩的加入(直接规定小孩的个数,即合并构造单向环形链表的两步骤)
/**
* 添加节点,构建环形链表的函数。
* @param nums 要添加的小孩数量,表示要创建多少个男孩节点。
* 注:该方法不返回任何值,但会修改类内部的环形链表状态。
*/
public void addBoy(int nums) {
if (nums < 1) {
return;
}
//使用for循环来创建环形链表
Boy curBoy = null;// 辅助指针,帮助构建环形链表
for (int i = 1; i <= nums; i++) {
//根据编号创建小孩节点
Boy boy = new Boy(i);
// 如果是第一个小孩
if (i == 1) {
first = boy;
first.setNext(first);// 构成环
curBoy = first;// 让curBoy指向第一个小孩
} else {
// 将当前节点(curBoy)的下一个节点设置
//为新创建的男孩节点(boy)
curBoy.setNext(boy);
// 设置新创建的男孩节点(boy)的下一个节点为
//环形链表的起始节点(first),形成环形结构
boy.setNext(first);
// 更新辅助指针(curBoy)以指向新加入的
//男孩节点(boy),以便后续继续构建环形链表
curBoy = boy;
}
}
}
2.输出出圈号
解析:
创键一个辅助指针helper,事先指向环形链表的最后一节点
小孩报数前,先让first和helper移动
/**
* 遍历环形链表显示出圈小孩标号
* @param startNo 表示从第几个小孩开始数数
* @param countNum 表示数几下
* @param nums 表示最初有多少小孩在圈
解析:
创键一个辅助指针helper,事先指向环形链表的最后一节点
1.小孩报数前,先让first和helper移动startNO-1次
2.当小孩报数时,让first和helper同时移动countNum - 1次
3.这时就可以将first指向的小孩节点出圈了
first = first.next;
helper.next = first;
* */
public void countBoy(int startNo, int countNum, int nums) {
// 先对数据进行校验
if (first == null || startNo < 1 || startNo > nums) {
System.out.println("参数输入有误,请重新输入");
return;
}
// 创建一个辅助指针,帮助完成小孩出圈
Boy helper = first;
while (helper.next == first) {//让helper指向最后
helper = helper.next;
}
//在小孩报数前,先让helper和first移动startNo - 1次
//保证刚开始first指向的是编号为startNo的小孩
for (int i = 0; i < startNo - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//正式报数
while (true) {
if (helper == first) {
break;//完成
}
for (int i = 0; i < countNum - 1; i++) {
//小孩报数,让helper和first移动countNum - 1次
first = first.getNext();
helper = helper.getNext();
}
System.out.printf("小孩%d出圈\n", first.getNo());
//进行出圈
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后留在圈中的小孩编号%d \n", helper.getNo());
}
总结
单向环形链表为约瑟夫问题提供了一个直观且高效的解决方案。通过创建一个表示人群的环形链表,并利用链表操作模拟报数和淘汰过程,我们可以轻松求得最终的幸存者编号。上述Java代码实现了这一逻辑,为解决此类问题提供了实用的编程范例。