太透彻了:约瑟夫环的三种解法

2024-02-17 11:50
文章标签 三种 约瑟夫 解法 透彻

本文主要是介绍太透彻了:约瑟夫环的三种解法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

????????关注后回复 “进群” ,拉你进程序员交流群????????

作者丨bigsai

来源丨bigsai

前言

约瑟夫环问题是算法中相当经典的一个问题,其问题理解是相当容易的,并且问题描述有非常多的版本,并且约瑟夫环问题还有很多变形,这篇约瑟夫问题的讲解,一定可以带你理解通通!

什么是约瑟夫环问题?

约瑟夫环问题在不同平台被"优化"描述的不一样,例如在牛客剑指offer叫孩子们的游戏,还有叫杀人游戏,点名……最直接的感觉还是力扣上剑指offer62的描述:圆圈中最后剩下的数字。

问题描述:

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

当然,这里考虑m,n都是正常的数据范围,其中

  • 1 <= n <= 10^5

  • 1 <= m <= 10^6

对于这个问题,你可能脑海中有了印象,想着小时候村里一群孩子坐在一起,从某个开始报数然后数到几出列,下一个重新开始一直到最后一个。不同人用不同方法解决,青铜直接模拟,钻石会优化一下,王者用公式,下面详细给大家讲解思路。

循环链表模拟

这个问题最本质其实就是循环链表的问题,围成一个圈之后,就没有结尾这就是一个典型的循环链表嘛!一个一个顺序报数,那不就是链表的遍历枚举嘛!数到对应数字的出列,这不就是循环链表的删除嘛!

链表模拟

并且这里还有非常方便的地方:

  • 循环链表的向下枚举不需要考虑头尾问题,直接node=node.next向下

  • 循环链表的删除也不需要考虑头尾问题,直接node.next=node.next.next删除

当然也有一些需要注意的地方

  • 形成环形链表很简单,只需要将普通链表的最后一个节点的next指向第一个节点即可

  • 循环链表中只有一个节点的时候停止返回,即node.next=node的时候

  • 删除,需要找到待删除的前面节点,所以我们删除计数的时候要少计一位,利用前面的那个节点直接删除后面节点即可

这样,思路明确,直接开撸代码:

class Solution {class node//链表节点{int val;public node(int value) {this.val=value;}node next;}public int lastRemaining(int n, int m) {if(m==1)return n-1;//一次一个直接返回最后一个即可node head=new node(0);node team=head;//创建一个链表for(int i=1;i<n;i++){team.next=new node(i);team=team.next;}team.next=head;//使形成环int index=0;//从0开始计数while (head.next!=head) {//当剩余节点不止一个的时候//如果index=m-2 那就说明下个节点(m-1)该删除了if(index==m-2){head.next=head.next.next;index=0;}else {index++;}head=head.next;}return head.val;}
}

当然,这种算法太复杂了,大部分的OJ你提交上去是无法AC的,因为超时太严重了,具体的我们可以下面分析。

有序集合模拟

上面使用链表直接模拟游戏过程会造成非常严重非常严重的超时,n个数字,数到第m个出列。因为m如果非常大远远大于n,那么将进行很多次转圈圈。

所以我们可以利用求余的方法判断等价最低的枚举次数,然后将其删除即可,在这里你可以继续使用自建链表去模拟,上面的while循环以及上面只需添加一个记录长度的每次求余算圈数即可:

int len=n;
while (head.next!=head) {if(index==(m-2)%len){head.next=head.next.next;index=0;len--;}else {index++;}head=head.next;
}

但我们很多时候不会手动去写一个链表模拟,我们会借助ArrayList和LinkedList去模拟,如果使用LinkedList其底层也是链表,使用ArrayList的话其底层数据结构是数组。不过在使用List其代码方法一致。

List可以直接知道长度,也可删除元素,使用List的难点是一个顺序表怎么模拟成循环链表?

