1.开言
约瑟夫环问题(Josephus Problem)是一个既富有挑战性又非常有趣的题目。它不仅考察了算法的设计思维,还展示了数据结构的巧妙应用。
小白我在刷牛客竞赛语法入门班数组字符串习题遇到了遇到了这个经典问题。虽然最初被其复杂的逻辑困住,但经过深入学习并参考了一些大佬的详细题解后,我终于掌握了一些,像数组,循环链表还是有着些许的理解,不过像递归和动态规划还暂时未学会,因此本文并非对这一问题的全面解读,而是分享多种实现这一算法的代码,希望能够帮助初学者理解约瑟夫环问题,也为那些已经理解算法思路但忘记具体实现的大佬们提供一些快速参考。
题目链接 https://ac.nowcoder.com/acm/contest/19306/1003
下面,让我们一起来探讨这个有趣的问题,并看看这些不同的解决方案。
2.C&C++数组解决方法
1.静态数组
#include <iostream>
using namespace std;
const int N = 100;
int josephus(int n,int m)
{
int cirarr[N];
int index = 0;// 从一个人开始, 若第k个人开始 k-1;
int count = 0; // 计数器;
for(int i=0;i<n;i++) // 初始化约瑟夫环
{
cirarr[i] = i+1;
}
while(n>1)// 模拟淘汰
{
count++;
if(count == m)
{
for(int j=index;j<n-1;j++) //相当于顺序表删除元素操作;
{
cirarr[j] = cirarr[j+1];
}
count = 0;// 重置计算器
n-- ;// 人数减一
index--; //修正index以指向正确的下一个元素
}
index++; // 移向下一个人
// 当到达约瑟夫环的末尾时,回到开始位置
if (index == n)
{
index = 0;
}
}
return cirarr[0];// 最后未被淘汰的人编号;
}
int main()
{
int n,m;
int num;
cout<<"请输入约瑟夫环的人数n : ";
cin>>n;
cout<<"请输入每次数的数字m : ";
cin>>m;
num = josephus(n,m);
cout<<"最后未被淘汰的编号是 "<<num<<endl;
return 0;
}
2.动态数组
1.C语言使用malloc来动态分配内存
#include <stdio.h>
#include <stdlib.h>
int josephus(int n, int k) {
int* people = (int*)malloc(n * sizeof(int));
int i, count = 0, position = 0, remaining = n;//剩余人数
// 初始化约瑟夫环
for (i = 0; i < n; i++) {
people[i] = 1; // 1表示人还在圈中
}
while (remaining > 1) {
if (people[position] == 1) {
count++;
if (count == k) {
people[position] = 0; // 0表示此人被淘汰
remaining--;
count = 0;
printf("第 %d 个人被淘汰\n", position + 1);
}
}
position = (position + 1) % n;
}
// 找到最后幸存者
for (i = 0; i < n; i++) {
if (people[i] == 1) {
free(people);
return i + 1; // 返回幸存者的编号(从1开始)
}
}
free(people);
return -1; // 错误情况
}
int main() {
int n, k;
printf("请输入总人数 n: ");
scanf("%d", &n);
printf("请输入报数值 k: ");
scanf("%d", &k);
int survivor = josephus(n, k);
printf("最后的幸存者编号是: %d\n", survivor);
return 0;
}
2.C++使用new创建动态数组
#include <iostream>
using namespace std;
int findKing(int n, int k, int m) {//报数报到m的人出队(报数是1,2,...m这样报的)
int* people = new int[n];
for (int i = 0; i < n; i++) {// 初始化
people[i] = 1; // 1 表示人在圈中,0 表示人已出局
}
int remaining = n; // 剩余人数
int index = k - 1; // 当前报数的人的位置
int step = 0; // 当前报的数
while (remaining > 1) {
if (people[index] == 1) {
step++;
if (step == m) {
people[index] = 0; // 将这个人标记为出局
step = 0; // 重置报数
remaining--; // 剩余人数减少
}
}
index = (index + 1) % n; // 移动到下一个位置
}
// 找到最后剩下的人
for (int i = 0; i < n; i++) {
if (people[i] == 1) {
delete[] people; // 释放内存
return i; // 返回大王的编号
}
}
delete[] people; // 释放内存
return -1; // 这种情况不应该发生
}
int main() {
int n, k, m;
cout << "请输入总人数 n: ";
cin >> n;
cout << "请输入开始报数的人的编号 k: ";
cin >> k;
cout << "请输入报数上限 m: ";
cin >> m;
int king = findKing(n, k, m);
cout << "大王的编号是: " << king << endl;
return 0;
}
3.C++动态数组vector
1.仅将C语言静态数组改为使用C++vector
- 使用vector容器
arr
来表示约瑟夫环。 - 初始化vector,使每个元素的值等于其索引加1。
- 使用
index
变量跟踪当前位置,count
变量计数。
使用vector而不是固定大小的数组,可以处理任意大小的n。并且使用了C++的标准库功能,如vector的 erase
方法。但对于非常大的 n,频繁使用 erase
可能会影响性能,因为它需要移动后面的所有元素。
#include <iostream>
#include <vector>
using namespace std;
int findKing(int n, int k, int m)
{
vector<int> arr(n);
for(int i=0;i<n;i++)
{
arr[i] = i+1;
}
int index = k-1;
int count = 0;
while(arr.size()>1)
{
count++;
if(count == m)
{
arr.erase(arr.begin()+index);//使用erase代替for循环
count = 0;
index--; //修正index以指向正确的下一个元素
}
index = (index + 1) % arr.size();// 移动到下一个位置
}
return arr[0];
}
int main() {
int n, k, m;
cin >> n;
cin >> k;
cin >> m;
int king = findKing(n, k, m);
cout << king << endl;
return 0;
}
2.新版本
- 新版本直接在一步中计算出要删除的人的位置:
index = (index + m - 1) % people.size();
这减少了循环中的操作次数,提高了效率。 - 新版本不需要index这个调整,因为删除后下一个人自动占据了这个位置。
#include <iostream>
#include <vector>
using namespace std;
int findKing(int n, int k, int m) {
vector<int> people(n);
for (int i = 0; i < n; i++) {
people[i] = i+1;
}
int index = k - 1; // 因为数组索引从0开始,所以k需要减1
while (people.size() > 1) {
index = (index + m - 1) % people.size(); // 找到要删除的人的位置
people.erase(people.begin() + index); // 将这个人移出队伍
// 不需要修改index,因为下一个人现在在这个位置上
}
return people[0]; // 返回最后剩下的人的编号
}
int main() {
int n, k, m;
cout << "请输入总人数 n: ";
cin >> n;
cout << "请输入开始报数的人的编号 k: ";
cin >> k;
cout << "请输入报数上限 m: ";
cin >> m;
int king = findKing(n, k, m);
cout << "大王的编号是: " << king << endl;
return 0;
}
3.循环单链表
1.C版本
- 使用结构体和函数,采用过程式编程方法。
- 使用
struct Node
定义链表节点,直接操作节点指针。 - 使用
malloc()
分配内存,使用free()
释放内存,需要手动管理内存。 - 使用
create()
函数一次性创建整个链表,在创建过程中直接构建循环链表。 josephus()
函数接受头节点和 k 值作为参数,使用count
变量累加,达到 k 时重置为 0。- 所有功能都在全局作用域中实现,通过直接修改指针直接操作链表,可能在大规模数据时略有性能优势。
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构
typedef struct Node
{
int data; // 节点数据
struct Node* next; // 指向下一个节点的指针
} Node;
// 创建约瑟夫环
Node *create(int n)
{
// 创建头节点
Node* head = (Node*)malloc(sizeof(Node));
head->data = 1;
head->next = head; // 循环链表,头节点指向自己
Node *prev = head;
// 创建其余的n-1个节点
for(int i = 2; i <= n; i++)
{
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = i;
prev->next = newNode; // 将新节点连接到链表
prev = newNode; // 更新prev指针
}
prev->next = head; // 最后一个节点指向头节点,形成循环
return head;
}
// 执行约瑟夫问题的淘汰过程
int josephus(Node* head, int k)
{
Node* current = head;
Node* prev = NULL;
int count = 0;
// 当链表中还有多于一个节点时继续循环
while(current->next != current)
{
count++;
if(count == k) // 如果数到k,执行淘汰
{
prev->next = current->next; // 从链表中移除当前节点
free(current); // 释放被淘汰节点的内存
current = prev->next; // 移动到下一个节点
count = 0; // 重置计数器
}
else
{
prev = current;
current = current->next; // 移动到下一个节点
}
}
int survivor = current->data; // 记录最后幸存者的编号
free(current); // 释放最后一个节点的内存
head = NULL; // 将头指针置空
return survivor;
}
int main()
{
int n, k;
int survivor;
// 获取用户输入
printf("请输入约瑟夫环的人数n:");
scanf("%d", &n);
printf("请输入每次数的数字k:");
scanf("%d", &k);
// 创建约瑟夫环并执行问题求解
Node* head = create(n);
survivor = josephus(head, k);
// 输出结果
printf("最后幸存者的编号是:%d\n", survivor);
return 0;
}
2.C++版本
- C++ 版本:使用类和对象,采用面向对象编程方法。
- 使用
class Node
定义链表节点,封装了CircularLinkedList
类来管理链表操作。 - 使用
new
运算符创建对象,使用delete
运算符销毁对象,在类的方法中封装了内存管理。 - 使用
insert()
方法逐个插入节点,在main()
函数中循环调用insert()
来构建链表。 josephus()
方法是CircularLinkedList
类的成员函数,使用 for 循环直接数到 k,通过类的内部状态(如size
)来控制循环结束。- 主要功能封装在
CircularLinkedList
类中,通过封装提高了代码的可维护性,但可能引入轻微的性能开销。
#include <iostream>
// 定义链表节点类
class Node {
public:
int data; // 节点数据
Node* next; // 指向下一个节点的指针
// 构造函数
Node(int value) : data(value), next(nullptr) {}
};
// 定义循环链表类
class CircularLinkedList {
private:
Node* head; // 头节点指针
int size; // 链表大小
public:
// 构造函数
CircularLinkedList() : head(nullptr), size(0) {}
// 插入新节点
void insert(int value) {
Node* newNode = new Node(value);
if (!head) {
head = newNode;
head->next = head; // 创建循环链表
} else {
Node* temp = head;
while (temp->next != head) {
temp = temp->next;
}
temp->next = newNode;
newNode->next = head;
}
size++;
}
// 执行约瑟夫问题
int josephus(int k) {
if (!head) return -1; // 如果链表为空,返回-1
Node* current = head;
Node* prev = nullptr;
while (size > 1) { // 当链表中还有多于一个节点时继续循环
// 移动k-1步
for (int i = 1; i < k; i++) {
prev = current;
current = current->next;
}
// 删除第k个节点
prev->next = current->next;
delete current;
size--;
current = prev->next;
}
// 保存最后幸存者的编号
int survivor = current->data;
delete current; // 删除最后一个节点
head = nullptr; // 将头指针置空
return survivor;
}
};
int main() {
int n, k;
// 获取用户输入
std::cout << "请输入人数n: ";
std::cin >> n;
std::cout << "请输入计数值k: ";
std::cin >> k;
CircularLinkedList cll; // 创建约瑟夫环
// 初始化约瑟夫环
for (int i = 1; i <= n; i++) {
cll.insert(i);
}
// 执行约瑟夫问题并获取结果
int survivor = cll.josephus(k);
// 输出结果
std::cout << "幸存者的编号是: " << survivor << std::endl;
return 0;
}
4.递归算法
#include <iostream>
// 递归函数来解决约瑟夫问题
int josephus(int n, int k) {
// 基本情况:如果只有一个人,他就是幸存者
if (n == 1) {
return 1;
}
// 递归步骤:
// 1. 解决n-1人的子问题
// 2. 调整结果以适应n人的情况
return (josephus(n - 1, k) + k - 1) % n + 1;
// 调整公式:(josephus(n - 1, k) + k - 1) % n + 1
// + k - 1:因为从上一轮幸存者的位置开始数,需要再数k-1个位置。
// % n:确保我们在圆圈内循环。
// + 1:因为我们的编号从1开始,而不是0。
}
int main() {
int n, k;
// 获取用户输入
std::cout << "请输入人数n: ";
std::cin >> n;
std::cout << "请输入计数值k: ";
std::cin >> k;
// 计算并输出结果
int survivor = josephus(n, k);
std::cout << "幸存者的编号是: " << survivor << std::endl;
return 0;
}
5.动态规划
#include <iostream>
#include <vector>
using namespace std;
int josephusDynamic(int n, int k) {
vector<int> dp(n + 1);
dp[1] = 1; // 基本情况:只有一个人时,幸存者位置为1
for (int i = 2; i <= n; i++) {
dp[i] = (dp[i - 1] + k - 1) % i + 1;//递推关系
}
return dp[n];//dp[i] 表示在有 i 个人的情况下,幸存者的位置
}
int main() {
int n, k;
cout << "请输入总人数 n: ";
cin >> n;
cout << "请输入报数间隔 k: ";
cin >> k;
int survivor = josephusDynamic(n, k);
cout << "最后幸存者的位置是: " << survivor << endl;
return 0;
}
6.本文小结
文章展示了从简单到复杂的不同编程方法,包括数组、链表、递归和动态规划等不同思路,给读者提供了全面的参考,希望能够帮助读者系统性地理解问题,并根据需要选择合适的解决方案。不仅适合初学者理解基本的数组和链表操作,也对进阶读者提供了递归和动态规划的更高级的解法。
不足之处 :
- 理论讲解略显不足:文章主要侧重于代码实现,但对每种方法背后的理论讲解较少,可能让一些读者难以深入理解不同算法之间的优劣和适用场景。
- 缺少性能分析:尽管介绍了多种解决方法,但没有对这些方法在实际应用中的性能(如时间复杂度和空间复杂度)进行深入分析。对于需要选择最优解的读者来说,这部分内容可能会有所欠缺。
- ......