2-6约瑟夫环

痞子三分冷 提交于 2020-02-29 17:05:30

题目描述

在这里插入图片描述
在这里插入图片描述

解题方法1

  • 如果链表数据为 10 20 30 40 50 60 70 80 90 。那么按照如上规则,节点的删除顺序应该为 30 60 90 40 80 50 20 70 最后剩下的节点为10。
  • 最常规的方法,我们只需要设置一个计数器count,然后每遍历一个节点count加一,当count==m时删除当前节点并让count=0,不断遍历节点和删除直到剩下最后一个节点退出循环。
  • 需要注意的是这里使用不带头节点的链表,避免头节点对问题的干扰。
  • 每次要遍历m次才能找到一个要删除的节点,链表一共有n个节点,整个过程的时间复杂度为 m*n。
public class Test {
    public static void main(String[] args) throws Exception {
        int[] arr = {10,20,30,40,50,60,70,80,90};
        Node head = create(arr);
        yuesefu(head,2);
    }
    public static Node yuesefu(Node head,int m){
        if(head==null || head.next==head || m<0){
            return head;
        }
        Node p = head;
        Node pre = p;
        while(pre.next!=p){
            pre = pre.next;
        }
        int count = 0; //计数器
        //只剩一个节点时退出循环
        while(pre!=p){
            count++;
            if(count==m){
                System.out.println("删除节点:"+p.val);
                pre.next = p.next; //删除指定节点
                count = 0;
                p = pre.next;
            }
            else{
                pre = pre.next;
                p=pre.next;
            }
        }
        System.out.println("剩余节点:"+p.val);
        return p;
    }

    //创建单循环链表 为了方便解决约瑟夫环问题这里不带头节点
    public static Node create(int[] arr){
        Node head = new Node(0); //头节点
        Node newnode = null; //指向新节点
        Node tail = head; //指向链表尾节点
        for(int a:arr){
            newnode = new Node(a);
            newnode.next = tail.next;
            tail.next = newnode;
            tail = newnode;
        }
        tail.next = head.next;
        return head.next;
    }
}
class Node{
    int val;
    Node next;
    Node(int val){
        this.val = val;
    }
}

解题方法2

  • 上述解题方法的时间复杂度为m*n并不符合题目要求的n的时间复杂度。我们必须要删除一个节点之后才能知道下一个要删除的节点是哪一个,所以比较花费时间。

  • 我们可以通过公式法直接推出来最终幸存的节点编号,然后再依次删除其他节点,把幸存节点作为头节点返回。

  • 为了方便导出递推式,我们重新定义一下题目。问题: N个人编号为0,1,2……,n-1,依次报数,每报到m时,杀掉那个人,求最后被杀的人的编号。

  • 现在假设共有十个人,m为4,我们每杀掉一个人就从下一个人开始重新进行编号,如下为整个杀人过程。
    在这里插入图片描述

  • 从图中可以直观地看出最后被杀的人的编号为4,但是如何利用程序来进行判断呢?

  • 其实可以发现9人环中第一个被杀的人其实就是10人环中第二个被杀的人,8人环中第一个被杀的人其实就是10人环中第三个被杀的人,以此类推1人环中第一个被杀的人就是10人环中最后一个被杀的人。

  • 我们我们试想一下能不能,通过1人环的下标0推出2人环的下标0 >> 三人环的下标1 >> 4人环的下标1 >>>> 10人环的下标4。

  • 现在比较麻烦的事情在于,每次删除一个人后剩下的所有人的下标都会重新进行排列,不过每次下标的变化都是有规律的,只要能够找到k-1人环中每个人下标与k人环中每个人下标的对应关系。我们就能通过递归的方式计算出1人环第一个杀掉的人在10人环中的位置。

  • 如何通过k-1人环的被杀的人编号,推出它在k人环中的编号。

//我们来看看10人环变成9人环编号变化的情况
     0 1 2 3 4 5 6 7 8 9
旧环(k环):  0 1 2   4 5 6 7 8 9
新环(k-1环):6 7 8   0 1 2 3 4 5
//可以看出旧环映射为新环其实就相当于把旧环中每个编号循环左移了m个位置
//那么反过来从新环映射到旧环就是把新环中每个编号循环右移m个位置
//k环到k-1环表示为数学公式就是:old = (new+m)%k,new表示k-1环的编号,old表示k环对应的编号
  • 现在我们得到了一个公式和一个结论:
    old = (new+m)%k,new表示k-1环的编号,old表示k环对应的编号
    k环中第最后一个被杀的人 = k-1环中最后个被杀的人 = … = 1环编号为0那个人

  • 我们将k环中最后一个被杀的人的编号记为函数: fun(int k , int m),那么就得到了最终的递推公式:
    fun(k,m) = (fun(k-1,m)+m)%k
    fun(1,m) = 0;

  • 最终可以得到求k个人中最后一个被杀的人的递归函数

