约瑟夫环是算法的一个经典问题,题目大意是N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。在算法中可以用一个循环链表来模拟人围成的圈,链表的每一个节点的数据域分别存放这个节点的 id 以及密码(即每个人报的数),链表首先按照系统的初始密码进行遍历循环,找到第一个节点拿到其密码然后删除之,在通过拿到的密码进行下一次遍历循环,直到链表中只剩下最后一个节点。
了解过递归的都知道,一个程序要想实现递归需要有两个条件,一是原问题要能转化成若干个子问题,并且子问题须于原问题求解方式一致; 二是递归必须有一个出口,即程序递归调用到最后必须有一个结束的终点来结束它。 仔细分析题目可以知道,算法的主要步骤为:遍历节点,找到密码对应的目标节点,删除目标节点;然后继续遍历,删除目标节点…直到只剩下最后一个节点。 所以通过分析可知,此问题符合递归的两个基本条件。故可以用递归的方法解决。
代码如下:
#include <stdio.h>
#include <malloc.h>
typedef struct LinkList {
int id;
int password;
struct LinkList *next;
}LinkList; //定义链表节点
LinkList *create(int n)
{
struct LinkList *head, *p, *tail;
int i,d = 1;
head = (struct LinkList * )malloc(sizeof(struct LinkList));
p = tail = (struct LinkList * )malloc(sizeof(struct LinkList));
head ->next = p;
for(i = 0; i < n; i++)
{
p -> id = d ++;
scanf("%d",&p -> password);
if(i == 0)
{
head->next = p;
}
else
{
tail->next = p;
}
tail = p;
p = (struct LinkList * )malloc(sizeof(struct LinkList));
}
tail ->next = head->next;
free(p);
return(tail); //尾插法建立循环链表,注意链表返回的是尾节点
}
find(struct LinkList *tail,int n, int firstpassword)
{
struct LinkList *pre, *p;
int newpassword;
pre = tail;
p = tail->next;
while(-- firstpassword){
pre = p;
p = p->next;
}
pre->next = p->next;
newpassword = p->password;
printf("%d ",p->id);
p = p->next;
if (n == 1) {
return;
}
find(pre,--n,newpassword);
}
main() {
int n,first;
struct LinkList *tail;
printf("请输入节点数:");
scanf("%d",&n);
printf("请输入初始密码:");
scanf("%d",&first);
printf("请顺序输入节点密码:");
tail = create(n);
find(tail,n,first);
}
主要就程序的find()函数部分做一下简要说明:
- 定义函数的参数为初始链表的尾指针*tail,链表的节点数n,初始密码firstpassword。
- 定义指针*pre,*p,做节点的遍历与删除用,定义newpassword用以存放每一次的新密码。
- 首先让指针pre指向链表尾节点,p指向尾节点的下一个节点,即p指向了循环链表的额头节点。
- 用while语句进行循环,password=0时循环结束(注意要先对password进行自减,具体原因可以自己在纸上画一下),循环时p进行链表的遍历,而pre始终指向p。
- 循环结束后,p指向的节点即为需要删除的目标节点,拿到密码输出目标节点的 id 随后正常进行删除即可,然后p指向目标节点的下一个节点,p继续指向目标节点。此处进行一个判断,如果链表的节点 n 数为一的话,说明已经到递归的终点了,就结束程序。
- 如若没到终点,便进行下一次递归,即函数调用自身,不过新调用是指针为pre,而且节点数自减,密码为新密码。
随机的一组测试结果如下: