百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

数据结构与算法之约瑟夫问题

toyiye 2024-06-21 12:09 10 浏览 0 评论

约瑟夫问题描述的是什么?

约瑟夫问题:有 N 个人围成一圈,每个人都有一个编号,编号由入圈的顺序决定,第一个入圈的人编号为 1,最后一个为 N,从第 k (1<=k<=N)个人开始报数,数到 m (1<=m<=N)的人将出圈,然后下一个人继续从 1 开始报数,直至所有人全部出圈,求依次出圈的编号。

如何存储数据

面对一道题,首先需要思考,要选用什么样的数据结构来保存数据。约瑟夫问题描述的是循环报数出圈的问题,报数始终围绕同一个方向进行,所以可以使用单向环形链表来存储。

每当有一个人入圈,就创建出一个新的节点,节点间首尾相连,代码如下:

//节点
class Node {
    //节点序号
    private Integer no;
    //指向下一个节点的引用
    private Node next;

    public Node(Integer no) {
        this.no = no;
    }

    @Override
    public String toString() {
        return "Node{" +
            "no=" + no +
            ",next=" + (next == null ? "" : next.no) +
            '}';
    }
}

//单向环形链表
class SingleCycleLinkedList {
    //头引用
    private Node head;
    //尾引用
    private Node tail;
    //链表长度
    private int size;

    /**
     * 初始化指定长度序号递增的环形链表
     *
     * @param size
     */
    public SingleCycleLinkedList(int size) {
        for (int i = 1; i <= size; i++) {
            add(new Node(i));
        }
        this.size = size;
    }

    /**
     * 插入环形链表
     *
     * @param node
     */
    public void add(Node node) {
        if (node == null) {
            return;
        }

        //链表为空,直接将 head, tail 引用指向新节点
        if (size == 0) {
            head = node;
            tail = node;
            size++;
            return;
        }

        //链表不为空,将新节点放在链表最后,同时新节点的 next 引用指向 head,完成成环操作
        tail.next = node;
        tail = tail.next;
        tail.next = head;
        size++;
}

    ...
}

核心逻辑在 add 方法中,需要注意的是,当链表为空时,新增的节点不能自成环,也就是 next 引用不能指向自己,所以第一次新增时,直接将 head, tail 指向新增的节点,不去操作节点的 next 引用。当链表不为空时,需要引入成环的步骤,成环步骤分解如下:

1.tail.next = node

2.tail = tail.next

3.tail.next = head

这样即完成了成环操作。

在 main 方法中进行测试,构造长度为 10 的环形链表:

SingleCycleLinkedList singleCycleLinkedList = new SingleCycleLinkedList(10);
System.out.println(singleCycleLinkedList);

结果如下:

Node{no=1,next=2}, Node{no=2,next=3}, Node{no=3,next=4}, Node{no=4,next=5}, Node{no=5,next=6}, Node{no=6,next=7}, Node{no=7,next=8}, Node{no=8,next=9}, Node{no=9,next=10}, Node{no=10,next=1}

解决约瑟夫问题

通过上一步,完成了数据的存储,接下来需要解决如何循环报数出圈的问题。题目要求从第 k 个人开始报数,所以要先找到报数的起始位置,然后开始循环报数,数到 m 的人出圈,也就是对应的节点要移出链表。需要注意的是,单向链表的节点无法自我删除,如图所示:

如果想删除编号为 2 的节点,cur 引用必须指向 1,这样才能将 1 的 next 引用从原来的 2 指向 3:

所以,在找报数的起始位置时,应当从起始位置的上一个位置开始计数,这样当寻找到待移除节点时,实际上是定位到了待移除节点的上一个节点。寻找报数起始位置的代码如下(代码中 start 变量就是参数 k):

//寻找开始报数的节点(这里从 tail 开始遍历,取报数节点的上一个节点,因为单向链表的节点删除必须依赖上一个节点)
Node tmp = tail;
int startIndex = 0;
while (startIndex++ != size) {
  if (start == startIndex) {
    break;
  }
  tmp = tmp.next;
}

找到了报数起始位置后,就要开始执行报数的操作,移除节点时需要注意,当链表中的节点数量只剩一个时,无需操作节点的 next 引用,直接将节点置空即可。 报数出圈的代码如下(代码中 step 变量就是参数 m):

//保存顺序出链的节点
List<Node> list = new ArrayList<>(size);