//传入总人数k和报数上限m 返回最后一个被杀的编号 假设将所有节点从0开始编号
 public static int fun(int k,int m){
     if(k==1)
         return 0;
     return (fun(k-1,m)+m)%k;
 }
  • 求出最后一个被杀人的编号后,找到此编号对应节点作为头节点返回,删除其他节点即可
public class Test {
    public static void main(String[] args) throws Exception {
        int[] arr = {10,20,30,40,50,60,70,80,90,100,110};
        Node head = create(arr);
        head = yuesefu(head,3);
        System.out.println("幸存者:"+head.val);

    }
    public static Node yuesefu(Node head,int m){
        if(head==null || head.next==head || m<0){
            return head;
        }
        int len=1;
        for(Node p=head.next;p!=head;p=p.next){
            len++;
        }
        int num = fun(len,m)+1; //编号num的节点就是链表第num+1个节点
        //找出链表第num个节点
        int i = 1;
        while(i<num){
            head = head.next;
            i++;
        }
        head.next = head;
        return head;
    }

   //传入总人数k和报数上限m 返回最后一个被杀的编号 假设将所有节点从0开始编号
    public static int fun(int k,int m){
        if(k==1)
            return 0;
        return (fun(k-1,m)+m)%k;
    }

    //创建单循环链表 为了方便解决约瑟夫环问题这里不带头节点
    public static Node create(int[] arr){
        Node head = new Node(0); //头节点
        Node newnode = null; //指向新节点
        Node tail = head; //指向链表尾节点
        for(int a:arr){
            newnode = new Node(a);
            newnode.next = tail.next;
            tail.next = newnode;
            tail = newnode;
        }
        tail.next = head.next;
        return head.next;
    }
}
class Node{
    int val;
    Node next;
    Node(int val){
        this.val = val;
    }
}

题目延申

  • 通过以上解法我们可以求出k个人中,最后一个被杀的人的编号。现在引申一下,能否求出k个人中第i个人被杀的编号。

  • 我们回过头再看一下这张图
    在这里插入图片描述

  • 我们在求10人环最后一个被杀的人是通过1人环第一个被杀的人依次往上递推最终得到10人环中第10个被杀的人的编号。

  • 其实道理完全一样
    求10人环第一个被杀的人,直接计算即可。
    求10人环第二个被杀的人,先求出9人环第一个被杀的人再往上递推。
    求10人环第三个被杀的人,先求出8人环第一个被杀的人再往上递推。
    求10人环第i个被杀的人,先求出10-i+1环第一个被杀的人再往上递推。
    求k人环第i个被杀的人,先求出k-i+1环第一个被杀的人再往上递推。

  • 所以此题与上一题唯一的区别就是,上一题从1人环编号0开始递推,此题从k-i+1环第一个被杀的人的编号开始递推。

  • 只要在上一题的基础上弄清楚k人环报数上限为m的情况下,第一个被杀掉的人在该环的位置即可。

  • 当k大于m的时候,第一个被杀的人的编号很明显是m-1。

  • 当k小于等于m的时候,第一个被杀的人的编号为可以举例观察:
    如果k=3 m=3,被杀的编号为2
    如果k=3 m=4,被杀的编号为0
    如果k=3 m=5,被杀的编号为1
    如果k=3 m=6,被杀的编号为2

  • 可以推出,当k小于m的时候,第一个被杀的人的编号为(m+k-1)%k。而且此公式对k大于m的情况也通用。

  • 综上:k环中第一个被杀编号,(m+k-1)%k。

  • 又知,old = (new+m)%k,new表示k-1环的编号,old表示k环对应的编号。k环中第i个被杀的人为k-1环第i-1个被杀的人。

  • 设k环中在报数上限为m的情况下,第i个被杀的人编号为函数 fun(int k,int m,int i),则

fun(k,m,i) = (fun(k-1,m,i-1)+m)%k
fun(k,m,1) = (m+k-1)%k
  • 则最终可以得到求k个人中第i个被杀的人的递归函数
public static int fun(int k,int m,int i){
    if(i==1)
        return (m+k-1)%k;
    return (fun(k-1,m,i-1)+m)%k;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!