传说罗马人占领了乔塔帕特,41 个犹太人被围堵在一个山洞里。他们拒绝被俘虏,而决定集体自杀,大家决定了一个自杀方案,41 个人围成一个圈,由第 1 个人开始顺时针报数,每报数为 3 的人立刻自杀,然后再由下一个人重新从 1 开始报数,依旧是每报数为 3 的人立刻自杀,依次循环下去。其中两位犹太人并不想自杀,是数学家约瑟夫和他的朋友,他们发现了自杀方案的规律,选了两个特定的位置,最后只剩下他们两人,而活了下来。那么这两个位置分别是什么?
这个问题转化成要解决的通用问题:即 n 个人围成一个圈,这 n 个人的编号从 0——(n-1), 第一个人(编号为0的人)从 1 开始报数,报数为 m 的人离开,再从下一个开始从 1 开始报数,报数为 m 的人离开,依次循环下去,直到剩下最后一个人(也可以剩最后两个,少循环一次就是了),那么,把最后一个人的编号打印出来。
类似问题:有30个小孩儿,编号从1-30,围成一圈依次报数,1、2、3 报到 3 的小孩儿退出这个圈, 然后剩下的小孩儿重新报数 1、2、3,问最后剩下的那个小孩儿的编号是多少?(看到这题几次了0.0)
方法一:数组
function countOff(num,m){
let players=[];
for(let i=1;i<=num;i++){
players.push(i);
}
let flag=0;
while(players.length>1){// 剩下一人,结束条件
let outPlayerNum=0,len=players.length;
for(let i=0;i<len;i++){
flag++;
if(flag===m){
flag=0;
console.log("出局:"+players[i-outPlayerNum]);
players.splice(i-outPlayerNum,1);
outPlayerNum++;
}
}
}
// return players[0];
console.log("剩下:"+players[0]);
}
// console.log("剩下:"+find(100,5))
countOff(30,3) // 剩下:29
方法二:循环队列实现
队列数据结构是遵循先进先出(也可以说成先来先服务)原则的一组有序的项。队列的尾部添加新元素,并从顶部(头部)移除元素。最新添加的元素必须排在队列的末尾。
function MyCircularQueue() {
var items = [];
//向队列插入元素
this.enQueue = function (value) {
return items.push(value);
}
//删除元素
this.deQueue = function () {
return items.shift();
}
//查看队列长度
this.size = function () {
return items.length;
}
}
function countOff(m, n) {
var queue = new MyCircularQueue();
//将名单存入队列
for (var i = 1; i <= m; i++) {
queue.enQueue(i);
}
var loser = '';
while (queue.size() > 1) {
for (var i = 0; i < n - 1; i++) {
queue.enQueue(queue.deQueue());
}
loser = queue.deQueue();
console.log('被淘汰的人为:' + loser);
}
// return queue.deQueue();
console.log('获胜者:' + queue.deQueue());
}
countOff(30, 3) // 获胜者:29
方法三:循环链表实现
链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称为指针或链接)组成。双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接,而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向下一个元素。循环链表可以像链接一样只是单向引用,也可以像双向链表一样有双向引用。循环链表和链表之间唯一的区别在于,最后一个元素指向下一个元素的指针,不是引用 null,而是指向第一个元素。
首先将循环双链表的构造函数实现出来,只写了要用到的方法,将文件存为 circularLinkedList.js
function CircularLinkedList() {
let Node = function(element) {
this.element = element;
this.next = null;
this.prev = null;
};
let head = null;
let tail = null;
let length = 0;
//从链表尾部添加一个新的项
this.append = function(element) {
let node = new Node(element);
let current;
if(!head) {
head = node;
tail = node;
}else {
current = tail;
current.next = node;
node.prev = current;
node.next = head;
tail = node;
head.prev = tail;
}
length++;
};
//返回元素在链表中的索引,如果链表中没有该元素则返回 -1
this.indexOf = function(element) {
let current = head,
index = 0;
do{
if(element == current.element) {
return index;
}
index++;
current = current.next;
}while(current.prev !== tail);
return -1;
};
//从链表的特定位置移除一项
this.removeAt = function(position) {
if(position > -1 && position < length) {
let current = head;
let previous;
let index = 0;
if(position === 0) {
head = current.next;
if(length === 1) {
tail = null;
}else {
head.prev = tail;
tail.next = head;
}
}else if(position === length-1) {
current = tail;
tail = current.prev;
tail.next = head;
head.prev = tail;
}else {
if(0 < position < length/2) {
while(index++ < position) {
previous = current;
current = current.next;
}
}else {
let current = tail;
let index = length-1;
while(position < index--) {
previous = current;
current = current.prev
}
}
previous.next = current.next;
current.next.prev = previous;
}
length--;
return current.element;
}else {
return null;
}
};
//根据元素,从链表中移除一项
this.remove = function(element) {
let index = this.indexOf(element);
return this.removeAt(index);
};
//由于链表项使用了 Node类,就需要重写继承自 JS 对象默认的 tostring 方法,让其只输出元素的值
this.toString = function() {
let current = head;
let string = '';
do{
string += current.element + ' ';
current = current.next;
}while(current.prev !== tail);
return string;
};
//返回链表包含的元素个数。与数组的 length 属性类似
this.size = function() {
return length;
};
//tail 变量是该构造函数的私有变量(这意味着它不能被构造函数外部访问和更改)。但是,如果我们需要在外部循环访问链表,就需要提供一种获取链表的第一个或最后一个元素的方法
this.getTail = function() {
return tail;
};
}
有了循环链表这种数据结构,我们就可以拿来解决问题了:
function josephRing(n, m) {
if(n<=1 || m<1) {
console.log("you can't play Joseph's game. n must be bigger than 1, m must be bigger than 0");
return;
}
let circular = new CircularLinkedList();
for(let i=0;i<n;i++) {
circular.append(i);
}
//给实例化对象添加属性,为当前项,将链表的最后一个元素引用赋值给它
circular.current = circular.getTail();
//给实例化对象添加方法,将 current 往前移动 m 次,移动第一次时指向的是第一个元素,因为 current 最开始指向的是最后一个元素
circular.advance = function(m) {
while(m>0) {
this.current = this.current.next;
m--;
}
};
//当链表只剩最后一项时,循环结束
while(circular.size() > 1) {
circular.advance(m);
//以 current 元素为参数删除该项
circular.remove(circular.current.element);
}
console.log(parseInt(circular.toString()) + 1 + ' is the winner.');
}
let start = new Date().getTime();
josephRing(10000,3);
let end = new Date().getTime();
console.log('====' + (end - start) + '====');
方法四:递归
function countOff(N, M) {
if (N < 1 || M < 1) {
return;
}
let source=[];
for(let i=1;i<=N;i++){
source.push(i);
}
// const source = Array(...Array(N)).map((_, i) => i + 1);
let index = 0;
while (source.length>1) {// 剩下一人,结束条件
index = (index + M - 1) % source.length;
console.log('出局:'+source[index]);
source.splice(index, 1);
}
console.log('剩下:'+source[0])
}
countOff(30,3) // 剩下:29
文章参考:
https://zhuanlan.zhihu.com/p/52993728
https://blog.csdn.net/qq_43043859/article/details/100987897