咱们仔细思考:假设当前长度为n,数到第m个(通过上面分析可以求余让这个有效的m%n不大于n)删除,在index位置删除。那么删除后剩下的就是n-1长度,index位置就是表示第一个计数的位置,我们可以通过求余得知走下一个删除需要多少步,那么下个位置怎么确定呢?

删除3号下标

你可以分类讨论看看走的次数是否越界,但这里有更巧妙的方法,可以直接求的下一次具体的位置,公式就是为:

index=(index+m-1)%(list.size());

因为index是从1计数,如果是循环的再往前m-1个就是真正的位置,但是这里可以先假设先将这个有序集合的长度扩大若干倍,然后从index计数开始找到假设不循环的位置index2,最后我们将这个位置index2%(集合长度)即为真正的长度。

真实位置计算

使用这个公式一举几得,既能把上面m过大循环过多的情况解决,又能找到真实的位置,就是将这个环先假设成线性的然后再去找到真的位置,如果不理解的话可以再看看这个图:

这种情况的话大部分的OJ是可以勉强过关的,面试官的层面也大概率差不多的,具体代码为:

class Solution {public int lastRemaining(int n, int m) {if(m==1)return n-1;List<Integer>list=new ArrayList<>();for(int i=0;i<n;i++){list.add(i);}int index=0;while (list.size()>1){index=(index+m-1)%(list.size());list.remove(index);}return list.get(0);}
}

递归公式解决

我们回顾上面的优化过程,上面用求余可以解决m比n大很多很多的情况(即理论上需要转很多很多圈的情况)。但是还可能存在n本身就很大的情况,无论是顺序表ArrayList还是链表LinkedList去频繁查询、删除都是很低效的。

所以聪明的人就开始从数据找一些规律或者关系。

先抛出公式:

f(n,m)=(f(n-1,m)+m)%n
f(n,m)指n个人,报第m个编号出列最终编号

下面要认真看一下我的分析过程:

我们举个例子,有0 1 2 3 4 5 6 7 8 9十个数字,假设m为3,最后结果可以先记成f(10,3),即使我们不知道它是多少。

当进行第一次时候,找到元素2 删除,此时还剩9个元素,但起始位置已经变成元素3。等价成3 4 5 6 7 8 9 0 1这9个数字重写开始找。

f(10,3)删除第一个数

此时这个序列最终剩下的一个值即为f(10,3),这个序列的值和f(9,3)不同,但是都是9个数且m等于3,所以其删除位置是相同的,即算法大体流程是一致的,只是各位置上的数字不一样。所以我们需要做的事情是找找这个序列上和f(9,3)值上有没有什么联系

寻找过程中别忘记两点,首先可通过%符号对数字有效扩充,即我们可以将3 4 5 6 7 8 9 0 1这个序列看成(3,4,5,6,7,8,9,10,11)%10.这里的10即为此时的n数值。

另外数值如果是连续的,那么最终一个结果的话是可以找到联系的(差值为一个定制)。所以我们可以就找到f(10,3)和f(9,3)值之间结果的关系,可以看下图:

f(10,3)删除一次和f(9,3)

所以f(10,3)的结果就可以转化为f(9,3)的表达,后面也是同理:

f(10,3)=(f(9,3)+3)%10
f(9,3)=(f(8,3)+3)%9
……
f(2,3)=(f(1,3)+3)%2
f(1,3)=0

这样,我们就不用模拟操作,可以直接从数值的关系找到递推的关系,可以轻轻松松的写下代码:

class Solution {int index=0;public int lastRemaining(int n, int m) {if(n==1)return 0;      return (lastRemaining(n-1,m)+m)%n;}
}

但是递归效率因为有个来回的规程,效率相比直接迭代差一些,也可从前往后迭代:

class Solution {public int lastRemaining(int n, int m) {int value=0;for(int i=1;i<=n;i++){value=(value+m)%i;}return  value;}
}

结语

