开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情
前言
本系列文章主要会总结一些常见的算法题目以及算法的易错点,难点,以及一些万用的公式,并且结合实际的 Leetcode 题目来进行加深理解以及实际应用,算法这种东西,属于是一到用时方恨少的类型,在平时总结一些常见的简单算法,经常磨练自己的算法思维,对于日常的开发还是能有不少的帮助的。
- 今天来利用环形链表解决经典的约瑟夫环问题
约瑟夫环问题是什么
约瑟夫问题,是一个计算机科学和数学中的问题,在计算机编程的算法中,类似问题又称为约瑟夫环,又称“丢手绢问题”。
据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
写一段程序将 n 个人围成一个圈,并且第 m 个人会被杀掉,计算一圈人中哪两个人会存活。
js数组和链表
在很多编程语言中,数组需要预先给定一个长度,添加新的元素很难;而且添加、删除等操作也比较烦,需要循环操作。
JavaScript提供了 push() 和 splice() 等函数来解决这类问题。
js没有内置链表:因为 JavaScript 的数组长度是动态的,所以就没有了链表的必要,而且数组可以满足链表几乎所有的操作。而且需要随机访问元素时,还是数组更方便。
作为链表唯一的优势,可能就是在删除增加的时候会有些许的性能优势,比方说上面提到的 约瑟夫环问题 就是一个需要对数据不断删除的问题。
关于链表的基本结构,以及在 JS 中的实现方式,前面已经讲过很多了,这里就不再赘述。
数组解法
假设人数为n个人,那么我们肯定是要优先构建一个长度为n的数组,每过m个人删除一个元素,那么现在关于数组有两种方案,一种是每过m个人后的元素从数组中删除,另一种是不做删除,将当前的元素置为0,作为一个已经排除的标记。
看一下置0的方法,通过一个数来保存排除的人数,当人数等于n的时候,当前排除的这个人,就是最后一个,在这之前就是不断地循环数组,跳过0,并且累加经过的人的个数,个数为m,就将排除的个数加一,并且数组当前置为0.
function josephRing(n, m) {
var arr = [];
for (var i = 1; i <= n; i++) {
arr.push(i);
};
var index = 0;
var out_num = 0;
var baoshu = 1;
//建立循环,不到n就一直继续
while (out_num < n) {
if (arr[index % n] != 0) {
if (baoshu % m == 0) {
out_num++;
if (out_num == n) {
return arr[index % n]
}
arr[index % n] = 0;
baoshu++;
} else {
baoshu++;
}
}
index++;
};
}
console.log(josephRing(40, 3));
链表解法
假设人数为n个人,那么我们可以优先构建长度为n的循环链表,并且把当前的链表个数作为节点的值,然后拿着循环链表去做删减的模拟操作。
主要的模拟过程就是判断链表的元素个数,不等于1的话就不断循环,每次循环都会向下前进间隔的人数的节点,然后删除掉当前节点,判断链表数量,一直重复,直到链表长度变成 1。
function createList(num) {
//链表节点的数据结构
function createNode(value) {
return {
value: value,
next: ''
}
}
//链表头节点
let head = createNode(1);
let node = head;
//自头节点之后创建节点之间的关联关系
for (let i = 2; i <= num; i++) {
node.next = createNode(i);
node = node.next;
}
//最后一个节点指向头节点,构成循环链表
node.next = head;
return head;
}
function josephRing(num, nth) {
//创建数据长度为num的循环链表
let node = createList(num);
//链表长度>1时,继续下一轮
while (num > 1) {
for (let i = 1; i <= nth - 1; i++) {
if (i == nth - 1) {
//i为nth-1,则node.next即为第nth个节点。剔除node.next
node.next = node.next.next;
//链表长度--
num--;
}
node = node.next;
}
}
//剩余的最后一个节点的value值即为最后一人编号
return node.value
}
console.log(josephRing(40,3)) // 28
性能差距
通过计算当前的时间来对比两个方法的性能差距
// 数组
let now1 = new Date();
console.log('数组');
console.log(josephRing1(40, 3));
let now2 = new Date();
console.log(now2 - now1 + "ms");
// 链表
let now3 = new Date();
console.log('链表');
console.log(josephRing2(40, 3));
let now4 = new Date();
console.log(now4 - now3 + "ms");
可以看到链表是存在着部分的性能优势的,通过扩大人数,也能够更加直观的看到结果
链表存在优势但是优势并不明显,只能说两种方法都可以用来解决约瑟夫环,并不会有太大的性能差距