今天跟大家唠唠“约瑟夫”这个事儿。起初,我也就是闲着没事干,想找点啥动动脑子,就瞅见了这么个问题。名字听着还挺洋气,叫“约瑟夫环”或者“约瑟夫问题”。
初识与好奇
我一看这问题描述,说是一堆人围成一圈,从某个人开始报数,报到特定数字的那个人就出局,然后下一个人再从1开始报数,又淘汰一个,这么一直玩下去,剩下那一个就是赢家。听起来不复杂嘛有点像咱们小时候玩的游戏。
我还特地去瞅了瞅“约瑟夫”这名字,据说在希伯来语里头,有“上帝会再次增加”或者“上帝是仁慈的”这么个意思。当时我就乐了,这问题里头可是一个劲儿地“减少”人,淘汰出局,好像跟“增加”和“仁慈”不太沾边儿,反而有点残酷淘汰赛的感觉。
动手试试看
我寻思着,这不就是个模拟嘛简单。打开我那吃饭的家伙,就开始琢磨怎么弄。
第一反应:用个列表不就行了?
我的思路特直接:
- 先搞个列表,把从1到N所有人都放进去。
- 然后就开始数数,数到M的那个人,就把他从列表里删掉。
- 删掉之后,从下一个人开始,继续数,直到列表里只剩下一个人。
听着是不是挺顺溜?我也觉得是。一开始人少的时候,比如10个人,报3的出局,跑起来刷刷的,没啥问题。我还在纸上画了画,手动模拟了一遍,确保逻辑没错。
遇到点小麻烦
后来我就想,要是人多了?比如几百上千,甚至上万个人。我把人数N调得老大,那个M也随便设了个数字。这时候问题就来了。
我发现,每次从列表里头删掉一个人之后,如果用的是普通数组或者Python里那种动态列表,它后面的元素都得往前挪位置。人少的时候不觉得,人一多,这个挪动的操作就变得特别慢,整个程序就卡起来了,效率贼低。
我就抓耳挠腮了,这不行,肯定有更好的办法。我想起以前学过的数据结构,这种频繁插入删除的,特别是中间删除的,链表好像更合适点?
换个思路:链表来帮忙
对,用循环链表!
我的改进思路是这样的:
- 把这些人看成是链表上的节点,每个节点存着这个人的编号,并且指向下一个人。
- 因为是围成一圈,所以一个人的指针要指回第一个人,形成一个环。
- 要淘汰人的时候,找到那个人对应节点的前一个节点。
- 然后让前一个节点的“下一个”指针,直接跳过要被淘汰的节点,指向被淘汰节点的下一个节点。这样,被淘汰的人就从链表里“断开”了。
- 因为是循环链表,删除操作相对来说就只是改改指针,不用大规模移动数据,效率应该会高不少。
虽然用链表实现起来,代码细节上要比直接用列表删除元素稍微麻烦一点点,要小心处理指针,别指错了或者搞出个断链。但我还是动手写了写,感觉确实比之前那个暴力删除列表元素的方法要好些。
我知道这个问题还有数学公式解法,一步到位,贼快。但当时我就是想体验一把这个模拟的过程,看看怎么把这个圈子转起来,怎么把人一个个“请”出去。
的小总结
折腾了一番之后,总算是把这个约瑟夫问题给模拟出来了,也体会到了不同数据结构在不同场景下的优劣。最开始觉得“上帝会增加”这个名字寓意跟问题本身有点反差萌,现在想想,可能也暗指了解决问题后“智慧的增加”?哈哈,瞎猜的。
这个实践过程还是挺有意思的。从一个简单的想法开始,遇到问题,再想办法优化,一步步把东西弄出来,这个过程本身就挺有成就感的。下次再碰到类似的问题,我可能会先考虑下数据规模,再决定是用简单粗暴的方法还是得琢磨下更优的结构了。
