目录
标题一、 需求分析及概要设计
根据课程设计题目的要求,充分地分析和理解问题,叙述系统的功能要求,明确问题要求做什么?以及限制条件是什么?
1. 1 问题描述
约瑟夫环问题是一个很经典的问题:一个圈共有N个人(N为不确定的数字),第一个人的编号为0或者1(两个都可以,看你的程序如何编写),假设这边我将第一个人的编号设置为1号,那么第二个人的编号就为2号,第三个人的编号就为3号,第N个人的编号就为N号,现在提供一个数字M,第一个人开始从1报数,第二个人报的数就是2,依次类推,报到M这个数字的人出局,紧接着从出局的这个人的下一个人重新开始从1报数,和上面过程类似,报到M的人出局,直到N个人全部出局,请问,这个出局的顺序是什么?
1. 2 基本要求
(1) 输入的形式和输入值的范围:
- 用户首先需要输入一个整数n(n > 0),表示链表中的人数,即参与游戏的人数。
- 接下来,对于链表中的每个节点(代表一个人),用户需要依次输入每个人的密码,假设密码也为整数且无特定范围限制。
- 最后,用户需要输入一个整数m(m > 0),作为游戏中每轮报数的上限值。
(2) 输出的形式:
- 程序将按照约瑟夫环问题的规则输出淘汰(或称为“出列”)人的顺序,以数字形式显示,各个数字之间用空格分隔,最后一个幸存者的数字后面跟一个换行符。
(3)程序所能达到的功能:
- 程序能够模拟实现约瑟夫环问题的过程:从链表头开始,每个人按顺序报数,报到m的人会被淘汰,然后从下一个人继续开始报数,直到链表中只剩余最后一个人为止。
(4)数据结构:
- 使用了单链表结构,定义了一个结构体Node,其中包含三个成员变量:password(表示报数上限值,这里也用作临时存储被淘汰者密码)、num(表示人的编号)和next(指向下一个节点的指针)。
(5)程序模块:
- 主要包括两个模块:
a. Josephus()函数:负责链表的建立、报数逻辑的实现及输出淘汰序列。
b. main()函数:调用Josephus()函数执行整个约瑟夫环问题的模拟过程。
(6)各模块之间的调用关系以及算法设计:
- main()函数调用Josephus()函数进行核心逻辑处理。
- 在Josephus()函数中,首先构建一个链表来表示所有参与者,然后通过循环遍历链表进行报数操作,每次报数到m的人被移除(释放其内存空间),并更新报数上限为被移除节点的密码值,直至链表只剩下一个节点,即找到最后的幸存者。
标题二 、详细设计
实现概要设计中定义的所有数据类型,对主程序和其他模块尽可能写出伪码算法(伪码算法达到的详细程度建议为:按照伪码算法可以在计算机上直接输入高级语言程序);写出函数和过程的调用关系;画出程序流程图。运用掌握C/C++语言编写程序,实现程序的各个模块功能。
2. 1 所有数据类型
```plaintext
struct Node { // 定义单链表结点
int password; // 结点存储的密码值,在本问题中用于更新报数上限
int num; // 结点对应的人员编号
struct Node* next; // 指向下一个结点的指针
};
typedef struct Node Node; // 类型别名,简化Node类型的使用
typedef Node* Linklist; // 定义Linklist为指向Node的指针类型,即链表头指针类型
2. 2 主程序和其他模块的伪码算法
- **主程序main():**
```plaintext
main():
初始化链表头结点L
调用Josephus()函数处理约瑟夫环问题
返回0
- **Josephus()函数:**
```plaintext
Josephus():
初始化链表头指针L,尾指针r指向L
输入人数n
创建n个结点并将它们链接成一个单链表,同时获取每个结点对应的密码值
输入报数上限值m
当链表中人数不为1时:
初始化计数器j为1,设置辅助指针q指向链表头,p指向头结点之后
循环直到报数到m:
q移动到p的位置,p移动到下一个结点
计数器j++
输出当前报到m的人的编号
更新报数上限m为当前报到m的人的密码值
链表人数减一
移除报到m的人结点(调整链表连接)
输出最后幸存的人编号
2. 3 函数调用关系
- `main()` 调用 `Josephus()`
2. 4 程序流程图
图1 约瑟夫环问题程序流程图
主要步骤:
1. **初始化链表与输入人数n**
- 创建头节点L,如果内存申请失败则输出错误信息并返回。
- 用户输入人数n,检查是否合法(大于0),否则输出错误信息并释放已分配的链表空间后返回。
2. **构建循环链表并输入每个人的信息**
- 使用for循环创建n个节点,每个节点包括密码和编号,并将节点链接成一个循环链表。
- 对于每个新创建的节点,让用户输入其密码,并检查输入合法性,否则释放所有节点内存后返回。
3. **输入报数上限m**
- 用户输入第一个报数上限值m,检查是否合法(大于0),否则释放所有节点内存后返回。
4. **执行约瑟夫环算法**
- 初始化两个指针q和p,其中p指向当前报数的人,q指向p的前一个节点。
- 当链表中人数n不为1时,进行以下操作:
- 从p开始计数,当计数到m时,输出该位置人的编号。
- 更新m为该人的密码。
- 删除报数到m的人(修改q指向p的下一个节点)。
- n减1,p移动到下一个节点,r记录已被删除节点的位置以便后续释放内存。
- 释放已被删除节点的内存。
5. **结束处理**
- 当只剩余一个节点时,输出其编号。
- 释放最后一个节点的内存。
- 调用`freeLinkedList()`函数释放整个链表的所有节点内存。
这段C语言代码已经实现了上述详细设计,可以直接编译运行解决约瑟夫环问题。
标题三 、程序设计规范性要求
1. 使用结构化程序设计语言C语言完成题目:本代码完全采用C语言编写,符合结构化程序设计的要求,使用了循环、条件判断等基本控制结构,并结合自定义的数据结构(结构体Node)来解决问题。
2. 规范化数据输入:代码在运行时首先提示用户输入“数据n的值”(表示参与游戏的人数)和“第一个报数上限值m”,并进行了有效的错误检查,确保输入的数据是正整数且满足题目要求,保证了输入数据的规范化。
3. 模块化程序设计:程序中包含了若干个功能明确的函数,如`Josephus()`函数负责整个问题的逻辑实现,而`freeLinkedList()`函数专门用于释放链表节点内存,体现了模块化设计思想。
(1)Josephus()`:这是主要功能函数,负责实现约瑟夫环问题的核心逻辑。它包含了以下几个子功能模块:
- 初始化链表:创建一个循环链表,用于存储约瑟夫环中的每个成员及其对应的信息(密码和编号)。
- 输入数据:接收用户输入的成员数量 `n` 和每个成员的密码,并将其封装为链表节点。
- 设置报数上限:接收用户输入的报数上限值 `m`。
- 计算并输出淘汰顺序:根据约瑟夫环的规则,遍历链表并对每个节点进行报数,达到上限的节点会被淘汰(从链表中移除并释放内存),并更新下一个淘汰者需要报数的上限值,直到只剩下最后一个成员。
```c
void Josephus() {
// ... 初始化链表、输入数据 ...
// ... 获取报数上限 ...
// 计算淘汰顺序
while (n != 1) {
// ... 报数逻辑 ...
// ... 淘汰节点并释放内存 ...
}
// 输出最后幸存者
printf("%d\n", p->num);
// 释放最后一个剩余节点的内存
free(p);
freeLinkedList(L);
}
```
若要进一步提升模块化程度,还可以将部分功能独立为更多的函数,例如:
- `createCircularList(int n)`:仅负责创建含有 `n` 个节点的循环链表,返回链表头节点。
- `inputMemberInfo(Linklist L, int n)`:负责接收用户输入每个成员的密码信息,并填充到链表中。
- `getEliminationOrder(Linklist L, int m)`:负责根据报数规则计算淘汰顺序,并直接返回淘汰顺序数组,而不是在原链表上进行操作,这样可以避免在计算过程中修改链表结构。结束后再统一清理链表内存。
(2) freeLinkedList(Linklist L)`:这是一个辅助函数,用于释放链表中所有节点占用的内存。在程序结束或者发生错误需要提前退出时,调用此函数可以防止内存泄漏。该函数通过迭代链表,将每个节点逐个释放,最后释放头节点。
void freeLinkedList(Linklist L) {
Node *current = L->next, *tmp;
while (current != L) {
tmp = current;
current = current->next;
free(tmp);
}
free(L);
}
4. 上机通过并获得预期执行结果:根据代码逻辑,当用户正确输入参数后,程序会按照约瑟夫环问题的规则正确计算出淘汰顺序,并在控制台上输出结果,满足题目要求。
5. 程序书写风格与注释:虽然原始代码没有提供完整的锯齿型书写格式,但在实际开发过程中,应遵循锯齿型(或称蛇形)的代码缩进格式,以便提高代码可读性。此外,代码中已经包含了必要的注释,说明了函数的作用以及关键步骤的意义,进一步增强了代码的可读性和维护性。
为了更好地符合规范要求,可以在现有代码的基础上加强注释,改进代码排版,使其更符合锯齿型书写格式标准。例如,对重要的变量初始化、逻辑判断和循环结构增加注释说明,使代码结构更为清晰。
标题四 、测试与分析
// 引入标准输入输出库
#include <stdio.h>
// 引入动态内存管理库
#include <stdlib.h>
// 定义链表节点结构体,包含密码字段、序号字段和指向下一个节点的指针
typedef struct Node {
int password;
int num;
// 指向链表中下一个节点的指针
struct Node* next;
} Node, * Linklist;
// 定义一个辅助函数,用于释放整个链表的内存
void freeLinkedList(Linklist L) {
// 初始化两个临时指针,current指向当前节点,tmp用于暂存将要释放的节点
Node* current = L->next, * tmp;
// 遍历链表直到回到头节点
while (current != L) {
tmp = current;
current = current->next;
// 释放不再需要的节点
free(tmp);
}
// 释放头节点
free(L);
}
// 定义约瑟夫环问题解决函数
void Josephus() {
// 初始化链表为空
Linklist L = NULL;
// 定义三个工作指针:p用于创建新节点,r用于记录尾节点,q用于遍历链表
Node* p, * r, * q;
int m, n, C, j;
// 创建头节点并初始化
L = (Linklist)malloc(sizeof(Node));
if (L == NULL) {
printf("链表申请不到空间!\n");
return;
}
L->next = L;
r = L;
// 输入总人数n
printf("请输入数据n的值(n > 0): ");
if (scanf_s("%d", &n) != 1 || n <= 0) {
printf("无效的输入!请确保n大于0。\n");
freeLinkedList(L);
return;
}
// 循环创建n个节点,并输入每个人对应的密码
for (j = 1; j <= n; j++) {
p = (Node*)malloc(sizeof(Node));
if (p == NULL) {
printf("无法为第%d个人分配内存!\n", j);
freeLinkedList(L);
return;
}
printf("请输入第%d个人的密码: ", j);
if (scanf_s("%d", &C) != 1) {
printf("无效的密码输入!\n");
freeLinkedList(L);
return;
}
p->password = C;
p->num = j;
// 将新节点链接到链表尾部
r->next = p;
r = p;
}
// 设置尾节点指向头节点,形成环形链表
r->next = L->next;
// 输入报数上限值m
printf("请输入第一个报数上限值m(m > 0): ");
if (scanf_s("%d", &m) != 1 || m <= 0) {
printf("无效的m值输入!请确保m大于0。\n");
freeLinkedList(L);
return;
}
// 输出淘汰顺序
printf("*****************************************\n");
printf("出列的顺序为:\n");
// 初始化q指向头节点,p指向头节点后一位
q = L;
p = L->next;
// 当人数不为1时继续执行循环
while (n != 1) {
j = 1;
// 计算报数,当达到上限或回到头节点时停止
while (j < m && p != L)
{
q = p;
p = p->next;
j++;
}
// 输出被淘汰者序号
printf("%d ", p->num);
// 更新报数上限为被淘汰者的密码
m = p->password;
// 减少存活人数
n--;
// 删除已淘汰节点
q->next = p->next;
r = p;
p = p->next;
free(r);
}
// 输出最后幸存者的序号
printf("%d\n", p->num);
// 释放最后一个节点的内存
free(p);
// 释放链表头结点及其占用的空间
freeLinkedList(L);
}
// 主函数入口
int main() {
// 调用Josephus函数解决问题
Josephus();
return 0;
}
### 测试用例1:基础案例
- 输入:n=5, 人员密码分别为[1, 2, 3, 4, 5],m=3
- 输出:3, 1, 2, 5, 4
### 测试用例2:单人情况
- 输入:n=1, 人员密码为[1],m=任意值(如3)
- 输出:无淘汰顺序,直接输出1
### 测试用例3:多人且m较大的情况
- 输入:n=10, 人员密码分别为[3, 5, 2, 7, 1, 8, 9, 6, 4, 10],m=5
- 输出:5,6,4,3,8,9,7,2,10,1
### 测试用例4:边界条件 - n=0
- 输入:n=0, 任意m
- 预期输出:无效的输入!请确保n大于0。
### 测试用例5:负数输入-
- 输入:n=5, 人员密码任意,m=-1
- 预期输出:无效的m值输入!请确保m大于0。
### 测试用例6:负数输入
- 输入:n=-1
- 输出:无效的输入!请确保n大于0。
### 测试用例7:相同密码的情况
- 输入:n=7, 人员密码均为[3],m=3
-输出:在这种情况下,无论密码是否相同,淘汰顺序只由m决定,预期输出是一个7个人淘汰后的顺序,每人密码均为3不影响淘汰顺序。
设计测试用例,输出测试的结果。这里的测试数据应该完整和严格,并对结果进行分析。
标题五 、总结
总结可以包括:课程设计过程的收获、遇到问题、遇到问题解决问题过程的思考、程序调试能力的思考、对数据结构这门课程的思考、在课程设计过程中对《数据结构》课程的认识等内容。
在本次基于约瑟夫环问题的课程设计中,我深入理解了如何运用数据结构——链表,来模拟和解决实际问题。设计之初,我首先明确了需求,即实现一个能够动态展示约瑟夫环问题淘汰过程的程序,并确保其能处理不同规模的问题实例。
在设计阶段,通过定义链表节点结构体,我实践了抽象数据类型的设计,意识到良好的数据结构选择对于优化算法性能和简化编程实现的重要性。同时,在创建和销毁链表的过程中,我对内存管理有了更深刻的理解,尤其是在动态内存分配和释放方面的注意事项。
在编码实现过程中,遇到了诸如如何处理边界条件、如何遍历链表等具体问题。比如,如何在链表中准确地移除特定节点而不破坏链表结构,这就涉及到了链表的插入、删除等核心操作。解决这些问题时,我不断调试和完善代码,提升了程序调试能力和对逻辑错误的敏锐度。
此外,这次课程设计也加深了我对《数据结构》课程的认识。理论知识如链表、栈、队列等在实际问题中的应用使我体会到理论与实践相结合的价值。通过实际编写和调试代码,我明白了为何合理高效的数据结构是计算机科学领域解决问题的基础工具,它不仅影响程序效率,而且在复杂问题的建模上有关键作用。
总之,此次课程设计强化了我的编程技能,锻炼了我从问题抽象到数据结构选择再到算法实现的能力,同时也增强了我对数据结构课程重要性的认识,让我更加珍视理论学习与实践操作相结合的学习方式。