数据结构与算法笔记DAY2——链表——约瑟夫环的四种方法

一、什么是约瑟夫环

      有n只猴子,按顺时针方向围成一圈选大王(编号从1到n),从第1号开始报数,一直数到m,数到m的猴子退出圈外,剩下的猴子再接着从1开始报数。就这样,直到圈内只剩下一只猴子时,这个猴子就是猴王,编程求输入n,m后,输出最后猴王的编号。    

     OJ链接:OpenJudge - 1748:约瑟夫问题

二、循环链表实现

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
//在每一次循环中,都先处理核心功能部分的代码
struct node {
    int data;
    struct node* next;
};

int main() {
    int n, m, i;

    int answer[100];//猴王
    int count = 0; //计数

    struct node* head, * tail, * p, * q;
    //虚拟头节点,数据域为-1
    head = (struct node*)malloc(sizeof(node));
    head->next = NULL;  //指针域赋为空,这很重要
    head->data = -1;


    while (1) {
        //通过输入样例我们看出来的:题目要求是可以多次输入n,m,因此我们要一直循环。
        scanf("%d %d", &n, &m);
        //如果n或m==0,释放head节点,然后退出循环
        if (n == 0 || m == 0) { free(head); break; }
        else {
            tail = head;
            //尾插法能保证顺序相同
            for (i = 1; i <= n; i++) {
                p = (struct node*)malloc(sizeof(node)); 
                p->data = i;  //给p赋值,是猴子的编号
                tail->next = p; //尾插
                p->next = head->next;//这里的head是一个虚拟头节点
                tail = p;//更新尾指针
            }
            //链表创建完成
            p = head->next;//更新p让他指向首元节点
            q = tail;//报数的节点是要删除的,也就是说我们还是需要一个前驱节点,也就是我们熟悉的双指针法
            i = 1;//报数的变量,从1开始
            while (p != q) {
            //这个循环是在干什么?是在寻找这一次输入的nm条件下的猴王
            //循环结束条件是什么?也就是说最后只剩下一个猴子的时候结束,那个猴子就是猴王。此时p和q重合。
                if (i == m) {
                    //找到了
                    q->next = p->next;//删除操作
                    free(p);//释放内存很重要
                    p = q->next;//p仍然应该指向q下一个节点,且从p开始重新报数
                    i = 1;//新一轮报数开始1
                }
                else {
                    //说明还没有找到
                    q = p;
                    p = p->next;
                    i++;
                }
              } //p!=q的这个循环在这里就应该结束了。

             //当最后剩两个节点时,如果删除的是首元节点(p = head -> next),此时会发现head和链表之间的链接断开了。如果需要维持这种链接,还需要对head的指针域进行赋值
             head->next = q;
             answer[count] = p->data;
             free(p);
             count++;
             //p和q都指向这个最后的猴子,保存了结果之后,就可以直接释放这个节点了
             //这个步骤很容易忘记,不能留下垃圾。
             head->next = NULL;//链表这个时候已经清空了,要给head的指针域置空,让他恢复到初始的空链表状态,便于下一次循环。
           
        }
    }//直到输入00的时候,才会退出循环。上面的if语句会释放head节点,这样才不会留下垃圾
    for (i = 0; i < count; i++) {
        printf("%d", answer[i]);
    }
    return 0;
}
遇到的问题
总是显示访问地址冲突妥妥大冤种
删除元素操作那里,应该是q -> next = p -> next写成了q -> next = p -> next -> next。当p指向最后一个结点的时候,p->next都已经是NULL,哪里来的p->next ->next

简约版没什么注释的代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
struct node {
    int data;
    struct node* next;
};
int main(void)
{
    int n, m;
    int i;
    int answer[100];
    int count = 0;
    struct node* head, * tail, * p, * q;

    head = (struct node*)malloc(sizeof(struct node));
    head->next = NULL;

    while (1) {
        scanf("%d %d", &n, &m);
        if (n == 0 || m == 0) {
            free(head);
            break;
        }
        else {
            //尾插法,生成循环链表
            tail = head;
            for (i = 0; i < n; i++) {
                p = (struct node*)malloc(sizeof(struct node));
                p->data = i + 1;
                tail->next = p;
                p->next = head->next;
                tail = p;
            }
            p = head->next;     //后结点
            q = tail;           //前结点
            i = 1;
            while (p != q) {
                if (i == m) {
                    q->next = p->next;
                    free(p);
                    p = q->next;
                    i = 1;
                }
                else {
                    q = p;
                    p = p->next;
                    i++;
                }
            }
            answer[count] = p->data;
            count++;
            free(p);
            head->next = NULL;
        }
    }

    for (i = 0; i < count; i++) {
        printf("%d\n", answer[i]);
    }

    return 0;
}

