剑指 Offer 之 圆圈中最后剩下的数字(约瑟夫环问题)
Contents
《剑指 Offer》中涉及到了约瑟夫环问题,虽然下面文章中所描述的和书中的题目有些差异,但从总体上看,都涉及到了约瑟夫环问题。
题目描述
0,1,···,n-1 这 n 个数字排成一个圆圈,从数字 0 开始,每次从这个圆圈里删除第 m 个数字。求出这个圆圈里剩下的最后一个数字。
而本文所涉及到的题目如下所示: 有很多人围成一个圈,由第 1 个人开始报数,报数到 3 的人就出队,然后再由下一个人重新从 1 开始报数,报数到 3 的那个人再出队,就这样直到剩下最后一个人。
你可以看到,这两个题目所描述的有些不同。第二题与第一题的不同之处在于:每次需要由下一个人重新从 1 开始报数,则最后剩下的那个人必然就是 1。
输入:一个环形单向链表的头节点 head 和报数的值 m。
输出:最后生存下来的节点,且这个节点自己组成环形单链表,其他节点都删除掉。
时间复杂度:O(N)
方法一
分析
首先根据题目的描述,走一遍这个流程。假如报数的值 m = 3,也就是需要删除的编号,如下图左侧所示。删除 3 后,则将原来的 4 置为 1, 5 置为 2, 1 置为 3, 2 置为 4,如下图右侧所示:
然后同样的再删除第三个数 3,则原来的 4 变成了 1,原来的 1 置为 2,原来的 2 置为 3,如下图所示:
然后再从 1 开始,删除第 3 个数,如下所示:
然后再从 1 开始数三个数,结果又回到了 1,即将这个 1 删除后,再将 2 变为 1,这就得到了最后的结果。如下图所示:
好,知道以上计算流程之后,我们再看看如何具体的分析这个问题。
我们 从后往前计算,每报一次数,然后链表中的下一个节点就重新编号,那么最后所剩节点的个数只有一个。并且可以确定的是,最后该节点一定是 1(如上图所示)。
那么现在需要找到一个公式,能够从底往上推出初始的状态,如下所示:
| 剩余节点数 | 存活节点的编号 |
|---|---|
| N | ? |
| N-1 | ? |
| ··· | ··· |
| 3 | ? |
| 2 | ? |
| 1 | 1 |
从 1 开始计算,一直计算到剩余节点数 N,此时对应的 存活节点的编号 就是在原链表中一个也没有报数的情况下所对应的节点,我们最后返回该节点即可。也就是说,我们一开始是不知道哪个节点最后是剩余的,而只知道最后那个剩余节点的编号一定是 1。
现在假设链表长度为 i,则每个节点的 编号 和 报数 之间的关系,如下所示:
| 编号 | 报数 |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| ··· | ··· |
| i | i |
| i+1 | 1 |
| i+2 | 2 |
| ··· | ··· |
| 2i | i |
| 2i+1 | 1 |
| 2i+2 | 2 |
| ··· | ··· |
| 3i | i |
| ··· | ··· |
我们可以将其绘制出来,如下所示:
然而,上面的图像是如何得到的呢?
这里可以对下面的图像进行平移,向右移动一位,再向上移动一位,从而得到上面的图像。
因此,可以得到 编号 和 报数 之间的关系:编号 = (报数 - 1) % i + 1。
如何平移?
左加右减,上加下减。
好,找到了 编号 和 报数 之间对应的关系之后,我们再看 新编号 和 老编号 之间的关系。当链表长度为 i 的时候,它们之间的关系如下所示:
S 表示被删除的节点。
新号是由于删除了节点 S,才变成长度为 i - 1 的。
| 老编号(长度为 i) | 新编号(长度为 i-1) |
|---|---|
| S-1 | i-1 |
| S | — |
| S+1 | 1 |
| S+2 | 2 |
由于 S 节点被删除了,所以它在新号中就没有编号了。原来在老编号中的 S+1,在新编号中就变成了 1。原来在老编号中的 S-1,在新编号中就变成了 i-1(因为新编号的长度就是 i-1)。
下面进行详细说明,如下图所示:
首先删除 3,然后从 4 的位置开始重新编号为 1。这里找一下对应的关系。新编号为 1 的时候,老编号为 4,也就是 S+1=3+1=4;新编号为 2 的时候,老编号为 5,也就是 S+2=3+2=5;…;新编号为 6 的时候,也就是 i-S=9-3=6,老编号为 9;新编号为 7 的时候,也就是 i-S+1=9-3+1=7,老编号回到了 1。
这里用如下坐标形式将上面的内容进行表示:
也就是说,新编号的 1 对应旧编号的 S+1,新编号的 i-S 对应旧编号的 i,新编号的 i-S+1 对应旧编号的 1,新编号的 i-1 对应旧编号的 S-1。此时 S 位置上的数已经被删除了。
现在将上图中的线延长,如下所示:
由于之前已经求出了函数:编号 = (报数 - 1) % i + 1,将该函数向左平移 S 个单位,就可以得到上图。过程如下:
从而可以得到对应关系:旧编号 = (新编号 - 1 + S) % i + 1。
由之前的对应关系可知:数到第 m 个就报数,当前报数的编号就是 S,则有:S = (m - 1) % i + 1。由于 m 已知,则将 S 带入到上式中,就可以求出 新编号 和 老编号 对应的关系。如下所示:
$$ 旧编号 = (新编号 - 1 + S) % i + 1 $$ $$ 旧编号 = [新编号 - 1 + (m - 1) % i + 1] % i + 1 $$ $$ 旧编号 = [新编号 + (m - 1) % i] % i + 1 $$ $$ 旧编号 = (新编号 + m - 1)% i + 1 $$
这里说一下最后两步为什么相等?
用 $ (a+k % i) %i $ 表示倒数第二个式子,用 $ (a+k) %i $ 表示倒数第一个式子。
假设 $ k=c \cdot i + t $,其中 $ t $ 表示余数,则有 $ (a+k % i) %i = (a+t) % i $。而 $ (a+k) %i = (a+t) % i $,所以 $ (a+k % i) %i = (a+k) %i $。
代码
|
|
对代码的详细解释:
方法二
这是在力扣的解答区看到的方法,讲得比较清晰,因此在这里做一个梳理。
该方法的核心在于:只关心最终活着的那个人的序号变化情况,从最终结果反推出规律。
例如一共有 n=8 个人,每当数到 m=3 的时候就将该人给杀掉,我们用 F(n, m) 来表示最后剩下的那个人的序号。整个流程如下图所示:
图片均来自【参考】文章。

