程序员代码面试指南第二版 17.环形链表的约瑟夫问题(普通和进阶)

welcome to my blog

程序员代码面试指南第二版 17.环形链表的约瑟夫问题(普通和进阶)

题目描述
普通版:时间复杂度要求O(n*m)
进阶版:时间复杂度要求O(n)

据说著名犹太历史学家 Josephus 有过以下故事:在罗马人占领乔塔帕特后,39 个犹太人与 Josephus 及他的朋友躲到一个洞中,39 个犹太人决定宁愿死也不要被敌人抓到,
于是决定了一种自杀方式,41 个人排成一个圆圈,由第 1 个人开始报数,报数到 3 的人就自杀,然后再由下一个人重新报 1,报数到 3 的人再自杀,这样依次下去,
直到剩下最后一个人时,那个人可以自由选择自己的命运。这就是著名的约瑟夫问题。现在请用单向环形链表得出最终存活的人的编号。

输入描述:
一行两个整数 n 和 m, n 表示环形链表的长度, m 表示每次报数到 m 就自杀。

输出描述:
输出最后存活下来的人编号(编号从1开始到n)

示例1

输入
5 2

输出
3
普通版
第一次做, 环形链表的形式, 比数组方便处理, 因为不用处理索引越界问题; 用一个指针指向待删除节点的前一个节点; 删除节点后不要更新curr, 因为curr指向的是当前计数节点的前一个节点, 看注释; 一共进行n-1次循环, 内循环至少执行m次, 所以时间复杂度是O(n*m)
import java.util.Scanner;
import java.util.ArrayList;

public class Main{
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        String s = sc.nextLine();
        int n = Integer.parseInt(s.split(" ")[0]);
        int m = Integer.parseInt(s.split(" ")[1]);
        //input check
        if(n<1 || m<1)
            System.out.println(-1);
        if(n==1)
            System.out.println(1);
        //创建环形链表
        ListNode head = new ListNode(1);
        ListNode curr = head;
        for(int i=2; i<=n; i++){
            curr.next = new ListNode(i);
            curr = curr.next;
        }
        curr.next = head;
        //开始解决约瑟夫问题; 需要找到待删除节点的前一个节点
        int count = 1;
        //当前curr指向编号最大的那个人
        while(curr.next != curr){
            while(count!=m){
                //execute
                count++;
                //update
                curr = curr.next;
            }
            //删除节点
            curr.next = curr.next.next;
            //update
            //删除节点后不能更新curr, 因为curr指向的是待删除的前一个节点, 不能更新成计数时的第一个节点
            //curr = curr.next;
            count = 1;
        }
        System.out.println(curr.val);
    }
    public static class ListNode{
        int val;
        ListNode next;
        ListNode(int val){
            this.val = val;
        }
    }
}
进阶版
在O(n)时间复杂度中解决问题; 核心: 弄明白编号和报数的函数关系, 老编号和新编号的函数关系; 老编号指的是总节点数更多时的编号, 新编号指的是总节点数更少时的编号; 获得幸存者在最初链表中的编号的函数可以通过自顶向下的递归函数实现或者通过自底向上的循环实现
  • 编号 = (报数 - 1) % i + 1, 其中i表示链表中节点总个数
  • 老编号 = (新编号 + m -1) % i + 1, 其中m表示数到m的自杀, i是老编号所在链表的节点总个数, 这个公式的作用就是, 当我们知道了新编号后, 就可以推出新编号对应的老编号; 幸存节点最后的编号一定是1, 因为到最后的时候链表中只有幸存节点一个了, 于是就可以推出链表中节点有两个的时候幸存节点的编号; 知道了幸存节点在节点总数为2时的编号后, 就能推出幸存节点在节点总数为3时的编号了; 知道了幸存节点在节点总数为3时的编号后, 就能推出幸存节点在节点总数为4时的编号了; 以此类推, 直到知道了幸存节点在节点总数为n-1时的编号后, 就能推出幸存节点在节点总数为n时的编号了
import java.util.Scanner;

public class Main{
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        String s = sc.nextLine();
        int n = Integer.parseInt(s.split(" ")[0]);
        int m = Integer.parseInt(s.split(" ")[1]);
        //input check
        if(n<=0 || m<1)
            return;

        //创建环形链表
        ListNode head = new ListNode(1);
        ListNode curr = head;
        for(int i=2; i<=n; i++){
            curr.next = new ListNode(i);
            curr = curr.next;
        }
        curr.next = head;

        //获取幸存节点在最初链表中的编号, 已知幸存节点在最后链表中的编号一定是1, 因为那时候环形链表中只剩它自己一个了
        int survivorNumInOld = 0;
        survivorNumInOld = getNumber(n, m);
        //在这里直接输出survivorNumInOld也对, 不过还是按照环形链表的方式走一遍吧
        //System.out.println(survivorNumInOld);

        int count = survivorNumInOld;
        curr = head;
        while(survivorNumInOld - 1 > 0){
            curr = curr.next;
            //update
            survivorNumInOld--;
        }
        curr.next = curr;
        System.out.println(curr.val);

    }
    /*
    老编号和新编号的关系
    old = (new + m - 1) % i + 1
    其中, i表示老编号所在链表的节点总个数(想不起来可以看看书上的图,54页)
    
    递归函数逻辑:返回节点总数为n, 被杀报数为m时最后幸存节点在节点总数为n时的编号
    */
    //递归版, 自顶向下, 在牛客上只能通过28.57%的案例
    public static int getNumber(int n, int m){
        //base case; 链表中只有幸存节点时, 幸存节点的编号一定是1, 所以返回1
        if(n==1)
            return 1;
        int newNumber = getNumber(n-1, m);
        //通过新编号推出, 该编号在长度加一的链表中的编号
        return (newNumber + m - 1) % n + 1;
    }
    //循环版, 自底向上
    public static int getNumber2(int n, int m){
        //幸存者最后的编号一定是1
        int res = 1;
        //通过循环不断求出幸存者在更长的链表中的编号
        for(int i=2; i<=n; i++){
            res = (res + m - 1) % i + 1;
        }
        return res;
    }
    public static class ListNode{
        int val;
        ListNode next;
        ListNode(int val){
            this.val = val;
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值