三、数组标志位实现

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//数组标记法
int main()
{
	int n, m;
	int number; //记录剩余的猴子个数
	int count = 1; //代表当前的报数
	int i, pos;
	while (~scanf("%d %d", &n, &m)) {//多组nm循环,输入0停止
		if (n == 0 || m == 0) {
			return 0;
		}
		number = n;
		int monkey[301] = { -1 };//数组存储猴子的编号和状态
		//退出的猴子数组值改为0,-1代表无效数组部分,1到n+1代表还没退出的猴子
		for (i = 0; i < n; i++) {
			monkey[i] = i + 1;
		}
		pos = 0;//pos用来控制当前处理的数组下标
		while (number > 1) {
		//剩余的猴子大于1时
			//报数为m的猴子改为0
			if (monkey[pos] > 0) {
				//报数轮次变多,报数为m的越来越多,元素为0的越来越多,只有大于0的元素才需要继续报数操作
				if (count != m) {
					//不退出
					count++;
					pos = (pos + 1) % n; //为什么要取模呢?
					//这是一个环形的结构。比如n = 10,那么10的下一个是1
				}  
				else {
					monkey[pos] = 0;//标记
					count = 1;//重新开始报数
					number--;//猴子数量减少
					pos = (pos + 1) % n;//普通数组构成环状数组的核心语句
				}

			}
		}
		for (i = 0; i < n; i++) {
			if (monkey[i] == 0)
				printf("%d", monkey[i]);
		}
	}
	return 0;
}

       但是OJ超时了。

四、数组模拟链表方式实现

怎样用数组模拟链表让数组的元素值为next的下标,这样就连接起来了
下标和猴子编号之间的关系下标+1为猴子的编号,最后输出的时候别忘了下标+1
双指针pos和prior一前一后,怎么移动才能有循环的效果呢?
//数组链接方式实现实现约瑟夫环
//用数组来模拟循环链表
//开拓思路
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
	int n, m;
	int number;//记录剩余猴子个数
	int count;//代表当前的报数
	int i, pos, prior;//pos当前下标,prior表示前一个下标。像不像我们链表里面的双指针?
	while (~scanf("%d %d", &n, &m)) {
		if (n == 0 || m == 0) return 0;
	    
		int monkey[301] = { 0 };//下标为-1的退出循环
		for (i = 0; i < n - 1; i++) {
			monkey[i] = i + 1;
		}
		//数组下标代表猴子编号,下标比猴子的实际编号小1
		//下标为0的猴子的数组元素是1,即他的下一个猴子是下标为1的猴子。一共10个猴子的话,最后一个猴子的下标为9,它对应的数组元素是0,即他的下一个猴子是下标为0的猴子,也就是说最后一个猴子的下一个猴子是第一个猴子
		monkey[i] = 0; //i = n-1的时候跳出循环,即最后一个猴子的数组元素值(编号)为0,形成循环链表

		number = n;
		pos = 0;//相当于单循环链表的p
		prior = n - 1; //相当于单循环链表的q,他最开始再尾指针的位置
		count = 1;//报数为1

		while (number > 1) {//或者是pos != prior
			if (count != m) {//没找到,移动
				prior = pos;
				pos = monkey[pos];
				count++;
			}
			else {
				monkey[prior] = monkey[pos];//数组值就是下一个猴子的下标
				monkey[pos] = -1;//标记为退出
				pos = monkey[prior];//当前猴子更新为下一个猴子
				number--;
				count = 1;
			}
		}
		printf("%d", pos + 1);//猴子的下标+1为猴子的编号
	}
	return 0;
}

链接:数据结构与算法作业:带密码的约瑟夫环(数据结构与算法上机作业专栏里面的文章已经更新)

拓展、约瑟夫环数学方法,推导在课后完成

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值