剑指 Offer 62. 圆圈中最后剩下的数字
一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情。
1、题目📑
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
实例1:
输入: n = 5, m = 3
输出: 3
实例1:
输入: n = 10, m = 17
输出: 2
限制:
1 <= n <= 10^51 <= m <= 10^6
2、思路🧠
方法一:模拟链表
假设当前删除的位置是 k,下一个删除的数字的位置是 k + m 。由于把当前位置的数字删除之后,删除之后的后面的数字会前移一位,所以实际的下一个位置是 k + m - 1 。但是数字遍历到末尾会从头继续开始报数,所以最后取模来模拟循环从头开始,也就是 (k + m − 1) (mod n)。
为什么LinkedList会超时?
看起来 LinkedList 和 ArrayList 删除操作的时间复杂度是一样的,但是在 ArrayList 的 remove 操作在后续移位的过程中,内存连续空间的拷贝的。所以相比于 LinkedList 大量非连续性地址访问,ArrayList 的性能是最合理的!
方法二:数学
最后只剩下一个元素,假设这个最后存活的元素为 num,这个元素最终的的下标一定是0 ,原因是最后只剩这一个元素,所以如果可以推出上一轮次中 num 的下标,然后根据上一轮 num 的下标推断出上上一轮 num 的下标, 直到推断出元素个数为n的那一轮 num 的下标,那我们就可以根据这个下标获取到最终的元素了。推断过程如下:
- 已知,最后一轮中
num的下标一定是0 - 上一轮应该是有两个元素,此轮次中
num的下标为(0 + m)%n = (0+3)%2 = 1,说明这一轮删除之前num的下标为1 - 再上一轮应该有3个元素,此轮次中 num 的下标为
(1+3)%3 = 1,说明这一轮某元素被删除之前num的下标为1 - 再上一轮应该有4个元素,此轮次中 num 的下标为
(1+3)%4 = 0,说明这一轮某元素被删除之前num的下标为0 - 再上一轮应该有5个元素,此轮次中 num 的下标为
(0+3)%5 = 3,说明这一轮某元素被删除之前num的下标为3 - ……
因为我们要删除的序列为 0~n-1 , 所以求出下标也就意味着求出了结果。例如 n=5 时,num 的初始下标为3,所以 num 就是3,换句话说就是从 0~n-1 的序列中, 经过 n-1 轮的胜劣淘汰,3 元素最终存活下来了。
推导公式: (此轮过后的 num下标 + m) % 上轮元素个数 = 上轮 num 的下标
废话少说 ~~~~~ 上代码!
3、代码👨💻
第一次commit AC
class Solution {
public int lastRemaining(int n, int m) {
ArrayList<Integer> list = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
list.add(i);
}
int k = 0;
while (n > 1) {
k = (k + m - 1) % n;
list.remove(k);
n--;
}
return list.get(0);
}
}
时间复杂度:O(N * N) 其中 N 为数组长度。
空间复杂度:O(1)
第二次commit AC
class Solution {
public int lastRemaining(int n, int m) {
int x = 0;
for (int i = 2; i <= n; i++) {
x = (x + m) % i;
}
return x;
}
}
时间复杂度:O(N) 其中 N 为数组长度。
空间复杂度:O(1)
4、总结
该题目的难点怎么来统计每次出圈的元素个数,还有有扎实的数学功底以及数学基础,来帮你分析题目总结规律。
❤️来自专栏《LeetCode基础算法题》欢迎订阅❤️
厂长写博客目的初衷很简单,希望大家在学习的过程中少走弯路,多学一些东西,对自己有帮助的留下你的赞赞👍或者关注➕都是对我最大的支持,你的关注和点赞给厂长每天更文的动力。
对文章其中一部分不理解,都可以评论区回复我,我们来一起讨论,共同学习,一起进步!
原题链接: 剑指 Offer 62. 圆圈中最后剩下的数字 - 力扣(LeetCode) (leetcode-cn.com)