1.2 约瑟夫环
一 需求分析
- 约瑟夫环的一种描述是:编号为1,2,…,n的n个人按顺时针方向围坐一圈,每个人持有一个密码(正整数)。一开始任选一个正整数作为报数上限值m,从第一个人开始按照顺时针方向自1开始顺序报数,报到m时停止报数。报m的人出列,将他的密码作为新的m值,从他在顺时针方向上的下一个人开始重新从1报数,如此下去,直至所有人全部出列为止。试设计一个程序求出出列顺序。
- 基本要求:利用单向循环链表存储结构模拟此过程,按照出列的顺序印出各人的编号。
- 输入格式:
第一行,输入m,n(以空格分隔),表示初始上限值和人数。
第二行,输入n个数字(以空格分隔),表示每个人持有的密码。
- 输出格式:
一行,按照出列顺序输出各人的编号。
二 概要设计
为实现上述程序功能,需要设计一个抽象数据类型:单向循环链表。
ADT CLinkList {
数据对象:D={a_i|a_i∈ElemSet,i=1,2,...,n,n>0}
数据关系:R={<a_i,a_{i+1}>|a_i,a_{i+1}∈D,i=1,2,...(n-1)}
基本操作:
InitCLink(&L)
操作结果:构造一个空的单向循环链表。
DestroyCLink(&L)
操作结果:销毁单向循环链表。
ListLength(L)
操作结果:返回链表长度。
ListInsert(&L,e)
初始条件:L已经存在,且0<i<L.length,e∈ElemSet。
操作结果:在L的尾端位置插入元素e,L的长度加1。
JosephsDelete(&L,m,out_queue) //重难点
初始条件:L已经存在,m>0。
操作结果:按照约瑟夫环规则依次删除链表中的元素,
将删除元素id装入out_queue,最后链表变为空链表。
} ADT CLinkList
于此同时,可以设计一个“密码人”的结构体,成员变量包括该成员的编号和密码。
三 详细设计
- 结点类型和指针类型
// 密码人结构体
typedef struct PasswordMan {
int id;
int password;
} PM;
// 单向循环链表节点
typedef struct CNode {
PM pm;
CNode* next;
} CNode, * CLinkList;
- 实现基本操作
// 构造空的循环链表
CLinkList InitList(CLinkList& L) {
L = nullptr;
return L;
}
//销毁循环链表
void DestroyList(CLinkList& L)
{
//链表未分配内存
if (L == nullptr)
{
return;
}
CLinkList current = L->next;
CLinkList nextNode = current;
while (current != L)
{
nextNode = current->next;
delete current;
current = nextNode;
}
//释放资源,将链表头指针置为nullptr
delete L;
L = nullptr;
}
// 链表长度
int ListLength(CLinkList L) {
if (L == nullptr) {
return 0;
}
int length = 0;
CLinkList current = L;
do {
length++;
current = current->next;
} while (current != L);
return length;
}
// 插入元素
void ListInsert(CLinkList& L, PM e) {
CLinkList newNode = new CNode;
if (newNode == nullptr) {
cout << "内存分配失败" << endl;
return;
}
newNode->pm = e;
if (L == nullptr) {
// 链表为空,新节点既是头节点也是尾节点
newNode->next = newNode;
L = newNode;
}
else {
// 链表非空,插入到尾部
CLinkList current = L;
while (current->next != L) {
current = current->next;
}
current->next = newNode;
newNode->next = L; // 形成循环链表
}
}
- 约瑟夫环删除
描述说明:根据需求分析,我们需要对函数传入参数初始步长initialStep,考虑到约瑟夫环的删除具有彻底性(将链表所有元素删除),我们需要对链表最后一个结点进行特殊处理,删除L后将其置为nullptr。为方便输出,我们传入参数deletedIds动态数组用于保存按照顺序删除的元素id。同时,至关重要的一点在于每次删除后需要将报数上限设置为删除元素的密码值,然后从删除元素后一结点开始重新计数。
// 按照约瑟夫环规则删除元素
void JosephusDelete(CLinkList& L, int initialStep,
vector<int>& deletedIds)
{
if (L == nullptr || initialStep <= 0) {
cout << "链表为空或步长无效" << endl;
return;
}
CLinkList current = L;
CLinkList prev = nullptr;
int step = initialStep; // 初始步长
while (ListLength(L) > 1) {
// 找到需要删除的节点
for (int i = 1; i < step; i++) {
prev = current;
current = current->next;
}
// 记录删除的节点的ID
deletedIds.push_back(current->pm.id);
// 删除节点
prev->next = current->next;
if (current == L) {
L = current->next; // 如果删除的是头节点,更新头节点
}
// 更新步长为被删除节点的密码值
step = current->pm.password;
delete current;
current = prev->next; // 更新当前节点
}
// 记录最后剩下的唯一节点的ID
if (L != nullptr) {
deletedIds.push_back(L->pm.id);
delete L;
L = nullptr; // 现在链表为空
}
}
四 调试分析
内容包括:
- 调试过程中遇到的问题以及解决方法
- 算法的时空分析
- 经验体会
- 在编写单向循环链表结构开始时,构造了首元结点,即不保存实际信息的头结点,这样的结构导致在设计约瑟夫环删除模块式异常困难,在测试时出现错误答案。
- 在更新当前结点时,刚开始考虑的是更新头结点的更新,导致程序结构混乱。
- 在设计约瑟夫环删除模块时未考虑最后剩下结点的删除与回收,导致程序出现错误。
- 算法的时空分析:
- 时间复杂度:在
JosephusDelete
函数中,ListLength(L)
被调用来计算链表的长度。每次调用ListLength
的时间复杂度是 O(n),其中 n 是链表的节点数。由于ListLength
在每次删除操作中被调用,链表长度从 n 逐渐减少到 1,因此总体上调用ListLength
的次数是 n 次。 所以,计算链表长度的时间复杂度是 O( n 2 n^2 n2)(在最坏情况下)。 删除节点中 ,for
循环遍历链表来找到要删除的节点。遍历操作的时间复杂度是 O(n),其中 n 是当前链表中的节点数。删除节点的时间复杂度是 O(1),但遍历的时间复杂度主导了整体复杂度。 - 空间复杂度:
JosephusDelete
函数的空间复杂度是 O(n),主要是链表和用于存储删除节点id
的vector
。
五 用户使用说明
- 本程序的运行环境为Windows11操作系统,执行文件由Visual Studio2022本地Windows调试器打开。
- 按照需求分析所说明的输入格式进行输入,结束符为“回车符”。
- 接受不同命令将返回不同结果。
20 7
3 1 7 2 4 8 4
输出结果: 6 1 4 7 2 3 5
六 测试结果
- 执行命令1:第一行输入m的初始值20,n=7,第二行输入7个人的密码,依次为:3 1 7 2 4 8 4;最终输出结果为:6 1 4 7 2 3 5。
int main()
{
int m, n;
cin >> m >> n; //初始报数上限值和人数
CLinkList L = InitList(L);
for (int i = 1; i <= n; i++)
{
PM e = { 0,0 };
e.id = i;
cin >> e.password;
ListInsert(L, e);
}
//输出队列
vector<int> out_queue;
JosephusDelete(L, m, out_queue);
cout << "输出结果:" << " ";
for (int i = 0; i < out_queue.size(); i++)
{
cout << out_queue[i] << " ";
}
return 0;
}
七 附录
带注释的源程序。