链接

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

求解

第一种:经典的解法, 用环形链表模拟圆圈。超时

创建一个总共有 n 个结点的环形链表,然后每次在这个链表中删除第 m 个结点。

  1. public static int lastRemaining(int n, int m) {
  2. if (n < 1 || m < 1) {
  3. return -1;
  4. }
  5. List<Integer> list = new LinkedList<>();
  6. for (int i = 0; i < n; i++) {
  7. list.add(i);
  8. }
  9. // 要删除元素的位置
  10. int index = 0;
  11. while (list.size() > 1) {
  12. // 只要移动m-1次就可以移动到下一个要删除的元素上
  13. index = (index + m - 1) % list.size();
  14. list.remove(index);
  15. return list.get(0);
  16. }

第二种:分析法

首先我们定义一个关于 n 和 m 的方程町矶时,表示每次在 n 个数字 0,1, … ,n-1中每次删除第 m 个数字最后剩下的数字。
在这 n个数字中, 第一个被删除的数字是(m-1)%n。为了简单起见,我们把(m- 1)%n 记为 k,那么删除k之后剩下的 n-1 个数字为 0,1,… ,k-1,k+1,… ,n-1,并且下一次删除从数字 k+1 开始计数。相当于在剩下的序列中, k+1 排在最前面,从而形成 k+1,… ,n- 1,0,I,… ,k-1 。该序列最后剩下的数字也应该是关于 n 和 m 的函数。由于这个序列的规律和前面最初的序列不一样(最初的序列是从 0 开始的连续序列),因此该函数不同于前面的函数,记为 f’(n-1,m)。最初序列最后剩下的数字 f(n, m)一定是删除一个数字之后的序列最后剩下的数字,即 f(n, m)=f’(n-1, m)。
接下来我们把剩下的这 n-1 个数字的序列 k-1, …,n-1,0,1,… ,k-1 做一个映射,映射的结果是形成一个从 0 到 n-2 的序列:   面试题62. 圆圈中最后剩下的数字 - 图1

代码实现

  1. public static int lastRemaining2(int n, int m) {
  2. if (n < 1 || m < 1) {
  3. return -1;
  4. }
  5. int last = 0;
  6. for (int i = 2; i <=n ; i++) {
  7. last = (last + m)%i;
  8. }
  9. return last;
  10. }

完整代码

  1. import java.util.LinkedList;
  2. import java.util.List;
  3. public class Test45 {
  4. public static int lastRemaining(int n, int m) {
  5. if (n < 1 || m < 1) {
  6. return -1;
  7. }
  8. List<Integer> list = new LinkedList<>();
  9. for (int i = 0; i < n; i++) {
  10. list.add(i);
  11. }
  12. // 要删除元素的位置
  13. int idx = 0;
  14. // 开始计数的位置
  15. int start = 0;
  16. while (list.size() > 1) {
  17. // 只要移动m-1次就可以移动到下一个要删除的元素上
  18. for (int i = 1; i < m; i++) {
  19. idx = (idx + 1) % list.size(); // 【A】
  20. }
  21. list.remove(idx);
  22. // 确保idx指向每一轮的第一个位置
  23. // 下面的可以不用,【A】已经可以保证其正确性了,可以分析n=6,m=6的第一次删除情况
  24. // if (idx == list.size()) {
  25. // idx = 0;
  26. // }
  27. }
  28. return list.get(0);
  29. }
  30. public static int lastRemaining2(int n, int m) {
  31. if (n < 1 || m < 1) {
  32. return -1;
  33. }
  34. int last = 0;
  35. for (int i = 2; i <=n ; i++) {
  36. last = (last + m)%i;
  37. }
  38. return last;
  39. }
  40. public static void main(String[] args) {
  41. test01();
  42. System.out.println();
  43. test02();
  44. }
  45. private static void test01() {
  46. System.out.println(lastRemaining(5, 3)); // 最后余下3
  47. System.out.println(lastRemaining(5, 2)); // 最后余下2
  48. System.out.println(lastRemaining(6, 7)); // 最后余下4
  49. System.out.println(lastRemaining(6, 6)); // 最后余下3
  50. System.out.println(lastRemaining(0, 0)); // 最后余下-1
  51. }
  52. private static void test02() {
  53. System.out.println(lastRemaining2(5, 3)); // 最后余下3
  54. System.out.println(lastRemaining2(5, 2)); // 最后余下2
  55. System.out.println(lastRemaining2(6, 7)); // 最后余下4
  56. System.out.println(lastRemaining2(6, 6)); // 最后余下3
  57. System.out.println(lastRemaining2(0, 0)); // 最后余下-1
  58. }
  59. }