【数据结构】单链表成环之约瑟夫问题

382 阅读4分钟

上期记录了两个经典的单链表操作题; 单链表倒转 和 单链表奇偶节点分离

再出个小节加深记忆,讲讲环形单链表,也就是末尾节点.next = 头节点;

注意一些链表长度为1的情况;

约瑟夫问题

也称为丢手绢问题,处理问题的时候建议将模型看成一个环。数组也不是不可以,只是没有那么高的性能。

据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

这场游戏中,k = 3,自杀人顺序为:

3,6,9,12,15,18,21,24,27,30,33,36,39,1,5,10,14,19,23,28,32,37,41,
7,13,20,26,34,40,8,17,29,38,11,25,2,22,4,35,16

活下来的人是:31

机智的约瑟夫,安排自己和他的基友在 16 和 31 的位置,16是最后一个自杀名额,数到16的时候,前面的犹太人都死光了,也没人管他们。真实剧情可能是数到 35 号,35号自杀,然后约瑟夫就和他基友就一起溜了。

解法1 - 单链表

将单链表成环,也就是 末尾节点指向头结点,在这之后,变成了一个环,严格来说没有什么头尾节点了。

先看代码吧:

public static int getWinner(int count,int k){
		// 保持好习惯,防御编程
        if(count <= 1 || k == 1){
            return count;
        }
		// val 从 1 开始;
        Test.Node head = new Test.Node(1);
        Test.Node cur = head;

        for (int i = 2; i <= count; i++) {
            Test.Node node = new Test.Node(i);
            cur.next = node;
            cur = node;
        }
        //制造形成一个环;
        cur.next = head;
        cur = head;


        int index = 1;
        while(cur.next != null){
            Test.Node next = cur.next;
            if (index % k == k-1) {
                //下一个就是要杀掉的人;
                if(cur != next.next){
                    //禁止自己指向自己
                    cur.next = next.next;
                } else {
                    cur.next = null;
                }
                System.out.println("kill:" + next.val);
                index = 0;
            }

            if(cur.next != null){
                cur = cur.next;
            }

            index ++;
        }

        return cur.val;
    }
  1. 没什么特别的逻辑,很简单。一开始自己造了一个链表环,后续开始遍历。
  2. 在 k 的倍数前面那个位置修改指向,跳过那个需要选中的节点;这样可以避免引入一个 pre 指针。
  3. 注意,剩最后两个节点的时候,还得选中一个出来,在改变指向的时候,不可自己指向自己,这样会陷入死循环;

解法2 - Queue队列

还可以利用队列来实现,队列有先进先出的特性。每次访问的时候 poll 一个元素,若符合条件,丢弃;不符合条件,继续add 到队列的末尾。这个事件复杂度跟 环装链表应该是一样的,因为java的大部分队列也是用链表来实现;

public static int getWinner2(int count,int k){
        if(count <= 1 || k == 1){
            return count;
        }

        LinkedList queue = new LinkedList<Integer>();
        for (int i = 0; i < count; i++) {
            queue.add(i+1);
        }

        int index = 1;
        while(queue.size()>1){
            Object val = queue.poll();
            if(index % k == 0){
                System.out.println("kill:"+val);
                index = 0;
            } else {
                queue.add(val);
            }
            index++;
        }
        return (int) queue.poll();
    }

解法3 - 数组

利用数组逻辑会较复杂,且需要记录当前还剩多少节点,需要进行元素移动;还有,数组实现性能较差,这边不再展开。具体实现,大家有兴趣可以谷歌搜一搜吧;

招聘广告 🐂

【优酷】杭州团队,长期招聘!!!

   - 前端「急」
   - Java 后端 「爆」
   - 移动端:安卓 & iOS 「热」

办公地点:蚂蚁Z空间。

面试方式:电话&视频优先。

主要有优酷少儿、创新项目等业务,P6/P7 都有。

想试一试的小伙伴,邮件联系 hdtpjhz@163.com