过来吧 > 社会 > 正文

​算法:约瑟夫环

时间:2026-01-10 22:24

来源:过来吧

点击:

算法:约瑟夫环

一、前言

夫拉维·约瑟夫是公元一世纪历史学家,传说中由于他的数学天赋,得以在犹太罗马战争期间活下来。关于他的故事,有这样一个版本:

在犹太罗马战争期间,他们41名犹太反抗者困在罗马人包围的洞穴中。这些反抗者宁愿自杀也不愿被活捉,于是决定围成一个圈圈。沿着这个圈圈每两个人杀死一个,直到剩下最后两个人为止。但是约瑟夫和一个未被告发的同谋者不希望无谓地自杀,于是他迅速计算出他和朋友在圈圈中应该站的位置。

《具体数学》第二版 1.3

我们对这个问题做一层抽象和简化:假设有 n 个人围成一圈,编号是 1 到 n。从 1 开始计数,每隔一个,抹去一个,直到剩下一个人,请问这个人的编号是多少?

本文将围绕这个问题,逐层深入,探讨问题的本质。这里会涉及到链表、递归、bit数组等概念,重点在推理过程。在继续阅读之前,建议读者拿出纸和笔,思考5-10分钟,脑海中有一个初步的想法再继续读下去。

二、整理思路

首先,我们尝试理解一下该算法的过程。假设 n = 10,根据对算法描述的直接理解,会走下面这个过程:

(下划线表示在上一轮就已经被删除)

将该过程可视化呈现以后,最直观的解决方案就有了,即使用 bitmap 存储每个数字的状态(默认为0),每次遍历更新一组数字的状态,直到仅有一个数字状态为 0。

这种方法每次都要遍历整个数组,每次遍历以后,状态为0的元素个数减半,所以需要遍历 logn 次,算法的整体时间复杂度是 O(n*logn),空间复杂度是 O(n)。下面是基于 Go 的实现:

// JosephusBitMap 基于 bitmap 实现 // 为了简化代码,使用 []bool 替代 bitmap func JosephusBitMap(n int) int {  bitmap := make([]bool, n, n)  toDel := false  for left := n; left > 1; { for idx := 0; idx < n; idx++ {  if bitmap[idx] { continue  }  if toDel { bitmap[idx] = true toDel = false left--  } else { toDel = true  } }  }  // 读取结果  for i := 0; i < n; i++ { if !bitmap[i] {  return i + 1 }  }  return -1 }

上面这段代码中,我们使用 []bool 存储 n 个节点的状态信息,toDel 用于记录该节点是否需要被删除。由于该方法每次都需要扫描所有 n 个节点,效率并不理想。

怎么样才能不扫描所有 n 个节点呢?显然,不扫描所有 n 个节点,意味着只需要扫描未被删除的节点,有两种解决方案:

将未删除的节点拷贝出来使用环形链表

方案1的核心逻辑不是删节点,而是将未删除的节点拷贝到另一个长度 n/2 的数组。这样减少了遍历次数,但是会带来 O(logn) 次内存分配和销毁。实现高并发服务时,过多的gc并不是一件好事,所以相对于bitmap方案孰优孰劣还不好说。

方案2中的环形链表在视觉上能准确地描述约瑟夫环。实现时,需要先根据 n 初始化一个环形链表,从值1的节点开始遍历,每隔一个删除一个,直到当前节点的下一个节点是它自己。该方案也需要分配n次内存,并进行n次销毁。与方案1不同的是,初始化和销毁的只是一个节点,而不是一个数组。为了对bitmap和linkedlist 两种方案的性能有直接的对比,我们也实现一下,代码如下:

// JosephusLinklist 是基于环形链表的实现 func JosephusLinklist(n int) int {  // 初始化环形链表  head := &Node{val: 1}  cur := head  for i := 2; i <= n; i++ { cur.next = &Node{val: i} cur = cur.next  }  cur.next = head  // 删除节点  for cur := head; ; { next := cur.next cur.next = next.next cur = cur.next // 终止条件:只有一个节点 if cur.next == cur {  return cur.val }  }  // 返回结果  return -1 }

Benchmark 结果还是有些出乎预料。n=10 时,bitmap 方案的性能是 LinkedList 方案的5倍;n=10000时,bitmap 方案的性能是 LinkedList 方案的3倍。Benchmark 结果说明了很多事情,大家自行脑补。

2020-04$ go test --bench=. --benchmem goos: darwin goarch: amd64 pkg: github.com/oscarzhao/oscarzhao.github.io/assets/2020-04 BenchmarkJosephusBitMap10-4 19414291  58.8 ns/op  16 B/op  1 allocs/op BenchmarkJosephusBitMap10000-4 12986 94531 ns/op10240 B/op  1 allocs/op BenchmarkJosephusLinklist10-43978577295 ns/op 160 B/op 10 allocs/op BenchmarkJosephusLinklist10000-43256319925 ns/op  160000 B/op  10000 allocs/op PASS ok  github.com/oscarzhao/oscarzhao.github.io/assets/2020-04 5.854s

事情到这里还没有结束,我们仍然在寻找更高效的解决方案。

不忘初心,方得始终。

我们回归到问题本身,不知道还有多少人记得这张图:

朋友,你是否已发现,经过第一轮过滤以后,剩下 1/3/5/7/9,当前节点还是1。对比 n=5 的情形,会发现 n=10 问题的解是:

// f 是求解函数 f(10) = 2*f(5)-1

朋友,你是否很开心,但又有很多问号?

问号1: 这条规则是否对于所有偶数都适用?问号2: 奇数的话,怎么处理?

针对问号1,答案是 yes!具体原因不做介绍。针对问号2,我们不妨以 n=11 为例,第一轮执行结束后,结果如下:

剩下的数字是 3、5、7、9、11,当前位置是3。对比 n=5 的情形,会发现 n=11 的解是:

// f 是求解函数 f(11) = 2*f(5)+1

同样,对于所有奇数,该递归式也是成立的。整合一下递归式,可以得到:

f(1) = 1 f(2*n)= 2*f(n) - 1 f(2*n+1) = 2*f(n) + 1

该方法的时间复杂度是 O(logn), 空间复杂度为1,且没有额外的内存分配。转换成代码如下:

func JosephusRecursion(n int) int {  if n == 1 { return 1  }  if n%2 == 1 { return 2*JosephusRecursion(n/2) - 1  } else { return 2*JosephusRecursion(n/2) + 1  } }

Benchmark 的结果也非常漂亮,在 n=10000 时,执行效率远超前两种实现:

对于一个常规的计算机程序来说,时间复杂度 O(logn) 已经非常不错了。但是我们可以把问题再向前推进一步:是否存在一个 O(1) 的解法?如果这种方法真的存在,那么必须不是递归,而是通过 n 直接映射到最终结果。

考虑到是映射,我们可以借助前面的实现,打印一张映射表,给我们提供一些思路。我们使用 JosephusRecursion 打印出 n=1-20对应的结果:

shuaihu@local:2020-04$ go test --run=TestJosephusTable 1,  1 2,  1 3,  3 4,  1 5,  3 6,  5 7,  7 8,  1 9,  3 10,  5 11,  7 12,  9 13, 11 14, 13 15, 15 16,  1 17,  3 18,  5 19,  7 20,  9 PASS ok  github.com/oscarzhao/oscarzhao.github.io/assets/2020-04 0.010s

我们稍微观察一下,做一下处理,显示成这样:

1 |  1 -------- 2 |  1 3 |  3 -------- 4 |  1 5 |  3 6 |  5 7 |  7 -------- 8 |  1 9 |  3 10 |  5 11 |  7 12 |  9 13 | 11 14 | 13 15 | 15 -------- 16 |  1 17 |  3 18 |  5 19 |  7 20 |  9

然后可以得到下面一组结论:

f(2^k) = 1f(2^k + 1) = 3f(2^k + 2) = 5f(2^(k+1) -1) = 2^(k+1) - 1

结论4 等价于

f(2^k + (2^k-1)) = 2^k + (2^k -1)

处理成通用的公式就是:

f(2^k + i) = 2*i + 1, for i in [0, 2^k-1]

此时,n = 2^k + i。那么问题来了,这个公式可以推广到所有整数 n 吗?我们用数学归纳法尝试推导一下。

数学归纳法的证明分为两步:

证明当n=0时命题成立。证明如果在n=k时命题成立,那么可以推导出在n=k+1时命题也成立。(m代表任意自然数)

在我们这个case里,我们已经有四个命题:

// 基础命题 f(1) = 1 // 对所有正整数 n 都成立的命题 f(2*n)= 2*f(n) - 1 f(2*n+1) = 2*f(n) + 1 // n <= 20 才成立的命题 f(2^k + i) = 2*i + 1, 当 n = 2^k + i

我们要验证:是否对于所有 n = 2^k+i, f(2^k + i) = 2*i+1 都成立。为了能使用前面三个命题进行推导,我们把 i 分为奇数和偶数两类,分别证明:

假设n是偶数,即 n =2^(k+1)+2m,当i=2m,  => f(n) = f(2^(k+1) + 2m)  => f(n) = f(2( 2^k + m))  => f(n) = 2f(2^k + m) - 1  => f(n) = 2(2m+1) - 1  => f(n) = 2(i+1) - 1  => f(n) = 2*i + 1

同样的方法,可以证明 i 是奇数时,推导也成立。

综上所述,对于所有 n = 2^k + i, f(n) = 2*i + 1。

截止目前,我们已经拿到了 n -> f(n) 的映射关系,如何转换成代码呢?我们知道 i 和 n 的关系,但需要一个可编程的方法根据 n 求出 i。回头看看映射表,显然二进制是一个方向。这里通过一个 测试用例将结果打印出来:

func TestJosephusTable(t *testing.T) {  fmt.Printf(" n,f(n), i| %5s, %5s, %5s\n", "n_bit", "fn_bi", "i_bit")  for i := 1; i <= 20; i++ { res := JosephusRecursion(i) fmt.Printf("%2d, %2d, %2d| %05b, %05b, %05b\n", i, res, res/2, i, res, res/2)  } }

执行结果是:

2020-04$ go test --run=TestJosephusTable n,f(n), i| n_bit, fn_bi, i_bit 1,  1,  0| 00001, 00001, 00000 2,  1,  0| 00010, 00001, 00000 3,  3,  1| 00011, 00011, 00001 4,  1,  0| 00100, 00001, 00000 5,  3,  1| 00101, 00011, 00001 6,  5,  2| 00110, 00101, 00010 7,  7,  3| 00111, 00111, 00011 8,  1,  0| 01000, 00001, 00000 9,  3,  1| 01001, 00011, 00001 10,  5,  2| 01010, 00101, 00010 11,  7,  3| 01011, 00111, 00011 12,  9,  4| 01100, 01001, 00100 13, 11,  5| 01101, 01011, 00101 14, 13,  6| 01110, 01101, 00110 15, 15,  7| 01111, 01111, 00111 16,  1,  0| 10000, 00001, 00000 17,  3,  1| 10001, 00011, 00001 18,  5,  2| 10010, 00101, 00010 19,  7,  3| 10011, 00111, 00011 20,  9,  4| 10100, 01001, 00100 PASS ok  github.com/oscarzhao/oscarzhao.github.io/assets/2020-04 0.010s

对比 n_bit, fn_bi, i_bit 后,我们很难发现 n_bit 和 fn_bit 的关系,但是 n_bit 和 i_bit 的关系比较明显:把 n_bit 最高位的 1 去掉,就是 i_bit。由于 f(n) = 2 * i + 1,所以

fn_bi = (i_bit << 1) + 1

事实上,不打印二进制结果,只观察 n 和 i 的关系也能看出点端倪。这个切入点更符合我的思考过程,然后才去考虑代码实现,引入二进制表示。

截止目前,在二进制上我们已经有了可行的解决方案,这里我们借助于 golang bits 库进行操作:

func JosephusBit(n int) int {  uintN := uint(n)  leftMove := bits.UintSize - bits.LeadingZeros(uintN) - 1  mask := (uint(1) << leftMove) - 1  i := mask & uintN  return int(i*2 + 1) }

计算机硬件层面原生支持二进制运算,理论上性能会不错。我们跑一组benchmark看看:

goos: darwin goarch: amd64 pkg: github.com/oscarzhao/oscarzhao.github.io/assets/2020-04 BenchmarkJosephusBitMap10-41787387460.3 ns/op BenchmarkJosephusBitMap10000-41400175533 ns/op BenchmarkJosephusLinklist10-4  4624140 253 ns/op BenchmarkJosephusLinklist10000-4  3920  273205 ns/op BenchmarkJosephusRecursion10-41663381406.90 ns/op BenchmarkJosephusRecursion10000-4 3506577033.8 ns/op BenchmarkJosephusBit10-410000000001.02 ns/op BenchmarkJosephusBit10000-4 10000000001.01 ns/op PASS ok  github.com/oscarzhao/oscarzhao.github.io/assets/2020-04 11.982s

1.02 ns/op,性能不会随着 n 变大而衰减,且没有额外的内存分配,堪称完美。

三、延伸一下

上面的问题是约瑟夫环的一个特例,我们稍微修改一下问题:从每两个人杀掉一个,改成每 k 个人杀掉一个。这个问题还有通用的解法吗?

不妨将这个问题抽象成一个函数 f(n, d int), d 的默认值是 2。问题的答案是,存在通用的解法。由于文章篇幅限制,这里只给一个简单的指引。上面提到了我们的递归式:

f(1) = 1 f(2*n)= 2*f(n) - 1 f(2*n+1) = 2*f(n) + 1

这是 d = 2 时的特例,将这个递归式推广以后,可以得到另一个式子:

f(1) = a; f(2*n+j)=2*f(n)+b_j,当j=0,1,n>=1时

这里 a = 1, b_0 = -1, b_1 = 1。将 n 转化为二进制形式,对这个公式进行推导,并延伸得到另一组公式:

f(j) = a_j,当 1 <= j < d 时 f(d*n+j)=c*f(n)+b_j,当0<=j

=1时

对这块有兴趣的同学可以翻到《具体数学》第一章第三节,了解详细的推导过程。

四、References

具体数学:

https://book.douban.com/subject/21323941//

Golang bits: https://golang.org/pkg/math/bits/

对这类文章感兴趣的童鞋可以关注微信公众号“深入Go语言”。

相关标签:

相关推荐

​假冒"茶色素"产品大量出现,正规产品企业联合司法发表严正声明

​假冒"茶色素"产品大量出现,正规产品企业联合司法发表严正声明

假冒茶色素产品大量出现,正规产品企业联合司法发表严正声明 中国大健康行业的发展有赖于国家的法治和企业的自律,要使健康产业更好地服务于人类,发挥更大的价值作用,就必须...

2026-01-10 22:21:54

​那个曾经得到赵本山赏识的曹佳睿,如今怎样了

​那个曾经得到赵本山赏识的曹佳睿,如今怎样了

那个曾经得到赵本山赏识的曹佳睿,如今怎样了 在阅读前,麻烦您点击一下“关注”,既方便您进行讨论和分享,又能给您带来不一样的参与感,十分感谢您的支持。 文|庭芥 编辑|庭...

