约瑟夫问题和单向环形链表

约瑟夫问题

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

另一个场景,17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:15个教徒和15 个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。

约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3。1

解题思路

1. 使用单向环形链表模拟游戏过程

1.1 单向环形链表

单向链表总是有一个指针指向下一个元素.
单向链表

单向环形链表最后一个元素循环指向第一个元素,形成闭环。
单向环形链表

1.2 定义一个单向环形链表模拟约瑟夫问题
/**
 * 约瑟夫问题与单向环形列表
 */
public class JosepfuQuestion {
    private Node first;
    private Node last;

    public JosepfuQuestion() {
    }
    public JosepfuQuestion(Integer amount) {
        addAll(amount);
    }

    /**
     * 初始化一个环形链表
     * @param amount 链表长度
     * @return
     */
    public boolean addAll(Integer amount) {
        if (first != null) {
            System.err.println("链表已经初始化过了!");
        }
        if (amount == null || amount < 1) {
            System.err.println("错误的初始化参数");
            return false;
        }
        for (int i = 0; i < amount; i++) {
            Node node = new Node(i + 1);
            if (first == null) {
                last = first = node;
                first.setNext(last);
            }else{
                last.setNext(node);
                last = node;
                last.setNext(first);
            }
        }
        return true;
    }

    /**
     *  展示链表中所有数据
     */
    public void display() {
        if (first == null) {
            System.err.println("链表为空");
            return;
        }
        Node tmp = first;
        while (tmp.hasNext()) {
            System.out.println("当前对象编号:" + tmp.getNo());
            tmp = tmp.getNext();
            if (first == tmp) {
                break;
            }
        }
    }

    /**
     * 游戏开始 默认从第一个人开始报数
     * @param outIndex 第几人出局
     * @param amount 一共有几人
     * @return
     */
    public String doPlay(int outIndex, int amount) {
        return doPlay(1, outIndex, amount);
    }
    public String doPlay(int startIndex, int outIndex, int amount) {
        // 初始化链表
        this.addAll(amount);

        // 1. 找到开始的元素
        Node startNode = first;
        for (int i = 1; i < startIndex; i++) {
            startNode = startNode.getNext();
        }
        // 2. 找到当前元素的前一个元素, 初始时即循环至末尾
        Node preNode = startNode;
        while (true) {
            if (preNode.getNext() == startNode) {
                // 如果前一个节点的下一个节点就是开始节点, 则已经找到开始元素的前一个元素
                break;
            }
            preNode = preNode.getNext();
        }

        int times = outIndex - 1;

        while (true) {
            // 3. 将开始元素和其前一个元素同时向后移动 times-1 次
            for (int i = 0; i < times; i++) {
                startNode = startNode.getNext();
                preNode = preNode.getNext();
            }
            if (startNode == preNode) {
                // 向后移动后发现前一个元素和删除元素重合则表示幸存者出现
                System.out.println("幸存者产生, 编号为: " + startNode.getNo());
                break;
            }
            // 此时的startNode就是需要移除的元素
            System.out.println("即将移除元素: " + startNode.getNo());
            // 4. 移除,将前一个元素指向删除元素的下一个元素即可
            preNode.setNext(startNode.getNext());

            // 5. 令下一个元素为新的开始元素  继续循环
            startNode = startNode.getNext();
        }
        return "幸存者是:" + startNode.getNo();
    }



    private static class Node {
        private Integer no;
        private Node next;

        public Node() {
        }

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

        public Node(Integer no, Node next) {
            this.no = no;
            this.next = next;
        }

        public boolean hasNext() {
            return this.next != null;
        }

        public Integer getNo() {
            return no;
        }

        public void setNo(Integer no) {
            this.no = no;
        }

        public Node getNext() {
            return next;
        }

        public void setNext(Node next) {
            this.next = next;
        }
    }
}

1.3 验证
    // ....
    @Test
    public void test1() {
        JosepfuQuestion josepfuQuestion = new JosepfuQuestion();
        josepfuQuestion.doPlay(1, 3,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
     *
     * Process finished with exit code 0
     */
    

  1. 场景描述来自百度百科 ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值