直接看对应的题目:
圆圈中最后剩下的数字
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof
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
示例 2:
输入: n = 10, m = 17
输出: 2
限制:
- 1 <= n <= 10^5
- 1 <= m <= 10^6
思路:
最简单的思路就是直接用链表模拟这个过程。对于C++而言可以用标准库中的list,然而时间复杂度太高了,应该是O(mn)。下面的代码我在力扣里直接判断超时了。
不过这里还是写一下,也算是复习list的用法,并且一个有知识点是 list 用 erase 删除节点,会返回后面一个节点的迭代器。
超时的C++代码:
class Solution {
public:
int lastRemaining(int n, int m) {
list<int> ring;
for (int i = 0; i < n; ++i) {
ring.push_back(i);
}
auto it = ring.begin();
while (ring.size() > 1) {
int num = 0;
while (num < m - 1) {
if (it == ring.end()) {
it = ring.begin();
}
++it;
++num;
}
if (it == ring.end()) {
it = ring.begin();
}
it = ring.erase(it);//list迭代器删除会自动返回下一个迭代器
}
return *(ring.begin());
}
};
想要写出时间复杂度低的不超时的代码,还得用数学推导的方式推导出一个递推公式。
公式的证明太硬,本人几次看懂几次忘记,想来也是没有真正懂,力扣题解里有很好的证明,同时也有用图像进行演示的,因此这里也不 详述证明过程,只谈谈个人对公式的具体含义的理解:
公式的大致意思是长度为 n 的序列经过约瑟夫不停地往后删除第 m 个元素的操作后最终留下的数值的索引 f(n,m) 可以通过长度为 n-1 的序列中最终留下的数字的索引 f(n-1,m) 计算得到。这样我们只要不停得递归,算到 f(1,m),由于长度为 1 的序列最终留下的数字的索引显然是 0,因此就能计算出最终结果。
虽然不证明,但还是最好用一个实例加深理解(其实根本没理解,用加深记忆更为妥当)。这里思考示例1,示例1中最终留下是数字索引 f(n,m) = f(5,3) = 3,然后我们在纸上演算一下 f(4,3) ,发现删除到最后留下的数字索引为 0 ,[m+f(n−1,m)]%n = (3+0)%5 = 3,这显然是符合公式的。
上面提到递归,但其实用迭代方法直接倒着算就可以了。通过该方法编写的代码时间复杂度是O(n),比起上述模拟整个过程的方法大大降低。
不超时的C++代码:
class Solution {
public:
int lastRemaining(int n, int m) {
int res = 0;
for(int i = 2; i <= n; ++i){
res = (m + res) % i;
}
return res;
}
};