7-28 猴子选大王(分数 20)

该博客介绍了一种选择猴王的算法,通过模拟猴子报数过程,每轮淘汰报数到3的猴子,直至最后剩下一只。程序使用C语言实现,输入候选猴子数量N,输出当选猴王的编号。此问题属于编程逻辑和循环控制的范畴,适合初学者理解和练习。

一群猴子要选新猴王。新猴王的选择方法是:让N只候选猴子围成一圈,从某位置起顺序编号为1~N号。从第1号开始报数,每轮从1报到3,凡报到3的猴子即退出圈子,接着又从紧邻的下一只猴子开始同样的报数。如此不断循环,最后剩下的一只猴子就选为猴王。请问是原来第几号猴子当选猴王?

输入格式:

输入在一行中给一个正整数N(≤1000)。

输出格式:

在一行中输出当选猴王的编号。

输入样例:

11

输出样例:

7

问题分析

此题与我之前写的文章比较类似,可以转过去

链接:http://t.csdn.cn/VidNn

代码实现

 

#include<stdio.h>
struct monkey{
    int number;
    int baoshu;
};
int main(){
    struct monkey houzi[1005];
    int m=3,n;
    scanf("%d",&n);
    for(int i=0;i<n;i++){
        houzi[i].number=i+1;
    }
    int i=0,b=1;
    while(n>1){
        houzi[i].baoshu=b;
        if(houzi[i].baoshu==m){
            for(int j=i;j<=n-1;j++){
                houzi[j]=houzi[j+1];
            }
            n--;
            if(i==n){
            	i=0;
			}
            b=1;
        }else{
        	i++;
        	if(i>=n){
            	i=0;
        	}
        	b++;
		}
    }
    printf("%d",houzi[0].number);
    return 0;
}

