前言: 约瑟夫环不愧是一道经典的算法题,原来也经常看到,但一直没有动手编码研究。最近又被同学提起这个问题,就研究了一下,发现这个问题可以挖掘的东西有很多,怪不得一直是面试的热门问题。

解法一,使用链表模拟:

使用链成环的单链表完全模拟这个计数淘汰的过程,计数到要淘汰的结点时,直接删除该结点。

typedef struct NODE{int num;struct NODE* next;
}PN;int cycle0(int n, int m){ // 使用链表实现PN* head = (PN*)malloc(sizeof(PN));head->num = 0;head->next = NULL;PN* tail = head; // 恒指向链表尾 for(int i = 1; i < n; ++i){tail->next = (PN*)malloc(sizeof(PN));tail = tail->next;tail->num = i;tail->next = NULL;}tail->next = head; // 链成环PN* p = head;int j = 0; // 报数器 while(p->next != p){ // 如果 p 的下一个结点指向自己,说明环中只剩一个结点 if(j == m-2){ // 每次报到 m-2 删除当前 p 指向结点的下一个结点 PN* q = p->next;p->next = q->next;free(q); // 释放内存 p = p->next;j = 0;}else{p = p->next;++j;}} return p->num;
}

解法二,使用数组模拟:

数组模拟法用数组下标对应人的编号。最简单直白的模拟方式,就是使用数组值来表示存活或者淘汰,一般我们用 0 和 1 来表示,如果数组元素值对应淘汰,则在计数时跳过该元素。

int cycle1(int n, int m){ // 使用数组实现 1 代表活 0 代表淘汰(反过来也可以) int a[n];for(int i = 0; i < n; ++i){a[i] = 1;}int i = 0, j = 0, count = 0;while(count < n-1){i %= n;if(a[i] == 1){if(j == m-1){j = 0;a[i] = 0;++count;}else ++j;++i;}else while(a[(++i)%n] == 0){} // 跳过淘汰者,这些人不计入报数}for(int i = 0; i < n; ++i){if(a[i] == 1){return i;}}
}

上面的代码写了一个循环跳过淘汰者,代码形式上太不美观。我们可以通过借助标志值来计数,优化代码形式,让代码看起来更美观,但并未优化代码效率。

int cycle2(int n, int m){ // 优化数组模拟法的代码 0 代表活 1 代表淘汰(反过来也可以)int a[n] = {0};int i = 0, j = 0, count = 0;while(count < n-1){if(a[i] == 0 && j == m-1){j = 0;a[i] = 1;++count;}j += 1-a[i]; // 利用 a[i] 的值来对 j 进行计数i = (i+1)%n;}for(int i = 0; i < n; ++i){if(a[i] == 0) return i;}
}

上面的代码虽然没有能提升效率,但给了一个优化思路,即充分利用数组元素的值。我们可以利用数组值来辅助定位到当前存活者的下一存活者,尽量跳过中间的淘汰者。辅助定位的方法是,当数组元素值等于元素下标时,表示此人存活,当需要淘汰当前的人时,就用后面一个元素的数组值覆盖当前的元素值,这样当前元素值和下标不等,表示当前这个人已被淘汰,还可以借助数组值定位到下一个可能存活的人身上。

int cycle3(int n, int m){ // 继续优化数组模拟法的代码,数组值 等于 下标表示存活 // 使用数组值引导到下一个人的下标int a[n];for(int i = 0; i < n; ++i){a[i] = i;}int i = 0, j = 0, count = 0;while(count < n-1){i %= n;if(a[i] == i){if(j == m-1){j = 0;a[i] = a[(i+1)%n];++count;}else ++j;++i;}else i = a[i]; // 优化的点 }for(int i = 0; i < n; ++i){if(a[i] == i){return i;}}
}