2026-01-10 22:19:39

​曹魏集团二把手,被曹操赞为“吾之子房”的荀彧为何被赐死

​曹魏集团二把手,被曹操赞为“吾之子房”的荀彧为何被赐死

曹魏集团二把手,被曹操赞为“吾之子房”的荀彧为何被赐死 序言 在群英荟萃的三国史上,人们熟悉蜀汉的诸葛亮,了解东吴的周瑜,却鲜有人知道曹操集团的荀彧。荀彧虽然低调,...

2026-01-10 22:17:25

​700亿赌王"正宫"黎婉华,为何33岁就遭重病?这场豪门恋"细思极恐"

​700亿赌王"正宫"黎婉华,为何33岁就遭重病?这场豪门恋"细思极恐"

700亿赌王正宫黎婉华,为何33岁就遭重病?这场豪门恋细思极恐 赌王传奇一生,不只是事业上,更传奇的是,他有 4房太太,17个孩子。 而 正宫 黎婉华, 毫无争议 ,是赌王的四房太太里...

2026-01-10 22:15:11

​完整版!2019年31省份高考分数线公布

​完整版!2019年31省份高考分数线公布

完整版!2019年31省份高考分数线公布 逾一千万! 2019年,全国高考报名人数又创历史新高,其中,高考报名人数排在前四位的省份分别为河南(逾100万)、广东(76万)、四川(65万)和...

2026-01-10 22:12:57

​都安县的壮族习俗

​都安县的壮族习俗

都安县的壮族习俗 都安县的壮族习俗 作者:都安县志编委会 编者按:本文摘自1993年版《都安县志》。收入「思恩府驿站」有编辑改动。如有侵权请联系删除。 壮族 县境壮人自称“布...

2026-01-10 22:10:42

​每天了解一个繁华都市—休斯顿

​每天了解一个繁华都市—休斯顿

每天了解一个繁华都市—休斯顿 导读:休斯顿,一座拥有近两百年历史的繁华都市,曾经的“石油大户”、“世界能源之都”。它究竟是一座怎样的城市呢?今天这期文章将带大家一起...

2026-01-10 22:08:28

​每天了解一个繁华都市—苏黎世

​每天了解一个繁华都市—苏黎世

每天了解一个繁华都市—苏黎世 导读:苏黎世,是瑞士第一大城市、苏黎世州的首府和瑞士最重要的工商业城市,以及全国政治、经济、文化和交通中心。同时,它也是全欧洲最富有的...

2026-01-10 22:06:14

​詹妮弗·劳伦斯:从无辜少女到领袖和战士,闪耀光芒的女性形象

​詹妮弗·劳伦斯:从无辜少女到领袖和战士,闪耀光芒的女性形象

詹妮弗·劳伦斯:从无辜少女到领袖和战士,闪耀光芒的女性形象 点击右上方关注,解锁每天好文章 文 | 薛铮铮aa 编辑 | 薛铮铮aa 前言 在当代电影界中,詹妮弗·劳伦斯无疑是一位备受...

2026-01-10 22:03:59

​半球电器:从巅峰走到衰败,用了35年

​半球电器:从巅峰走到衰败,用了35年

半球电器:从巅峰走到衰败,用了35年 2017年5月湖北工商局公布了全省小家电抽查检验结果,此次抽查的不合格产品共21批次, 其中半球电器就占了4批,涉及的产品包括电磁炉、电饭锅...

2026-01-09 18:05:13

​武汉东湖学院现任领导

​武汉东湖学院现任领导

武汉东湖学院现任领导 周宝生:现任武汉东湖学院董事长;党籍:中共党员;出生日期:1953年;出生地:湖北省咸宁市嘉鱼县;曾获:湖北省十大杰出公民荣誉称号 王恕立:现任武汉...

2026-01-09 18:02:59

