《Java数据结构与算法》进阶篇:约瑟夫问题

约瑟夫问题(有时也称为约瑟夫斯置换,是一个计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。又称“丢手绢问题”。)

历史典故

据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

引申出问题

通过上面的这个故事,我们可以用一个简单的问题来表述:
设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出圈,它的下一位又从1开始报数,数到m的那个人又出圈,依次类推,直到所有人出圈为止,由此产生一个出圈编号的序列。

思路分析

用一个不带头结点的单向环形链表来处理约瑟夫问题:先构成一个有n个结点的单向环形链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。

下面我用示意图简单描述以下这个出圈过程:
在这里插入图片描述
在这里插入图片描述
最终出圈的结果是:2->4->1->5->3,整个过程还是比较简单、清晰的。
接下来,我将通过以下3个步骤,完整解析如何处理该问题:

  1. 构造单向环形链表
  2. 遍历环形链表
  3. 报数出圈

实现步骤

完整代码如下,大家可以搭配下面的详细解析步骤,慢慢理解。O(∩_∩)O哈哈~

/**
 * 约瑟夫问题
 *
 * @author 一生都在战斗
 * @date 2021-07-01 5:27 下午
 */
public class Josephus {

    public static void main(String[] args) {

        CircleSingleLinkedList list = new CircleSingleLinkedList();
        list.add(41);
        System.out.println("遍历圈中编号 =========================》");
        list.show();
        System.out.println("开始报数出圈 =========================》");
        list.out(1, 3);
    }

}


class CircleSingleLinkedList {

    /**
     * 构造辅助指针first
     */
    private Node first = null;

    /**
     * 生成指定人数的环形队列
     *
     * @param num 环形队列人数
     */
    public void add(int num) {
        // 构造一个辅助指针cur,每次都指向最新的节点
        Node cur = null;
        for (int i = 1; i <= num; i++) {
            // 生成新节点
            Node node = new Node(i);
            if (i == 1) {
                // 当前节点是第一个节点
                first = node; // 给first指针赋值
                cur = node; // cur指针指向最新的节点
                cur.next = first; // 新节点指向first节点
            } else {
                cur.next = node; // 将cur节点的next指向新节点,也就意味着新节点加入到了环形链表中
                cur = node; // 将cur指针后移,指向最新加入的节点
                cur.next = first; // 将新加入节点的next指向first节点,重新形成新的环
            }
        }

    }


    public void show() {
        Node cur = first;
        for (; ; ) {
            System.out.println("当前编号:" + cur.no);
            if (cur.next == first) {
                break;
            }
            cur = cur.next;
        }
    }


    /**
     * 报数出圈
     *
     * @param startNo 开始报数的人员编号
     * @param count   报数为count的人出圈
     */
    public void out(int startNo, int count) {

        // 构造辅助指针help
        Node help = first;

        for (; ; ) {
            // 将help指针指向环形链表的最后一个节点
            if (help.next == first) {
                break;
            }
            help = help.next;
        }

        for (int i = 0; i < startNo - 1; i++) {
            // 将first、help指针先移动到指定的"开始报数的人员编号"的节点处
            first = first.next;
            help = help.next;
        }

        for (; ; ) {
            if (first == help) {
                // 当first和help指向同一个节点时,说明此时环形链表只剩下最后一个节点了
                System.out.println("圈中最后一个人编号:" + first.no);
                break;
            }
            for (int i = 0; i < count - 1; i++) {
                // 将first、help移动指定的"count-1"次
                first = first.next;
                help = help.next;
            }
            // 移动完成后,first指向的节点就是需要出圈的编号
            System.out.println("出圈编号:" + first.no);
            first = first.next;
            help.next = first;
        }
    }


}

class Node {
    int no;
    Node next;

    public Node(int no) {
        this.no = no;
    }
}

构造单向环形链表

  1. 首先我们需要一个辅助指针first
  2. 然后我们创建第一个节点,让first始终指向这第一个节点,并且形成环
  3. 然后我们还需要一个辅助指针cur,指向最新加入的节点
  4. 后面新增节点时,只需要把该节点加入环形链表(cur.next=新节点),再向后移动cur指针,并且让新节点指向first节点,再次形成一个环
    流程示意图如下:

在这里插入图片描述
在这里插入图片描述
后面的节点加入时,以此类推。。。
下面来看代码:

  1. 创建节点类
class Node {
    int no;
    Node next;

    public Node(int no) {
        this.no = no;
    }
}
  1. 创建环形链表类
class CircleSingleLinkedList {

    /**
     * 构造辅助指针first
     */
    private Node first = null;
}
  1. 生成指定人数的环形队列
/**
 * 生成指定人数的环形队列
 *
 * @param num 环形队列人数
 */
