【面试题】圆圈中最后剩下的数字(环形链表法+数学法)

题目描述

在这里插入图片描述

解法一:模拟环形链表

这道题如果模拟实现的话其实考的是「循环单向链表」(链表尾指向链表头部,且每个结点只有一个后继结点),由于 Java 里面没有现成的类库可以使用,因此先尝试用 LinkedList 发现超时,然后用 ArrayList。

Java

class Solution {
    public int lastRemaining(int n, int m) {
        /*
        若使用 LinkedList 会超时
        LinkedList 虽然删除指定节点的时间复杂度是 O(1)的,但是在 remove 时间复杂度仍然是 O(n)的,因为需要从头遍历到需要删除的位置。
        而 ArrayList直接按索引找到需要删除的位置,时间复杂度是 O(1),但 remove的整体时间复杂度是 O(n),因为后续元素需要向前搬运。
        看起来二者单次删除操作的时间复杂度一样,但是
        ArrayList 的 remove操作在搬运后续元素时,其实是内存连续空间的拷贝!所以相比于LinkedList大量非连续性地址访问,ArrayList的性能更优。
        */
        ArrayList<Integer> list = new ArrayList<> ();
        
        // 将 n个数添加到链表中
        for(int i=0; i<n; i++) {
            list.add(i);
        }
        // 每次删除第 m个数字,直到链表中只剩下一个元素
        int idx = 0;
        while(n>1) {
            idx = (idx + m -1) % n; // 通过数学公式计算得到下一个要删除的位置
            list.remove(idx);
            n--;
        }
        // 返回链表中剩下的唯一元素
        return list.get(0);
    }
}

复杂度分析:

  • 时间复杂度 O(n2):每次删除的时间复杂度是 O(n),删除了 n-1 次,所以整体时间复杂度是 O(n2)。
  • 空间复杂度 O(n):需要一个辅助链表来模拟圆圈。

解法二:数学法

  • 定义⼀个关于 n和 m的方程 f(n,m) ,表示在 n个数字 0,1,…,n-1中不断删除第 m个数字最后剩下的数字。

  • 第一个被删除的数字是(m-1)%n (取余的原因是m可能比n大,m-1是因为 n个数是从 0 开始计数), 我们记作k,k=(m-1)%n

  • 那么删除 k 后剩下的n-1个数字就变成了:0,1,……k-1, k+1,……,n-1,并且下一次删除从数字 k+1开始计数,我们把下一轮第一个数字即 k+1 排在剩下的序列中的最前面,并且将这个长度为 n-1的数组映射到 0~n-2 这个序列范围内。
    在这里插入图片描述

  • 把映射数字记为x,原始数字记为y,那么映射数字变回原始数字的公式为:y = (x + k+1) % n

  • 在映射的数字序列中,从 n-1个数字中不断删除第 m个数字,由前面定义可以知道,最后剩下的数字为 f(n-1,m)。我们把它映射回原始数组中的数字,由上一个公式可以得到最后剩下的原始数字是:(f(n-1,m) + k+1) % n,这个数字也就是一开始我们标记的整个序列中最后剩下的数字 f(n,m)。因此,可以得到递推公式为:f(n,m) = (f(n-1,m) + k+1) % n。

  • 将 k=(m-1)%n 代入简化得,f(n,m) = (f(n-1,m) + m) % n 且 f(1, m) = 0
    (注意:在数字中,(a%n+b)%n=(a+b)%n,f(1, m) = 0 则代表 n=1即系列中一开始只有一个数字 0,很显然最后剩下的数字就是 0)。

递归实现

class Solution {
    public int lastRemaining(int n, int m) {
        return recur(n, m);
    }

    public int recur(int n, int m) {
        if(n == 1) return 0; // 递归终止条件
        return (recur(n-1, m) + m) % n; // 递推公式
    }
}

复杂度分析:

  • 时间复杂度 O(n):需要求解的函数有 n个,递归 n次。
  • 空间复杂度 O(n):递归函数调用栈的深度为 n。

迭代实现

class Solution {
    public int lastRemaining(int n, int m) {
        int res = 0;
        for(int i=2; i<=n; i++) // 从 i=2 开始,是因为 i=1 时只要 m>0 直接返回 0 即可;i<=n 是因为需要一直递推到 n个数的情况
            res = (res + m) % i;
        return res;
    }
}

复杂度分析:

  • 时间复杂度 O(n):for循环执行 n次。
  • 空间复杂度 O(1):只需要一个变量。

参考

https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-by-lee/

https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/si-chong-fang-fa-xiang-xi-jie-da-by-yuanninesuns/

https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/javajie-jue-yue-se-fu-huan-wen-ti-gao-su-ni-wei-sh/

最详细约瑟夫环数学推导

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值