剑指offer(第二版)——圆圈中最后剩下的数字
作者:互联网
PS:《剑指offer》是很多同学找工作都会参考的一本面试指南,同时也是一本算法指南(为什么它这么受欢迎,主要应该是其提供了一个循序渐进的优化解法,这点我觉得十分友好)。现在很多互联网的算法面试题基本上可以在这里找到影子,为了以后方便参考与回顾,现将书中例题用Java实现(第二版),欢迎各位同学一起交流进步。
GitHub: https://github.com/Uplpw/SwordOffer。
完整题目链接: https://blog.csdn.net/qq_41866626/article/details/120415258
目录
1 题目描述
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
leetcode链接: 圆圈中最后剩下的数字(以下代码已测试,提交通过)
2 测试用例
一般是考虑功能用例,特殊(边缘)用例或者是反例,无效测试用例这三种情况。甚至可以从测试用例寻找一些规律解决问题,同时也可以让我们的程序更加完整鲁棒。
(1)功能用例:n是正整数。
(2)边缘用例:n=1。
(3)无效用例:n小于等于0。
3 思路
分析:
实际上,本题是著名的 “约瑟夫环” 问题。下面介绍两种解决方式。
解法1:采用模拟法
题目已经很详细地给出问题的求解流程,我们可以采用模拟法进行解决。
可以使用list模拟环,不断删除元素、更新环的开始位置,剩下最后的一个元素即所求解。该解法时间复杂度比较高,但leetcode还是可以通过的。
时间空间复杂度: O ( n 2 ) , O ( n ) O(n^2),O(n) O(n2),O(n)
解法2:数学解法
我们可以通过举例子、分析其规律找出对应的解法,也有同学把这道题当作动态规划做的,不过前提也是得分析出题目删除元素以及新的环头规律,所以本质是不变的(最后代码甚至都一模一样)。
如:n=5,m=3时,数组 [0, 1, 2, 3, 4],下面是求解过程中的具体步骤与相应变化内容。
轮次 | 初始环顺序 | 删除元素 | 更新后的环顺序 |
---|---|---|---|
1 | [0, 1, 2, 3, 4] | 2 | [3, 4, 0, 1] |
2 | [3, 4, 0, 1] | 0 | [1, 3, 4] |
3 | [1, 3, 4] | 4 | [1, 3] |
4 | [1, 3] | 1 | [3] |
最后剩下的数字是 3。
我们注意表中每次删除元素后环头元素的位置与删除前环头元素的位置,同时特别是这种关系与 m 值有密切的关系。
不难发现,新的一轮的开头是固定地向前移位 m 个位置。
我们从最后剩下的 3 倒着看,我们可以反向推出这个数字 3 在之前每个轮次的位置
第四轮反推,补上 m 个位置,然后模上当时的数组大小 2,位置是(0 + 3) % 2 = 1。
第三轮反推,补上 m 个位置,然后模上当时的数组大小 3,位置是(1 + 3) % 3 = 1。
第二轮反推,补上 m 个位置,然后模上当时的数组大小 4,位置是(1 + 3) % 4 = 0。
第一轮反推,补上 m 个位置,然后模上当时的数组大小 5,位置是(0 + 3) % 5 = 3。
即数字位置变化为,当前位置加上 m 对当前数组元素个数取余即是下一轮该元素的位置。
另外我们可以通过数学推导计算得到。
对于「n, m 问题」,首轮删除环中第 m 个数字后,得到一个长度为 n - 1 的数字环。由于有可能 m > n ,因此删除的数字为 (m - 1) % n,删除后的数字环从下个数字(即 m % n)开始,设 t = m % n ,可得数字环:
t , t + 1 , t + 2 , . . . , 0 , 1 , . . . , t − 3 , t − 2 t, t + 1, t + 2, ..., 0, 1, ..., t - 3, t - 2 t,t+1,t+2,...,0,1,...,t−3,t−2
删除一轮后的数字环也变为一个「n-1, m 问题」,观察以下数字编号对应关系:
「n−1,m问题」 | 「n,m问题」 |
---|---|
0 | t + 0 |
1 | t + 1 |
2 | t + 2 |
… | … |
n - 3 | t - 2 |
n - 2 | t - 1 |
设「n-1, m 问题」中某个数字为 x ,则可得递推关系: x → ( x + t ) % n x→(x+t)\%n x→(x+t)%n
换而言之,若已知「n-1, m 问题」的解 f(n - 1) ,则可通过以上公式计算得到「n, m 问题」的解 f(n) ,即:
f ( n ) = ( f ( n − 1 ) + t ) % n = ( f ( n − 1 ) + m % n ) % n = ( f ( n − 1 ) + m ) % n f(n) =(f(n−1)+t)\%n =(f(n−1)+m\%n)\%n =(f(n−1)+m)\%n f(n)=(f(n−1)+t)%n=(f(n−1)+m%n)%n=(f(n−1)+m)%n
时间空间复杂度: O ( n ) , O ( 1 ) O(n),O(1) O(n),O(1)
4 代码
算法实现:
package com.java.offer.chapter6.sixtytwo;
import java.util.ArrayList;
import java.util.List;
public class LastNumberInCircle {
// 采用模拟法 O(n^2)
public static int lastRemaining(int n, int m) {
if (n <= 0 || m <= 0) {
return -1;
}
// 添加数字到列表(使用链表模拟环)
List<Integer> list = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
list.add(i);
}
// 第一次环开始的索引位置
int start = 0;
while (n > 1) {
// 找到要删除的元素位置 (m-1)%n 因为m可能比n大,所以取余操作
start = (start + m - 1) % n;
// 移除元素
list.remove(start);
// 元素数量减1
n--;
}
return list.get(0);
}
// 数学解法/动态规划
public static int lastRemaining1(int n, int m) {
if (n <= 0 || m <= 0) {
return -1;
}
// 当前环头位置
int x = 0;
for (int i = 2; i <= n; i++) {
// 环头位置加上 m 对当前元素个数取余(因为最后剩下一个元素在环头位置)
x = (x + m) % i;
}
return x;
}
}
参考
在解决本书例题时,参考了一些大佬的题解,比如leetcode上的官方、K神,以及其他的博客,在之后的每个例题详解后都会给出参考的思路或者代码链接,同学们都可以点进去看看!
本例题参考:
本文如有什么不足或不对的地方,欢迎大家批评指正,最后希望能和大家一起交流进步、拿到心仪的 offer !!!
标签:元素,数字,删除,offer,int,位置,圆圈,解法 来源: https://blog.csdn.net/qq_41866626/article/details/120579706