public void add(int num) {
    // 构造一个辅助指针cur,每次都指向最新的节点
    Node cur = null;
    for (int i = 1; i <= num; i++) {
        // 生成新节点
        Node node = new Node(i);
        if (i == 1) {
            // 当前节点是第一个节点
            first = node; // 给first指针赋值
            cur = node; // cur指针指向最新的节点
            cur.next = first; // 新节点指向first节点
        } else {
            cur.next = node; // 将cur节点的next指向新节点,也就意味着新节点加入到了环形链表中
            cur = node; // 将cur指针后移,指向最新加入的节点
            cur.next = first; // 将新加入节点的next指向first节点,重新形成新的环
        }
    }
}

遍历环形链表

遍历过程比较简单,同样需要一个辅助指针cur,cur初始指向同first一致,接着就可以循环遍历,每执行一次,cur指针后移一位,当cur.next=first时,说明遍历结束了。

public void show() {
    Node cur = first;
    for (; ; ) {
        System.out.println("当前编号:" + cur.no);
        if (cur.next == first) {
            break;
        }
        cur = cur.next;
    }
}

报数出圈

  1. 首先我们需要一个辅助指针help,指向环形链表的最后一个节点
    在这里插入图片描述

  2. 根据题目中指定的从编号为k的人开始报数,我们需要将first、help指针同时移动k-1次。
    在这里插入图片描述

  3. 开始报数,将first、help指针同时移动m-1次,这时first指针指向的节点就是需要出圈的节点,(这里移动m-1次的原因同上)

  4. 找到需要出圈的节点后,我们只需要变动first和help的指针指向即可:
    first=first.next
    help.next=first

    原来first指向的节点就会因为没有任何引用,而被GC回收
    在这里插入图片描述

/**
 * 报数出圈
 * @param startNo 开始报数的人员编号
 * @param count 报数为count的人出圈
 */
public void out(int startNo, int count) {

    // 构造辅助指针help
    Node help = first;

    for (; ; ) {
        // 将help指针指向环形链表的最后一个节点
        if (help.next == first) {
            break;
        }
        help = help.next;
    }

    for (int i = 0; i < startNo - 1; i++) {
        // 将first、help指针先移动到指定的"开始报数的人员编号"的节点处
        first = first.next;
        help = help.next;
    }

    for (; ; ) {
        if (first == help) {
            // 当first和help指向同一个节点时,说明此时环形链表只剩下最后一个节点了
            System.out.println("圈中最后一个人编号:" + first.no);
            break;
        }
        for (int i = 0; i < count - 1; i++) {
            // 将first、help移动指定的"count-1"次
            first = first.next;
            help = help.next;
        }
        // 移动完成后,first指向的节点就是需要出圈的编号
        System.out.println("出圈编号:" + first.no);
        first = first.next;
        help.next = first;
    }
}

代码测试

  1. 首先我们来看上面5个人出圈的问题,main方法代码如下设置:
public static void main(String[] args) {

    CircleSingleLinkedList list = new CircleSingleLinkedList();
    list.add(5);
    System.out.println("遍历圈中编号 =========================》");
    list.show();
    System.out.println("开始报数出圈 =========================》");
    list.out(1, 2);
}

打印结果:

遍历圈中编号 =========================》
当前编号:1
当前编号:2
当前编号:3
当前编号:4
当前编号:5
开始报数出圈 =========================》
出圈编号:2
出圈编号:4
出圈编号:1
出圈编号:5
圈中最后一个人编号:3

结果符合预期。

  1. 我们再来看开篇的约瑟夫问题,设置相关参数,打印结果如下:
遍历圈中编号 =========================》
当前编号:1
当前编号:2
当前编号:3
当前编号:4
当前编号:5
当前编号:6
当前编号:7
当前编号:8
当前编号:9
当前编号:10
当前编号:11
当前编号:12
当前编号:13
当前编号:14
当前编号:15
当前编号:16
当前编号:17
当前编号:18
当前编号:19
当前编号:20
当前编号:21
当前编号:22
当前编号:23
当前编号:24
当前编号:25
当前编号:26
当前编号:27
当前编号:28
当前编号:29
当前编号:30
当前编号:31
当前编号:32
当前编号:33
当前编号:34
当前编号:35
当前编号:36
当前编号:37
当前编号:38
当前编号:39
当前编号:40
当前编号:41
开始报数出圈 =========================》
出圈编号:3
出圈编号:6
出圈编号:9
出圈编号:12
出圈编号:15
出圈编号:18
出圈编号:21
出圈编号:24
出圈编号:27
出圈编号:30
出圈编号:33
出圈编号:36
出圈编号:39
出圈编号:1
出圈编号:5
出圈编号:10
出圈编号:14
出圈编号:19
出圈编号:23
出圈编号:28
出圈编号:32
出圈编号:37
出圈编号:41
出圈编号:7
出圈编号:13
出圈编号:20
出圈编号:26
出圈编号:34
出圈编号:40
出圈编号:8
出圈编号:17
出圈编号:29
出圈编号:38
出圈编号:11
出圈编号:25
出圈编号:2
出圈编号:22
出圈编号:4
出圈编号:35
出圈编号:16
圈中最后一个人编号:31

最后两个编号为16和31,跟历史典故的结果也一致。

完结撒花!!!
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值