我想,通过本篇文章你应该掌握和理解了约瑟夫环问题,这种裸的约瑟夫环问题出现的概率很大,考察很频繁,链表模拟是根本思想,有序集合模拟链表是提升,而公式递推才是最有学习价值的地方,如果你刚开始接触不理解可以多看几遍。如果能用公式递推给面试官说两句,讲讲原理,那一定会让面试官眼前一亮的!

-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

点击????卡片,关注后回复【面试题】即可获取

在看点这里好文分享给更多人↓↓

这篇关于太透彻了:约瑟夫环的三种解法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/717666

相关文章

二叉树三种遍历方式及其实现

一、基本概念 每个结点最多有两棵子树,左子树和右子树,次序不可以颠倒。 性质: 1、非空二叉树的第n层上至多有2^(n-1)个元素。 2、深度为h的二叉树至多有2^h-1个结点。 3、对任何一棵二叉树T,如果其终端结点数(即叶子结点数)为n0,度为2的结点数为n2,则n0 = n2 + 1。 满二叉树:所有终端都在同一层次,且非终端结点的度数为2。 在满二叉树中若其深度为h,则其所包含

Java格式化日期的三种方式

1)借助DateFormat类: public String toString(Date d) { SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); return sdf.format(d); } 2)使用String.format()方法。 String.format()的用法类似于C语

段,页,段页,三种内存(RAM)管理机制分析

段,页,段页         是为实现虚拟内存而产生的技术。直接使用物理内存弊端:地址空间不隔离,内存使用效率低。 段 段:就是按照二进制文件的格式,在内存给进程分段(包括堆栈、数据段、代码段)。通过段寄存器中的段表来进行虚拟地址和物理地址的转换。 段实现的虚拟地址 = 段号+offset 物理地址:被分为很多个有编号的段,每个进程的虚拟地址都有段号,这样可以实现虚实地址之间的转换。其实所谓的地

ApplicationContext 获取的三种方法

spring为ApplicationContext提供的3种实现分别 为:ClassPathXmlApplicationContext,FileSystemXmlApplicationContext和 XmlWebApplicationContext,其中XmlWebApplicationContext是专为Web工程定制的。使用举例如下:    1. FileSystemXmlApplicati

力扣刷题 杨辉三角(使用c++ vector解法)

杨辉三角 题目描述示例1示例2提示:代码 题目描述 给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中,每个数是它左上方和右上方的数的和。 示例1 输入: numRows = 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] 示例2 输入: numRows = 1

html中的标签元素大体被分为三种不同的类型:块状元素、内联元素(又叫行内元素)和内联块状元素讲解

元素分类--块级元素 什么是块级元素?在html中<div>、 <p>、<h1>、<form>、<ul> 和 <li>就是块级元素。设置display:block就是将元素显示为块级元素。如下代码就是将内联元素a转换为块状元素,从而使a元素具有块状元素特点。 a{display:block;} 块级元素特点: 1、每个块级元素都从新的一行开始,并且其后的元素也另起一行。(真霸道,一个块级元

随机数初始化数组然后用选择法(三种,细微的区别)对数组进行排序

#include<stdio.h>#include<stdlib.h>#include<time.h>//时间头文件 int main(){time_t ts;//设置时间变量 unsigned int randdata = time(&ts);//获取时间,转换为无符号int srand(randdata);//设置随机数种子int a[10];//用随机数初始化数组for(int i=

Java反射之类的实例对象的三种表示方式

<span style="font-size:14px;">package com.imooc.reflect;public class ClassDemo1 {public static void main(String[] args) {//Foo的实例对象如何表示Foo foo1 = new Foo();//foo1就表示出来了//Foo这个类,也是一个实例对象,Class类的实例对象,如

调用FileOutputStream中的三种write方式

package fileoutputstream;import java.io.FileOutputStream;import java.io.IOException;//import java.io.OutputStream;public class FileOutputStreamDemo2 {public static void main(String[] args) throws I

【java编程(在线笔试)】【链表】两道k个一组翻转链表题目(包含非递归和递归两种解法)

一、给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点也翻转顺序。 1. 非递归解法 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode(int x) {