数据结构-约瑟夫环

据说著名犹太历史学家Josephus有过以下的故事:

在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。

然而Josephus 和他的朋友并不想遵从。

首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。

问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

这个问题被世人们称作为约瑟夫问题,这个圈也可以称作为约瑟夫环!

我们也可以将这个问题和小时候玩过的丢手绢联想起来,一群小朋友围成一个圈,约定其中一个小朋友从1开始报数,然后后面的小朋友按顺序报数,当一个小朋友报数为k时,则出列,他的下一位又从1开始报数,直到所有人都出列为止~

对于这个问题:我们需要使用一个不带头节点的单向环形链表来处理,先构成一个有n个结点的单向循环链表,然后从k结点起,开始从1计数,计数到m时,对应结点从链表中删除,被删除结点的下一个结点又从1开始计数,直到最后一个结点也被删除后,算法结束!在这里插入图片描述
单向环形链表的具体实现思路

  • 首先,创建一个first结点指向第一个结点,并且这个first结点不能移动!然后再创建一个辅助结点pNode,同样指向第一个结点!而且,即使只有一个结点,我们也可以让其形成环状结构,也就是将第一个结点的next域指向自己!
  • 当第二个结点加入进链表的时候,我们将第一个结点的next域指向第二个结点,也就是将pNode.next指向第二个结点,然后再将第二个结点的next域指向first结点,最后将pNode结点下移!依次类推,我们就可以得到一个环形的单向链表!
    // 添加结点
    public void addNode(int number) {
        if (number < 2) {
            System.out.println("至少需要两个结点!");
            return;
        }
        Node pNode = null;
        for (int i = 1; i <= number; i++) {
            Node node = new Node(i);
            if (i == 1) {
                first = node;
                first.next = first;
                pNode = node;
            } else {
                pNode.next = node;
                node.next = first;
                pNode = node;
            }
        }
    }
    // 遍历结点
    public void display() {
        if (first == null) {
            System.out.println("该链表为空!");
            return;
        }
        Node pNode = first;
        while(true) {
            System.out.println(pNode);
            if (pNode.next == first) {
                break;
            }
            pNode = pNode.next;
        }
    }
}

约瑟夫问题的具体实现

假设链表中一共有5个结点,我们从第1个结点开始计数,当计数到2时,对应结点从链表中删除,下一个结点继续从1开始计数,直到剩下最后一个结点!简单的演算,我们可以得到删除的顺序为2->4->1->5->3。

具体实现以上所述的操作,我们需要创建一个辅助结点helper,事先指向first结点的前一个结点,假设此时first指向第一个结点,那么helper就指向最后一个结点!

但在问题的描述中,我们可以定义从第k个结点开始计数,于是我们先将first结点和helper结点移动k-1次!

  • 设置一个变量count存储规定的计数,当计数为count时,对应结点从链表中删除,同时,first结点和helper结点也需要移动count-1次,这样first正好指向待删除的结点,而help指向前一个结点。
  • 此时,开始进行结点删除操作!首先将first结点向下移动一次,即first=first.next,然后再将helper.next指向first!
    // 约瑟夫
    public void Josephus(int startNo, int count) {
        if (startNo < 1 || first == null || count < 0 || count > size) {
            System.out.println("参数输入有误!");
            return;
        }
        Node helper = first;
        // 将helper和first移动至第一个计数的结点
        for (int i = 0; i < startNo - 1; i++) {
            helper = helper.next;
            first = first.next;
        }
        // 将helper移动到first的前一个结点
        while(true) {
            if (helper.next == first) {
                break;
            }
            helper = helper.next;
        }
        while(true) {
            if (helper == first) {
                break;
            }
            // 开始计数,将first移动至删除结点
            for (int i = 0; i < count - 1; i++) {
                first = first.next;
                helper = helper.next;
            }
            System.out.println(first);
            first = first.next;
            helper.next = first;
        }
        System.out.println("最后的结点为:" + first);
    }
}
  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值