从上图可以看到,我们将这 8 个人用字母代替,在每一轮中,红色的方格表示当前需要被杀掉的人,绿色的方格表示最终存活下来的人。N=8 时,将 C 杀掉,则下一轮(N=7)将会从 D 开始报数,然后往右数 m=3 个数,再将 F 杀掉,以此类推。
此外,注意观察每一轮中最后活下来的 G 的索引变化情况。当最后只剩下一个人的时候,这个人的编号(索引号)一定是 0。
接下来我们看看如何进行反推,例如如何从 N=7 反推出 N=8 时的情况,如下图所示:

在从 N=7 回到 N=8 的过程中,先将被杀掉的 C 补充回去,即黄色方格;然后将 N=7 右移 m 个单位,由于多出 3 个人(A、B、C)越界了,因此将它们移到前面;最后得到的结果就和 N=8 一样了。
因此,从 N=7 到 N=8 的递推公式为:$f(8, 3)=[f(7, 3) + 3] % 8$,而将此公式推广到一般情况,则有:$f(N, m)=[f(N-1, 3) + 3] % N$。
最后整理得到的递推公式:
$$ 当 n=1 时,f(N, m) = 0 $$
$$ 当 n > 1 时,f(N, m) = [f(N-1, m) + m] % N $$
代码
|
|