最后沿袭这个思路还可以进一步优化算法,还是通过数组值来确定下一存活者,但这次是精准定位到下一存活者。与上一方法不同的是,数组值存储的是下一个存活者的编号,使用两个索引,分别为 p 和 c,p 为上一个存活者的编号,其数组值为当前存活者编号,c 为当前存活者的编号,其数组值为下一个存活者的编号,当需要淘汰当前存活者时,令 a[p] = a[c] 即可,即上一存活者指向的是下一存活者,当前存活者的编号被覆盖,相当于在数组中删除了当前存活者。这种改进算法不仅不需要每次判断数组值等于多少,而且可达的数组值一定表示的是真实的下一个存活者,大大提升了上一算法的效率。

// 你可以类比二叉树的双亲表示法(使用数组表示二叉树)来理解这个算法
// 这个算法的本质是,相当于使用数组来模拟链表,数组值就是指针,覆盖数组值就相当于链表中的删除结点操作。
int cycle4(int n, int m){ // 继续优化数组模拟法的代码int a[n];a[n-1] = 0;for(int i = 0; i < n-1; ++i){a[i] = i+1;}int c = 0, p = n-1, j = 0, count = 0;while(count < n-1){if(j == m-1){j = 0;a[p] = a[c]; // 删除当前存活者,p 此时指向的就是下一存活者,所以 p 指针不需要移动。++count; }else{++j;p = c;}c = a[c];}return c;
}

如果你理解了上述算法的本质是模拟链表,那么就像我们给出的第一个链表模拟法的算法一样,我们使用一个指针便可以完成遍历和删除结点的操作,并不需要使用 p,c 两个索引来配合遍历和删除操作。

int cycle5(int n, int m){ // 使用单索引int a[n];a[n-1] = 0;for(int i = 0; i < n-1; ++i){a[i] = i+1;}int c = n-1, j = 0, count = 0;while(count < n-1){if(j == m-1){j = 0;a[c] = a[a[c]]; // 删除当前存活者 ++count; }else{++j;c = a[c];}}return c;
}

解法三,动态规划:

优点是代码简洁,时间复杂度仅为 O ( n ) O(n) O(n)。缺点是只能获得最后存活者的编号,无法像模拟法一样可以获取淘汰过程中的编号序列。

先给出状态转移方程:

