面试题62. 圆圈中最后剩下的数字【困难】
题目描述
0,1,,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
解题思路&代码
方法一:模拟法
完全模拟题目操作,但是由于 Python 中删除节点的时间复杂度 $O(N)$ ,共要删除 $N-1$ 次,故时间复杂度为 $O(N^2)$,这会导致超时。
根据 Java解决约瑟夫环问题,告诉你为什么模拟会超时!  这一篇文章,可以发现,使用模拟法会导致超时。但是采用 Java 中的 ArrayList 类型能 AC。
不过从时间复杂度上分析:
| 查找复杂度 | 删除复杂度 | 总复杂度 | |
|---|---|---|---|
| ArrayList | O(1) | O(n) | O(n) | 
| LinkedList | O(n) | O(1) | O(n) | 
无论采用 ArrayList 或是 LinkedList 的时间复杂度都是一样的。只是作者在研究源码发现,Java 的 remove 是对连续内存空间的拷贝,可以通过AC。
不过这类方法我们还是不准备采用,下面给出 连续存储 和 链式存储 的代码
代码
代码一:Python 连续存储【List 类型】
# 2080 ms 勉强 AC
class Solution:
    def lastRemaining(self, n: int, m: int) -> int:
        s = list(range(n))
        i, j = 0, n
        for _ in range(n - 1):
            i = (i + m - 1) % j
            s.pop(i) # 删除节点操作为 O(N)
            j -= 1
        return s[0]
代码二:双端队列【deque】
# 超时
class Solution:
    def lastRemaining(self, n: int, m: int) -> int:
        if n < 1:
            return -1
        queue = collections.deque()
        for i in range(n):
            queue.append(i)
        while len(queue) > 1:
            for _ in range(m - 1):  # 走第 m - 1步
                queue.append(queue.popleft())
            queue.popleft() # 击鼓传花,传到 第 m 个人的头上
        return queue[0]
代码三:Python 链式存储
# 超时
class Node:
    def __init__(self, value):
        self.value = value
        self.next = None 
def create_nodes(n):
    head = Node(0)
    pre = head
    for i in range(1, n):
        new_node = Node(i)
        pre.next = new_node
        pre = pre.next 
    pre.next = head # 连接头,形成闭环
    return head 
class Solution:
    def lastRemaining(self, n: int, m: int) -> int:
        if n == 1: return 0
        head = create_nodes(n)
        pre = None 
        cur = head
        while cur != cur.next: # 若 cur == cur.next 那么代表只有一个节点
            for i in range(m - 1): # 删除第 m 个,先走 m - 1 步
                pre = cur
                cur = cur.next
            print(cur.value) # 输出删除的节点
            pre.next = cur.next # 删除 cur 这一节点
            cur = pre.next
        return cur.value
方法二:分析数字规律
详细解答,请移步 换个角度举例解决约瑟夫环

可以知道,最后的答案一定是位于 数据的0位置,所以可以总结出 逆推公式: $$ f(n,m) = \begin{cases} 0 & n = 1\ (f(n-1, m)+m)%n & n>1\ \end{cases} $$
代码
class Solution:
    def lastRemaining(self, n: int, m: int) -> int:
        pos = 0
        for i in range(2, n + 1):
            pos = (pos + m) % i 
        return pos