//开始计数,数到指定间隔后节点出链
int count = 1;
while (size > 1) {
    if (count == step) {
        //节点出链
        //1.定义一个引用,指向待删除节点
        Node delNode = tmp.next;
        //2.将当前节点的 next 引用指向待删除节点的下一个节点
        tmp.next = delNode.next;
        //3.链表长度-1
        size--;
        //4.置空待删除节点的 next 引用
        delNode.next = null;
        //5.保存已删除节点
        list.add(delNode);
        //6.重置计数器
        count = 1;
    } else {
        //继续循环计数
        tmp = tmp.next;
        count++;
    }
}

//链表只剩一个节点时,不需要操作next指针删除节点,直接将头尾置空
tmp.next = null;
head = null;
tail = null;
size = 0;
list.add(tmp);

注意,在移除节点后,必须要保证链表仍然成环,移除步骤分解如下(假设链表剩 3 个节点,要移出编号为 3 的节点):

1.Node delNode = tmp.next

2.tmp.next = delNode.next

3.delNode.next = null

报数出圈的完整代码如下:

    /**
     * 从 start 位置开始,每隔 step 后节点出链
     *
     * @param start 报数起始位置
     * @param step 报数出圈间隔
     * @return 依次出链的节点列表
     */
    public List<Node> poll(int start, int step) {
        if (start <= 0 || start > size) {
            throw new RuntimeException("起始位置需大于0并且小于等于链表长度");
        }
        if (step <= 0) {
            throw new RuntimeException("间隔需大于0");
        }

        //寻找开始报数的节点(这里从 tail 开始遍历,取报数节点的上一个节点,因为单向链表的节点删除必须依赖上一个节点)
        Node tmp = tail;
        int startIndex = 0;
        while (startIndex++ != size) {
            if (start == startIndex) {
                break;
            }
            tmp = tmp.next;
        }

        //保存顺序出链的节点
        List<Node> list = new ArrayList<>(size);

        //开始计数,数到指定间隔后节点出链
        int count = 1;
        while (size > 1) {
            if (count == step) {
                //节点出链
                //1.定义一个引用,指向待删除节点
                Node delNode = tmp.next;
                //2.将当前节点的 next 引用指向待删除节点的下一个节点
                tmp.next = delNode.next;
                //3.链表长度-1
                size--;
                //4.置空待删除节点的 next 引用
                delNode.next = null;
                //5.保存已删除节点
                list.add(delNode);
                //6.重置计数器
                count = 1;
            } else {
                //继续循环计数
                tmp = tmp.next;
                count++;
            }
        }

        //链表只剩一个节点时,不需要操作next指针删除节点,直接将头尾置空
        tmp.next = null;
        head = null;
        tail = null;
        size = 0;
        list.add(tmp);

        return list;
    }

对上述代码进行测试:

//n: 圈内人数, k: 报数的起始位置, m: 报数出队的间隔
int n = 10;
int k = 2;
int m = 3;

List<Node> pollList = singleCycleLinkedList.poll(k, m);
System.out.printf("size: %d, start: %d, step: %d\n", n, k, m);
System.out.println(pollList.stream().map(node -> node.no).collect(Collectors.toList()));

结果如下:

size: 10, start: 2, step: 3
[4, 7, 10, 3, 8, 2, 9, 6, 1, 5]

数据验证

当 n = 10, k = 2, m = 3 时,节点移除的分解步骤如下:

完整节点:Node{no=1}, Node{no=2}, Node{no=3}, Node{no=4}, Node{no=5}, Node{no=6}, Node{no=7}, Node{no=8}, Node{no=9}, Node{no=10}

4 出圈:Node{no=1}, Node{no=2}, Node{no=3}, Node{no=5}, Node{no=6}, Node{no=7}, Node{no=8}, Node{no=9}, Node{no=10}

7 出圈:Node{no=1}, Node{no=2}, Node{no=3}, Node{no=5}, Node{no=6}, Node{no=8}, Node{no=9}, Node{no=10}

10 出圈:Node{no=1}, Node{no=2}, Node{no=3}, Node{no=5}, Node{no=6}, Node{no=8}, Node{no=9}

3 出圈:Node{no=1}, Node{no=2}, Node{no=5}, Node{no=6}, Node{no=8}, Node{no=9}

8 出圈:Node{no=1}, Node{no=2}, Node{no=5}, Node{no=6}, Node{no=9}

2 出圈:Node{no=1}, Node{no=5}, Node{no=6}, Node{no=9}

9 出圈:Node{no=1}, Node{no=5}, Node{no=6}

6 出圈:Node{no=1}, Node{no=5}

1 出圈:Node{no=5}

5 出圈

出圈顺序依次为: [4, 7, 10, 3, 8, 2, 9, 6, 1, 5]。与结果一致。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码