算法合集 | 链表 | 约瑟夫环问题

149 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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。

图片.png

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");

图片.png

可以看到链表是存在着部分的性能优势的,通过扩大人数,也能够更加直观的看到结果

图片.png

链表存在优势但是优势并不明显,只能说两种方法都可以用来解决约瑟夫环,并不会有太大的性能差距

参考

百度百科-约瑟夫环问题