【LeetCode】剑指 Offer 62. 圆圈中最后剩下的数字(约瑟夫环问题) p300 -- Java Version

2023-05-16

题目链接:https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/

1. 题目介绍(62. 圆圈中最后剩下的数字)

0,1,···,n-1 这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4 这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

【测试用例】:
在这里插入图片描述

【条件约束】:
在这里插入图片描述

2. 题解

2.1 环形链表(经典解法)-- O(mn)

时间复杂度 O(mn),空间复杂度 O(n)
在这里插入图片描述

解题思路】:
经典解法:用环形链表模拟圆圈

  • 我们可以创建一个共有 n 个节点的环形链表,然后每次在这个链表中删除第 m 个节点

……
这样做的优点就在于:

  • 循环链表的向下枚举不需要考虑头尾问题,直接node=node.next向下;
  • 循环聊表的删除也不需要考虑头尾问题,直接node.next=node.next.next删除。
    ……
    Note:当链表结构中不存在前驱节点时,且需要删除某一节点,我们尽可能的提前遍历到要删除节点的前一节点,就通过node.next=node.next.next 来实现删除,而不是等遍历到删除节点,再考虑删除的问题,因此此时没有前驱节点,你将很难确定你的前驱节点是谁,除非提前创建临时节点将当前节点的前驱记录了下来。
    ……

……
但使用循环链表仍有很多问题:

  • 比如说,该方法每次删除一个数字都需要 m 步的运算,一旦碰见 m 远大于 n 的情况,就可能存在在环形链表中重复遍历很多遍的情况,极大地影响了时间复杂度。

……
实现策略】:

  1. 创建头节点 head,并为其赋值为0;
  2. 创建一个链表 team,通过 for 循环对链表进行初始化;
  3. 让链表的尾节点的后继指向头节点,使链表形成环;
  4. 定义索引变量 index,用来指代当前走了多少步,如果走到了m-1步,就开始删除 m步 所处位置的节点;
  5. 删除结束后,返回剩下节点的值。
class Solution {
    // 定义链表结构
    public class ListNode {
        int val;
        ListNode next;
        ListNode(int x) { val = x; }
    }
    // Solution1:环形链表
    public int lastRemaining(int n, int m) {
        if(m == 1) return n-1;  // 一次一个直接返回最后一个即可
        ListNode head = new ListNode(0);
        ListNode team = head;  // 创建一个链表
        for(int i = 1; i < n; i++)
        {
            team.next = new ListNode(i);
            team = team.next;
        }
        team.next = head;  //使形成环
        int index = 1;  //从1开始计数
        while (head.next != head) {  // 当剩余节点不止一个的时候
            // 如果index = m-1 那就说明下个节点(m)该删除了
            // 因为如果遍历到要删除的节点再删除, 此时由于没有前驱节点,我们无法确定当前节点的上一节点是谁?
            // 一般的办法是定义一个临时遍历pre, 提前存储当前节点的上一节点
            // 但此处直接提前判断, 在要删除的上一节点就执行删除操作,即: 将要删除节点的前驱的后继指向要删除节点的后继
            if(index == m-1)
            {
                head.next = head.next.next;
                // 删除之后, 让index归位, 方便后面重新计算
                index = 1;
            }
            else {
                index++;
            }
            head = head.next;
        }
        return head.val;
    }
}

在这里插入图片描述

2.2 模拟链表 + 求余 – O(n2) ⭐

时间复杂度O(n2),空间复杂度O(n)
在这里插入图片描述

解题思路】:
环形链表中使用链表直接模拟游戏全过程造成了非常严重的超时,n个数字,数到第m个出列,m如果非常大,远大于n,那么将产生很多次重复的转圈。
那么在本题解中,就要来解决这个重复转圈的问题:

  • 首先,很多时候我们完全不需要手动去写一个链表结构,而是可以通过ArrayListLinkedList去模拟一个链表;如果使用LinkedList其底层也是链表,使用ArrayList的话其底层数据结构是数组,此处,我们选择了ArrayList来实现链表的模拟;
  • 其次,由上图我们可以推理出一个公式,index=(index+m-1)%(list.size()),根据此公式,我们可以直接确定下一次要删除的元素的具体位置,从而避开重复的转圈遍历。
    ……
    Note:关于删除元素的位置公式,不理解的可以根据上图,自己走一遍流程。在本题中,数字和下标都是从 0 开始起算,所以第 m 步 走到的是 列表中第 m-1 个元素 (m < n 的情况下);而当我们每次找到并删除一个元素时,由于该元素已被删除,它的下标就自动被下一元素所继承,那还是相当于我们只需要走 m-1 步就到了下一个需要删除的元素的位置;同时,我们结合环形链表的特质,通过 位置 与 列表长度 的求余,就可以去掉多余的重复循环,直接找到 要删除元素在列表中的位置。
    ……

……
实现策略】:

  1. 创建列表 list,用来模拟链表;
  2. 初始化列表,将 0 ~ n-1的元素全部加入到列表中;
  3. 循环根据位置公式 idx = (idx+m-1) % list.size() 删除元素,直至列表中剩下最后一个元素;
  4. 最后返回结果。
class Solution {
    // Solution2:模拟链表
    public int lastRemaining(int n, int m) {
        if(m == 1) return n-1; // 一次一个直接返回最后一个即可
        List<Integer> list = new ArrayList<>(); // 创建列表,用来模拟链表
        for (int i = 0; i < n; i++) { // 初始化列表
            list.add(i);
        }
        int idx = 0;
        while (list.size() > 1) { // 循环删除元素,直至剩下最后一个
            idx = (idx+m-1) % list.size(); // 根据公式,我们可以找到每次要删除的元素的位置
            list.remove(idx); 
        }

        return list.get(0);
    }
}

在这里插入图片描述

2.3 数学分析 – O(n)

时间复杂度O(n),空间复杂度O(1)
在这里插入图片描述

解题思路】:
说实话数学题解法,确实又快又好,但难的是怎么推出来,如果碰到其它问题,且之前没有接触过,一般人很难在很短的时间内就能得到正确的推理结果,所以这种数学解法看看,锻炼锻炼思维就好,不要求掌握。
……
推理公式:f(n,m) 指n个人,报第m个编号出列最终编号
在这里插入图片描述
以 n = 10,m = 9 为例:最后结果记为 f(10,3)
在这里插入图片描述
那么其转化过程就为:

  • f(10,3)=(f(9,3)+3)%10
  • f(9,3)=(f(8,3)+3)%9
    ……
  • f(2,3)=(f(1,3)+3)%2
  • f(1,3)=0

……
实现策略】:

  • 根据公式即可轻松写出代码;
class Solution {
    // Solution3:数学分析
    public int lastRemaining(int n, int m) {
        int value=0;
            for(int i=1;i<=n;i++)
            {
                value=(value+m)%i;
            }
            return  value;
    }
}

在这里插入图片描述

3. 参考资料

[1] 圆圈中最后剩下的数字(图解三种方法) – bigsai,该题解在循环链表这一方面讲的很清楚.
[2] Java解决约瑟夫环问题,告诉你为什么模拟会超时!-- Sweetiee🍬的小号,对数学分析题解进行了比较清晰的描述,值得参考.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【LeetCode】剑指 Offer 62. 圆圈中最后剩下的数字(约瑟夫环问题) p300 -- Java Version 的相关文章

随机推荐