假设有 n n n 个人,编号为 0 , 1 , ⋯ , n − 1 0,1,\cdots,n-1 0,1,⋯,n−1,每报数 m ( m < n ) m(m<n) m(m<n) 次淘汰一个人。
f ( n ) f(n) f(n) 表示 n n n 个人中最终存活者的编号。
{ f ( 1 ) = 0 f ( n ) = ( f ( n − 1 ) + m ) % n \begin{cases}f(1) = 0\\f(n) = (f(n-1)+m)\%n\end{cases} {f(1)=0f(n)=(f(n−1)+m)%n​

递推公式解释:

n n n 个人,编号为 0 , 1 , ⋯ , n − 1 0,1,\cdots,n-1 0,1,⋯,n−1,从 编号为 0 的人开始报数,第一个被淘汰的人,编号为 m − 1 m-1 m−1。此时剩下 n − 1 n-1 n−1 个人,下一次报数从编号为 m m m 的人开始。将这剩下的 n − 1 n-1 n−1 个人按报数顺序一字排开,序列为: m , m + 1 , ⋯ , n − 1 , 0 , 1 , ⋯ , m − 2 m,m+1,\cdots,n-1,0,1,\cdots,m-2 m,m+1,⋯,n−1,0,1,⋯,m−2 对比总人数为 n − 1 n-1 n−1 个人时的编号序列: 0 , 1 , ⋯ , n − 2 0,1,\cdots,n-2 0,1,⋯,n−2 可以得到两者的对应关系为 f ( n ) = ( f ( n − 1 ) + m ) % n f(n) = (f(n-1)+m)\%n f(n)=(f(n−1)+m)%n。可以认为这个递推公式就是通过上述找规律的方式看出来的。
这就意味着,如果我们已知总人数为 n − 1 n-1 n−1 时最终存活者的编号,就可以得到这个人在总人数为 n n n 时对应的编号。

上面为了方便,我们假设的是 m < n m<n m<n,其实当 m > n m>n m>n 时,递推式子不变。因为当 m > n m>n m>n 时,每报数 m m m 次淘汰一人,相当于每报数 m % n m\%n m%n 次淘汰一人,所以有 f ( n ) = ( f ( n − 1 ) + m % n ) % n = ( f ( n − 1 ) + m ) % n f(n) = (f(n-1)+m\%n)\%n=(f(n-1)+m)\%n f(n)=(f(n−1)+m%n)%n=(f(n−1)+m)%n,式子不变。

递推示例:

如果感觉上面描述的实在不好理解,可以自己找个例子用这个递推的式子实战一下,应该就有点感觉了。例如我们要求 n = 5 , m = 3 n=5,m=3 n=5,m=3 的情况,dp 过程是这样的:

  1. f ( 1 ) = 0 f(1)=0 f(1)=0,即人数为 1 时,最终存活者的编号为 0。
  2. f ( 2 ) = ( f ( 1 ) + 3 ) % 2 = ( 0 + 3 ) % 2 = 1 f(2)=(f(1)+3)\%2=(0+3)\%2=1 f(2)=(f(1)+3)%2=(0+3)%2=1,即那个在人数剩 1 个人时最终存活者的编号在人数为 2 时,对应的编号为 1。
  3. 同理 f ( 3 ) = ( 1 + 3 ) % 3 = 1 f(3)=(1+3)\%3=1 f(3)=(1+3)%3=1,即最终存活者的编号对应到总人数为 3 时,编号为 1。
  4. 同理 f ( 4 ) = ( 1 + 3 ) % 4 = 0 f(4)=(1+3)\%4=0 f(4)=(1+3)%4=0,对应为总人数为 4 时,编号为 0。
  5. 同理 f ( 5 ) = ( 0 + 3 ) % 5 = 3 f(5)=(0+3)\%5=3 f(5)=(0+3)%5=3,对应为总人数为 5 时,编号为 3。递推结束。

递归解法如下:

// dp 的递归解法
int cycle6(int n, int m){if(n == 1) return 0;return (cycle5(n-1,m) + m)%n;
}

递推解法如下:

// dp 的递推解法
int cycle7(int n, int m){int alive = 0; // 对应 i = 1 的结果 for(int i = 2; i <= n; ++i){alive = (alive + m)%i;}return alive;
}

最后附上完整的测试代码:

#include<stdio.h>
#include<stdlib.h>
typedef struct NODE{int num;struct NODE* next;
}PN;int cycle0(int n, int m){ // 使用链表实现PN* head = (PN*)malloc(sizeof(PN));head->num = 0;head->next = NULL;PN* tail = head; // 恒指向链表尾 for(int i = 1; i < n; ++i){tail->next = (PN*)malloc(sizeof(PN));tail = tail->next;tail->num = i;tail->next = NULL;}tail->next = head; // 链成环PN* p = head;int j = 0; // 报数器 while(p->next != p){ // 如果 p 的下一个结点指向自己,说明环中只剩一个结点 if(j == m-2){ // 每次报到 m-2 删除当前 p 指向结点的下一个结点 PN* q = p->next;p->next = q->next;free(q); // 释放内存 p = p->next;j = 0;}else{p = p->next;++j;}} return p->num;
}int cycle1(int n, int m){ // 使用数组实现 1 代表活 0 代表淘汰(反过来也一样) int a[n];for(int i = 0; i < n; ++i){a[i] = 1;}int i = 0, j = 0, count = 0;while(count < n-1){i %= n;if(a[i] == 1){if(j == m-1){j = 0;a[i] = 0;++count;}else ++j;++i;}else while(a[(++i)%n] == 0){}}for(int i = 0; i < n; ++i){if(a[i] == 1){return i;}}
}int cycle2(int n, int m){ // 优化数组的代码 0 代表活 1 代表淘汰(反过来也一样)int a[n] = {0};int i = 0, j = 0, count = 0;while(count < n-1){if(a[i] == 0 && j == m-1){j = 0;a[i] = 1;++count;}j += 1-a[i]; // 利用 a[i] 的值来对 j 进行计数i = (i+1)%n;}for(int i = 0; i < n; ++i){if(a[i] == 0) return i;}
}int cycle3(int n, int m){ // 继续优化数组的代码,数组值 等于 下标表示活 // 使用数组值引导到下一个人的下标int a[n];for(int i = 0; i < n; ++i){a[i] = i;}int i = 0, j = 0, count = 0;while(count < n-1){i %= n;if(a[i] == i){if(j == m-1){j = 0;a[i] = a[(i+1)%n];++count;}else ++j;++i;}else i = a[i]; // 优化的点 }for(int i = 0; i < n; ++i){if(a[i] == i){return i;}}
}
// 本质是使用数组模拟链表
int cycle4(int n, int m){ // 继续优化数组的代码 不需要每次都判断 数组值// 和上一种方法不同的是,数组值表示的是下一个存活的人int a[n];a[n-1] = 0;for(int i = 0; i < n-1; ++i){a[i] = i+1;}int c = 0, p = n-1, j = 0, count = 0;while(count < n-1){if(j == m-1){j = 0;a[p] = a[c]; // 删除当前存活的人 即 p 索引的数组值 ++count; }else{++j;p = c;}c = a[c];}return c;
}// 使用单索引实现
int cycle5(int n, int m){int a[n];a[n-1] = 0;for(int i = 0; i < n-1; ++i){a[i] = i+1;}int c = n-1, j = 0, count = 0;while(count < n-1){if(j == m-1){j = 0;a[c] = a[a[c]]; // 删除当前存活者 ++count; }else{++j;c = a[c];}}return c;
}// dp 的解法只能输出最后存活者的序号,无法输出淘汰序列
int cycle6(int n, int m){ // dp 的递归解法 if(n == 1) return 0;return (cycle5(n-1,m) + m)%n;
}int cycle7(int n, int m){ // dp 的递推解法 int alive = 0; // 对应 i = 1 的结果 for(int i = 2; i <= n; ++i){alive = (alive + m)%i;}return alive;
}int main(){int n = 10000, m = 3;printf("%d\n",cycle0(n,m));printf("%d\n",cycle1(n,m));printf("%d\n",cycle2(n,m));printf("%d\n",cycle3(n,m));printf("%d\n",cycle4(n,m));printf("%d\n",cycle5(n,m));printf("%d\n",cycle6(n,m));printf("%d\n",cycle7(n,m));return 0;
}

约瑟夫环解法大全(C语言版)相关推荐

  1. 约瑟夫环问题(C语言循环链表)

    1.约瑟夫环问题(C语言循环链表) 我相信大家都可以画出这个图,知道大体的解题的思想,但是却不知道代码该怎么下手,因此,下面我直接上代码,代码中该注释的地方我都进行了注释,希望到大家有帮助: #inc ...

  2. 约瑟夫环c语言代码顺序存储,顺序表实现约瑟夫环地问题,C语言.doc

    顺序表实现约瑟夫环地问题,C语言 计算机科学与工程学院 PAGE PAGE 2 <算法与数据结构>试验报告 计算机科学与工程学院 <算法与数据结构>试验报告[一] 专业班级 1 ...

  3. c语言约瑟夫环分函数,c语言实现约瑟夫环问题

    <c语言实现约瑟夫环问题>由会员分享,可在线阅读,更多相关<c语言实现约瑟夫环问题(16页珍藏版)>请在人人文库网上搜索. 1.一)基本问题1问题描述设有编号为1,2,小的n ...

  4. 约瑟夫环问题(C语言数组和循环链表)

    本文将用两种方式(数组和循环链表)求解约瑟夫环问题,为了更好理解,本文将从洛谷的P1996 约瑟夫问题出发. 题目描述 n个人围成一圈,从第一个人开始报数,数到 m的人出列,再由下一个人重新从1开始报 ...

  5. 约瑟夫环问题(c语言实现)

    约瑟夫环问题 问题描述 算法 1. 非递归法 算法理解 程序 2. 递归法 程序 3. 循环双链表 思路 程序 问题描述 有N个人,编号为 1-n,围成一个圈,编号1的人手上有一个热狗开始向下一个人传 ...

  6. 约瑟夫环问题(c语言)

    约瑟夫问题是一个有趣的游戏.有n个人围成一圈,从第一个人开始沿顺时针方向报数(从1到3报数),凡报到3的人退出圈子,问最后留下的是原来第几号的那个人? 比如输入5,输出4 思路:循环链表,模拟运行,见 ...

  7. 单链表实现约瑟夫环(JosephCircle)(C语言)

    //使链表形成一个环 void FormCyc(PSListNode *pHead) {if (NULL == pHead){return;}else{PSListNode pNode = *pHea ...

  8. 映射递归循环-约瑟夫环问题递归解法的一点理解

    先说明一点,如果有什么不对的地方,欢迎大家批评指正. 先来看这个类型的某个题目描述: 约瑟夫生者死者游戏 约瑟夫游戏的大意:30个游客同乘一条船,因为严重超载, 加上风浪大作,危险万分.因此船长告诉乘 ...

  9. 深度剖析Josephus ring(约瑟夫环)C语言版

    深度剖析Josephus ring(约瑟夫环)C语言版 鉴于C语言更适合展示算法的底层设计,并且便于读者的研究与思考,故而小编使用C语言来展示约瑟夫环的精巧与奥妙. Hello!!各位同学们,欢迎来到 ...

最新文章

  1. 椭圆形中间一个大写的v_年轻人想要的出街姿态,新CR-V整的明明白白
  2. java returnAddres_JavaClient 查询ES-(重要)
  3. 小青蛙oracle跟踪,Oracle 存储过程:游标
  4. pickle模块的基本使用
  5. JSP的9个内置对象-application
  6. Python+django网页设计入门(3):使用SQLite数据库
  7. iOS开发-UITableView常用方法
  8. Spring Web MVC 的工作流程
  9. 删除误添加的本地github检查库文件
  10. CCNA中文版完整教程
  11. hd获取硬盘序列号_获取硬盘序列号
  12. Android smali语法
  13. 学习大数据需要哪些数学知识?
  14. 【Unity3D自学记录】Unity3D之KeyCode键值
  15. bean named 'transactionManager' available: No matching PlatformTransactionManager bean found for qua
  16. 网络安全技术概论知识点
  17. ubuntu 改屏幕分辨率命令_ubuntu 修改分辨率为自定义分辨率
  18. Linux I/O编程 实验内容
  19. HTML图像和超链接及文字颜色与排版
  20. 删除用户 userdel

热门文章

  1. 【工具分享】如何识别手机里偷数据的那些软件(适用于安卓平台)
  2. 每日一句_《鹧鸪天·西都作》
  3. 程序设计C实验二题目一计算员工周工资
  4. 【NOI2015 Day1】软件包管理器
  5. 基于观测器的T-S模糊系统故障分析simulink仿真
  6. 苹果x为什么总黑屏_苹果X突然黑屏重启怎么回事?教你强制重启方法
  7. ssis 转换中文字符乱码_SSIS软件包中的字符映射转换
  8. 2023-02-11:给你两个整数 m 和 n 。构造一个 m x n 的网格,其中每个单元格最开始是白色, 请你用 红、绿、蓝 三种颜色为每个单元格涂色。所有单元格都需要被涂色, 涂色方案需要满足:
  9. dva自定义组件及使用方法
  10. iOS 应用信息、手机设备信息、网络信息、权限信息、GPS、网络信息、存储信息、屏幕信息、传感器信息、手机卡信息等信息获取工具