<think>根据用户描述,猴子大王问题(约瑟夫问题)可以使用循环链表解决。 规则:M只猴子,编号1~M,从第一个猴子开始报数(从1开始),数到K的猴子出圈,下一个猴子从1开始报数,直到只剩一只猴子。 我们需要: 1. 构建一个循环链表,每个节点代表一只猴子,存储编号。 2. 从第一个节点开始,模拟报数过程。 3. 每次数到K,就将当前节点移除(出圈)。 4. 重复步骤2-3,直到只剩一个节点(即链表只剩下一个节点,且该节点的next指向自己)。 5. 输出该节点的编号。 注意:由于题目要求输入为两个正整数m和k,输出为最后猴王的编号。 我们将按照以下步骤实现: 1. 定义节点结构。 2. 创建循环链表,初始化m个节点(编号1~m)。 3. 设置一个指针指向当前节点,初始为头节点(或者第一个节点)。 4. 循环直到链表中只剩一个节点: a. 从1开始报数,报数到k-1(因为报数到k的那个节点要删除,实际上我们走k-1步就到了要删除节点的前一个,但在循环链表中删除节点需要定位前驱?但这里我们使用单向循环链表也可以,但注意题目要求用循环链表,且我们这里为了方便,可以记录前驱节点,这样删除方便。但题目没有指定单向还是双向,我们使用单向循环链表即可。 然而,用户之前提到双向循环链表,但这个问题单向循环链表就可以解决。不过,为了统一,我们可以使用单向循环链表。 但是,注意:用户之前的问题要求是双向循环链表操作,但这个问题是猴子大王,与双向链表操作问题不同。因此,这里我们按照单向循环链表实现。 不过,由于用户要求解决猴子大王问题,我们就按照常规的约瑟夫环问题解决。 另一种思路:使用数学递推公式(效率更高),但用户要求使用循环链表模拟。 我们择用单向循环链表模拟。 步骤: 1. 创建节点结构体,包含编号和指向下一个节点的指针。 2. 构建循环链表:依次创建m个节点,并连接成环。 3. 设置一个指针current指向第一个节点,然后我们还需要一个指针prev,用于删除节点(因为要删除current,就需要知道前驱节点prev)。初始时,prev应该指向链表的最后一个节点(这样当current指向第一个节点时,prev就是current的前驱)。 4. 模拟报数过程: for i in range(1, k): # 注意:只需要报数k-1次,因为当前节点已经算1 prev = current current = current->next 此时,current指向的就是要删除的节点(报数到k的节点)。 操作:将prev->next指向current->next,然后删除current节点。 然后current指向下一个节点(即prev->next),继续报数。 5. 直到链表中只剩下一个节点(即current->next == current,或者prev==current),则该节点就是猴王。 注意:当链表只有一个节点时,我们停止循环。 具体实现: 我们定义一个节点结构: typedef struct Node { int num; // 猴子的编号 struct Node *next; } Node; 创建循环链表: Node *head = NULL; Node *prev = NULL; for (int i = 1; i <= m; i++) { Node *node = (Node*)malloc(sizeof(Node)); node->num = i; if (head == NULL) { head = node; prev = node; } else { prev->next = node; prev = node; } } prev->next = head; // 最后一个节点指向头,形成环 但是,我们不需要头节点,我们直接用第一个节点开始。注意我们模拟报数是从编号为1的节点开始的。 然后,我们初始化: current = head; // 当前节点从第一个开始 prev = NULL; // 但是我们如何得到第一个节点的前一个?我们知道循环链表最后一个节点是第一个节点的前驱。 所以,我们可以先让prev指向最后一个节点(即prev->next = head的那个节点,也就是上面循环结束后的prev)。 然后,当我们删除节点时,注意如果删除的是头节点,我们需要更吗?其实不需要,因为我们使用环,而且我们并不需要固定头节点。我们只在环中操作。 模拟过程: while (current->next != current) { // 当不止一个节点 // 报数: 从1到k-1 for (int i = 1; i < k; i++) { prev = current; current = current->next; } // 现在current就是要出圈的节点 Node *temp = current; prev->next = current->next; // 将prev的下一个指向current的下一个 current = current->next; // current跳到下一个节点,继续报数 free(temp); // 释放出圈节点 } // 循环结束,current就是猴王 printf("%d\n", current->num); free(current); // 最后释放 但是,这里有一个问题:当k=1时,我们每次删除第一个节点,那么需要特殊处理吗?实际上,上述代码在k=1时,内层循环不执行(i从1到0,不执行),然后删除的就是当前的current,然后current=current->next,然后继续。但这样会一直删除直到最后一个?其实这样也可以,但注意当只剩一个节点时,我们删除后,current->next还是等于自己吗?在删除一个节点后,剩下的最后一个节点满足current->next==current?不一定,因为我们在删除节点时,如果只剩一个节点,那么prev->next指向current->next(也就是自己),所以current->next还是指向自己。所以循环条件满足退出。 但是,注意:当m=1时,我们一开始就只剩一个节点,所以直接输出。 但是,上面的模拟过程在k大于1时正确,但当k=1时,内层循环不执行,我们直接删除current,然后current后移,但是此时prev指向的是当前current的前一个(其实在初始时,prev指向最后一个节点,current指向第一个节点,然后不执行循环,删除第一个节点,然后current指向第二个节点?不对,初始时,prev指向最后一个节点(即第m个节点),而current指向第一个节点(即prev->next)。删除第一个节点后,prev->next指向第一个节点的下一个(即第二个节点),current变为第二个节点。然后继续,直到只剩一个节点。 但是,当k=1时,相当于每次删除第一个节点,那么最后留下的应该是最后一个节点。所以我们的逻辑应该正确。 但是,注意:当m=0时,需要特殊处理(没有猴子),但题目要求输入正整数,不考虑0。 然而,上述代码有一个问题:当我们删除节点时,如果删除的节点是头节点(head)?其实这里我们不维护头节点,所以无所谓。我们只维护current和prev。 但是,在构建链表时,我们使用head作为第一个节点,而prev指向最后一个节点。在删除过程中,head可能被删除,所以后续不应该再使用head。 因此,我们不需要head,只需要current和prev。 修正:在构建链表后,我们让current指向第一个节点(即head),prev指向最后一个节点(即head的前驱)。然后开始循环。 另一种情况:当k很大(比如大于当前剩余节点数)时,我们需要模运算吗?实际上,我们不需要,因为循环链表,我们可以一直走,但是这样效率低。我们可以优化:实际上,我们只需要走 k mod 当前剩余个数 的步数,但为了简单,我们按照常规做法,走k-1步(因为每次都是走k-1步到要删除节点的前一个?不对,我们报数k-1次,相当于移动k-1次,然后下一个节点就是要删除的节点。 但是,移动k-1次可能会绕圈多次?如果k很大,我们可以对当前剩余人数取模减少步数。但这里我们不优化,因为题目没有说明m和k的大小关系(可能很大,但为了简单,我们先不考虑优化,因为题目要求是模拟过程)。 然而,如果m和k很大,模拟效率会很低。但题目没有明确范围,所以先按照模拟实现。 注意:题目要求输入两个正整数m和k,输出最后猴王的编号。 因此,我们编写函数: int josephus(int m, int k) 但是,如果k=0,应该是不合法的,但题目输入正整数,不考虑。 代码实现: 我们使用C语言实现。 注意:如果m=1,那么就直接返回1。 还有一种特殊情况:k=1,那就是依次删除,最后剩下的是第m个。 我们将实现上述循环链表模拟。 但是,为了效率,我们也可以使用数组或者数学方法,但题目要求用循环链表,我们就用循环链表。 现在,我们写代码。 注意:内存泄漏问题,我们在最后释放猴王节点。 但是,我们也可以在函数中动态创建链表,并返回结果,然后释放所有节点?其实在模拟过程中,我们把除了猴王以外的节点都释放了,最后释放猴王节点。 步骤: 1. 如果m<=0,返回0或者错误(但题目输入正整数,所以不考虑)。 2. 创建一个循环链表,有m个节点。 3. 初始化current为第一个节点,prev为最后一个节点。 4. 当链表不止一个节点(current->next != current),但实际上,我们可以用一个计数器,也可以在循环条件中用prev != current(因为当只有两个节点时,删除一个后,prev和current会指向同一个节点?不,当只有两个节点时,删除一个后,剩下的节点自己指向自己,所以用current->next != current 判断更合适。 5. 报数k-1次:移动prev和current。 6. 删除current指向的节点,并将current移动到下一个节点。 7. 最后剩下一个节点,返回该节点的编号,并释放该节点。 但是,注意:在循环中,当只剩两个节点时,删除一个节点后,剩下的节点满足current->next==current?不一定,因为删除后,剩下的节点自己指向自己。所以循环条件 current->next != current 是满足的(删除前有两个节点,删除后只剩一个,此时current->next==current?不,因为当我们删除一个节点后,剩下的节点应该自己指向自己吗?实际上,在删除节点时,我们会将前一个节点的next指向被删除节点的下一个节点。当剩下两个节点A和B时,假设当前current在A,prev在B(因为初始时prev在B?不,初始时prev指向最后一个节点,即B,current指向A)。然后我们删除A:将B->next指向A->next(即B),然后current变为A->next(也就是B)。此时,链表只剩下节点B,而B->next指向B。所以循环条件是判断current->next != current?此时B->next==B,所以条件不成立,循环结束。所以条件正确。 但是,我们也可以用一个计数器,记录剩余节点数,当剩余节点数为1时退出循环。 我们采用current->next != current作为循环条件。 代码: 注意:当m=0时,返回0;m=1时,直接返回1(不需要删除)。 实现: 我们将编写一个C程序,从标准输入读取m和k,输出猴王编号。 但是,用户输入的是两个正整数,所以我们可以这样: int m, k; scanf("%d%d", &m, &k); 然后调用函数计算猴王编号。 但是,题目描述说:输入为两个正整数m和k,输出为当猴王的编号。 因此,程序如下: 注意:需要处理边界情况。 我们开始写代码。 </think># 猴子大王问题(约瑟夫环问题)的循环链表解法 猴子大王问题是一个经典的约瑟夫环问题,可以用循环链表高效地模拟整个举过程。下面我将详细解释实现思路并提供完整的解决方案。 ## 问题分析 - M只猴子编号1~M围成一圈 - 从第1只猴子开始报数(从1开始) - 每数到K的猴子出圈 - 下一只猴子从1开始报数 - 重复过程直到只剩一只猴子,即为猴王 ## 循环链表解法 使用单向循环链表模拟猴子围成的圈,每个节点代表一只猴子,包含编号和指向下一只猴子的指针。 ### 核心算法步骤 1. 创建包含M个节点的循环链表 2. 初始化指针:当前节点`current`指向1号猴子,前驱节点`prev`指向M号猴子 3. 当链表不止一个节点时循环: - 报数K-1次(移动指针K-1步) - 删除当前节点(猴子出圈) -当前指针为下一个节点 4. 最后剩下的节点即为猴王 ### 数学原理 猴子大王问题可用递推公式求解: $$ f(M,K) = (f(M-1,K) + K) \mod M $$ 其中 $f(1,K)=0$。但循环链表解法更直观地模拟了实际过程。 ## C语言实现代码 ```c #include <stdio.h> #include <stdlib.h> // 定义链表节点 typedef struct Node { int num; // 猴子编号 struct Node *next; // 指向下一只猴子 } Node; int find_king(int m, int k) { // 处理边界情况 if (m <= 0 || k <= 0) return -1; if (m == 1) return 1; // 创建循环链表 Node *head = NULL; Node *prev = NULL; // 创建m个节点的循环链表 for (int i = 1; i <= m; i++) { Node *node = (Node*)malloc(sizeof(Node)); node->num = i; if (head == NULL) { head = node; } else { prev->next = node; } prev = node; } prev->next = head; // 尾节点指向头节点,形成环 Node *current = head; // 从第一只猴子开始 prev = NULL; // 查找prev指针的初始位置(指向当前节点的前一个节点) Node *temp = head; while (temp->next != head) { temp = temp->next; } prev = temp; // prev指向最后一只猴子 // 模拟举过程 while (current->next != current) { // 当不止一只猴子时 // 报数(移动k-1次) for (int i = 1; i < k; i++) { prev = current; current = current->next; } // 删除当前节点(猴子出圈) prev->next = current->next; Node *to_free = current; current = current->next; free(to_free); } // 保存猴王编号并释放最后一个节点 int king = current->num; free(current); return king; } int main() { int m, k; printf("请输入猴子总数M和报数K(空格分隔): "); scanf("%d %d", &m, &k); if (m <= 0 || k <= 0) { printf("输入必须为正整数\n"); return 1; } int king = find_king(m, k); printf("猴王是: %d\n", king); return 0; } ``` ## 测试示例 ``` 输入: 5, 2 输出: 猴王是: 3 举过程: 初始: 1->2->3->4->5->1 第1轮: 1报1, 22(出圈) → 剩余: 1,3,4,5 第2轮: 3报1,4报2(出圈) → 剩余: 1,3,5 第3轮: 5报1,1报2(出圈) → 剩余: 3,5 第4轮: 3报1,5报2(出圈) → 剩余: 3(猴王) ``` ## 算法分析 - **时间复杂度**:$O(M \times K)$ - **空间复杂度**:$O(M)$ - **优化方向**:对于大规模数据,可以使用数学递推公式将复杂度优化到$O(M)$
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值