皇上,还记得我吗?我就是1999年那个Linux伊甸园啊-----24小时滚动更新开源资讯,全年无休!

约瑟夫环的几种实现方式

摘要: 约瑟夫环是一个数学的应用问题:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。通常解决这类问题时我们把编号从0~n-1,最后 [1] 结果+1即为原问题的解。

一、概念

在开始正题之前,还是解释一下约瑟夫环是什么。约瑟夫环是一个数学的应用问题:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。通常解决这类问题时我们把编号从0~n-1,最后 [1]  结果+1即为原问题的解。

二、通过数组循环实现

最常规的思路就是通过一个数组,数组中每个元素都是一个人,然后对数组进行循环处理,每当数组中的人数到m时,将其标记为淘汰。直到最后数组中只有一个人未被淘汰。

所以,首先,我们需要一个计算方法,参数中有总数和淘汰数两个参数。

private static Integer compute(Integer total, Integer keyNumber) {
}

第二步,我们需要一个长度为total的布尔值数组,数组的index就表示了第几个人,元素的true和false表示了这个人是否被淘汰。一开始我们需要将所有人都设置为未被淘汰。

        /*开始时设置一个长度为总人数的数组,并将元素都设为true start*/
        Boolean[] peopleFlags = new Boolean[total];
        for (int i = 0; i < total; i++) {
            peopleFlags[i] = true;
        }
        /*开始时设置一个长度为总人数的数组,并将元素都设为true end*/

接下来第三步,我们需要三个变量:

第一个变量记录还剩多少人为被淘汰,这个变量的初始值为总人数;

第二个变量记录数到了多少,当这个参数等于淘汰数时归零;

第三个参数记录当时数到了第几个人,当这个参数等于总人数时归零(因为是一个圈,所以最后一个人数完后又轮到第一个人数数)

        int peopleLeft = total; //剩下的人数
        int count = 0; //计数器,每过一个人加一,加到keyNumber时归零
        int index = 0; //标记从哪里开始

第四步就开始循环计算了,首先判断剩余的人数是否大于一,如果大于一进入循环,取index,如果这个人未被淘汰,则计数器加一,如果等于keyNumber则淘汰这个人,否则跳过计数继续,当index等于总人数时

        while (peopleLeft > 1) {
            if (peopleFlags[index]) {
                //说明还没有被淘汰 计数器加1
                count++;

                if (count == keyNumber) {
                    count = 0; //计数器归0
                    peopleFlags[index] = false; //此人被淘汰
                    peopleLeft--;//未被淘汰的人数-1
                }
            }
            index++;

            //当当前人等于总人数时,则又从第一人开始计数
            if (index == total) {
                index = 0;
            }
        }

最后,计算结束后,数组中只有一个元素为true,而这个就是最后没被淘汰的那个人,现在我们就开开始找到是谁赢得了这次比赛。

        //经过上面的循环,现在数组中被淘汰的人都标记为false,最后没被淘汰都人标记为true
        for (int j = 0; j < total; j++) {
            if (peopleFlags[j]) {
                return j + 1;
            }
        }
        return null;

我们验证一下结果:

    public static void main(String[] args) {
        int total = 10;
        int keyNumber = 3;
        Integer winner = compute(total, keyNumber);
        System.out.println(total + "个人围成一圈数数,数到" + keyNumber + "的被淘汰,最后剩下的是第" + winner + "个人。");
    }

约瑟夫环的几种实现方式

OK,第一种方法成功。

二、通过链表方式实现

最早了解到这种方法是通过马士兵Java教学视频了解到的方法,其实通过这个方式可以让人更好的理解面向对象的思想和双向循环链表。

第一步,我们要抽象出一些对象,在约瑟夫环这个命题中,有这样两个对象:人和环。人具有几个属性:编号,左边的人和右边的人。环具有几个属性和方法:总人数,第一个人,最后一个人,添加人和移除人。

好了,第二步我们就开始实现自己抽象出来的对象,先是People对象:

public class People {
    private Integer id;
    //因为是一个圈子那么人就有左右两个属性,而且左右也都是人,所以明确定义其类型
    private People left;
    private People right;

    public People(Integer i) {
        this.id = i + 1;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public People getLeft() {
        return left;
    }

    public void setLeft(People left) {
        this.left = left;
    }

    public People getRight() {
        return right;
    }

    public void setRight(People right) {
        this.right = right;
    }
}

接下来是Circle对象:

public class Circle {
    private int total = 0;
    private People first = null;
    private People last = null;