​被退市的乐视网,其独特的商业模式,暗藏破产原因?

​被退市的乐视网,其独特的商业模式,暗藏破产原因?

被退市的乐视网,其独特的商业模式,暗藏破产原因? #2月财经新势力# 近年来,我国互联网经济进入高速发展的新时代。 互联网视频行业也随着整个经济稳步发展,无论视频行业规模...

2026-01-09 18:00:45

​毛选第二篇:《湖南农民运动考察报告》当代启示

​毛选第二篇:《湖南农民运动考察报告》当代启示

毛选第二篇:《湖南农民运动考察报告》当代启示 今天我们来一起读毛选第二篇:《湖南农民运动考察报告》,在读这篇文章之前,我们先一起了解一下这篇文章的写作背景及当时的故...

2026-01-09 17:58:30

​对黄鳝自然习性的了解

​对黄鳝自然习性的了解

对黄鳝自然习性的了解 讲水产里的故事,用养殖人的语境,给你送来一个真材实料的匠心精品文章,敬请关注! 目前黄鳝养殖技术尚不完全成熟,有养鳝想法的朋友须慎重。本文仅从...

2026-01-09 17:56:16

​2018NBA夏季签约转会汇总:司机续约甜瓜离队

​2018NBA夏季签约转会汇总:司机续约甜瓜离队

2018NBA夏季签约转会汇总:司机续约甜瓜离队 7月1日消息 NBA自由球员市场开启,以下为交易与签约汇总一览: 亚特兰大老鹰 转入:林书豪(交易自篮网),卡梅罗-安东尼(三方交易自...

2026-01-09 17:54:02

​为什么秀恩爱,死得快?起底背后隐秘的心理动机,找到了3个原因

​为什么秀恩爱,死得快?起底背后隐秘的心理动机,找到了3个原因

为什么秀恩爱,死得快?起底背后隐秘的心理动机,找到了3个原因 谈了恋爱,许多人都想广而告之,让更多人知道自己有了甜蜜的爱情。 而自从有了朋友圈,秀恩爱的现象就越来越风...

2026-01-09 17:51:47

​盘点现役女排国手球衣号码,分析技术特点及奥运前景,俩数字空缺

​盘点现役女排国手球衣号码,分析技术特点及奥运前景,俩数字空缺

盘点现役女排国手球衣号码,分析技术特点及奥运前景,俩数字空缺 每一名上场打球的球员都会有自己的球衣号码,对于在国家队效力的女排国手而言,能在国家队拥有固定数字无疑是...

2026-01-09 17:49:33

​火水未济卦——易经六十四卦详解

​火水未济卦——易经六十四卦详解

火水未济卦——易经六十四卦详解 既济卦讲的是由盛转衰的过程,那么未济卦讲的就是由初始发展走向兴盛的过程。 火水未济卦 #暑期创作大赛# 未济卦火上水下,水火不相交,即水火...

2026-01-09 17:47:18

​美国黑人为何无法取得与白人同等的地位?林肯引爆的种族歧视炸弹

​美国黑人为何无法取得与白人同等的地位?林肯引爆的种族歧视炸弹

美国黑人为何无法取得与白人同等的地位?林肯引爆的种族歧视炸弹 1865年4月14日福特剧院,当刺杀了林肯的精神病枪手在舞台上大喊“ 这就是暴君的下场 ”时,他一定不会想到这位“...

2026-01-09 17:45:04

​索马里海盗连美国军舰都敢抢,为何不敢抢中国船只?有两个原因

​索马里海盗连美国军舰都敢抢,为何不敢抢中国船只?有两个原因

索马里海盗连美国军舰都敢抢,为何不敢抢中国船只?有两个原因 前言 索马里海盗作为嚣张跋扈,无恶不作的代言词, 在亚丁湾海域见人就抢,引发多次劫持伤人事件。 他们的行为就...

2026-01-08 21:46:22