    /**
     * 向环里添加人,将新人添加到链表的尾部
     *
     * @param newPeople 新加入都人
     */
    public void addPeople(People newPeople) {
        if (total <= 0) {
            //如果总数小于或等于零,说明这是环里一个人都没有,这是添加后第一个和最后一个都是自己
            first = newPeople;
            last = newPeople;
            //此时这个人都的左边和右边也都是自己
            newPeople.setLeft(newPeople);
            newPeople.setRight(newPeople);
        } else {
            //如果环里有人,则将新人加入到尾部,因为是尾部,所以操作的就是first和last
            last.setRight(newPeople);//将原先最后一个人的右边变为新人
            newPeople.setLeft(last);//将新人的左边设为原最后一个人也就是现在的倒数第二人
            newPeople.setRight(first);//将行人的右边设为第一个人
            first.setLeft(newPeople);//将第一人的左边变为新人
            last = newPeople;//将最有一个人设为新人
        }
        total++;
    }

    /**
     * 删除被淘汰的人
     *
     * @param deletePeople 需要删除的人
     */
    public void deletePeople(People deletePeople) {
        if (total <= 0) {
            return;
        } else if (total == 1) {
            //如果环中只有一个人,那么游戏结束,首尾都设为null
            first = null;
            last = null;
        } else {
            //环中的人大于一个,是开始删除操作
            if (deletePeople == first) {
                first = deletePeople.getRight(); //如果是第一个人,那么他右边都人就变成第一人
            } else if (deletePeople == last) {
                last = deletePeople.getLeft();   //相反,如果是最后一个人,那么他左边边都人就变成最后一人
            }

            //将删除的人左边的人的右边设为删除的人的右边的人,是不是有点儿绕,举个例子:
            //234,我们要删除3,那么将2的左边设为4。
            deletePeople.getLeft().setRight(deletePeople.getRight());
            //同理,将删除的人右边的人的左边设为删除人的左边的人。
            deletePeople.getRight().setLeft(deletePeople.getLeft());
        }
        total--;
    }

    public int getTotal() {
        return total;
    }

    public void setTotal(int total) {
        this.total = total;
    }

    public People getFirst() {
        return first;
    }

    public void setFirst(People first) {
        this.first = first;
    }

    public People getLast() {
        return last;
    }

    public void setLast(People last) {
        this.last = last;
    }
}

最后,我们开始计算了,先向环里添加人,然后取到第一个人进行处理,处理完第一个之后取到第一人之后的一个人继续处理,最后返回处理完的环。

    private static Circle compute(Integer total, Integer keyNumber){
        Circle circle = new Circle();
        for (int i = 0; i < total; i++) {
            People people = new People(i);
            circle.addPeople(people); //向环中添加人
        }

        Integer count = 0;//用来计数
        People people = circle.getFirst(); //先拿到第一个人
        while (circle.getTotal() > 1) {
            count++;
            if (count.equals(keyNumber)) {
                count = 0;
                circle.deletePeople(people);
            }
            people = people.getRight();//一个一个往后报数
        }
        return circle;
    }

下面我们来看一下执行结果:

   public static void main(String[] args) {
        int total = 10; //定义要添加的人数
        int keyNumber = 3; //数到3退出
        Circle circle = compute(total, keyNumber);
        System.out.println(total + "个人围成一圈数数,数到" + keyNumber + "的被淘汰,最后剩下的是第" + circle.getFirst().getId() + "个人。");
    }

约瑟夫环的几种实现方式

成功,是不是觉得思想上有点绕,但是这是典型的面向对象思想,可以多消化消化。

三、Java自带链表实现(LinkedList)

其实使用Java自带的LinkedList就可以很简单的实现我们要的效果。直接上代码了:

public class ThirdWay {
    public static void main(String[] args) {
        Integer total = 10;
        Integer keyNumber = 3;
        LinkedList<Integer> list = new LinkedList<>();
        for (int i = 0; i < total; i++) {
            list.addLast(i+1);
        }
        int index = 0;
        while (list.size() > 1) {
            for (int i = 1; i < keyNumber; i++) {
                if (index == list.size() - 1) {
                    index = 0;
                } else {
                    index++;
                }
            }
            list.remove(index);
        }
        System.out.println(total + "个人围成一圈数数,数到" + keyNumber + "的被淘汰,最后剩下的是第" + list.get(0) + "个人。");
    }
}

是不是很简单,能够这么简单的基础是因为链表的remove()方法的特殊性,第一轮循环时,index=2的元素就是3,但它找到需要删除的元素后链表size-1,此时index=2指向的是原index=3的元素,也就说index不用变,这样正好满足了我们的需求。

约瑟夫环还有其他的方法可以实现,但是我目前学习到的只有这三种方法实现,如果以后学习到其他的方法实现,再回来进行补充。

附源码地址:https://gitee.com/jack90john/joseph

转自 https://my.oschina.net/jack90john/blog/1791110