循环单向链表与约瑟夫问题

循环链表介绍

先不急着看约瑟夫问题是什么,先了解循环链表的结构,那什么是循环链表?

循环,顾名思义,从链表中第一个节点出发,还会遇到第一个节点,形成循环的一环。也就是说链表中最后一个节点的下一个节点是第一个节点,但是头节点也是一个节点,所以最后一个节点的下一个节点应该是头节点才对。

如下图,有两种情况:

其中更大的长方形是节点的数据域,更小的长方形是节点的指针域,指向链表中的下一个节点。

循环链表的代码实现

因为解决约瑟夫问题,只需要初始化、插入、打印功能,所以别的功能就没实现。

节点定义

首先,循环链表和普通链表节点的结构体定义一模一样,那就写出来吧。

typedef struct _LoopLinkList //循环链表
{
	int date;
	struct _LoopLinkList* next;
}LinkNode,LinkList;

 循环链表的初始化

初始化循环链表,其中函数的参数是一个指向头节点的指针,如图所示:

两个箭头有些重叠了,不过无伤大雅。

函数参数中的取地址符 & 是引用的意思,为C++特有的,相当于就在使用这个一级指针 L ,无需使用二级指针来接收这个一级指针 L 的地址,如果去掉这个 & ,会发生什么事呢?那就是 L 这个指针所指向的地址无法改变,但是可以改变所指向的变量的值。

bool initLinkList(LinkList *&L)
{
	L = new LinkNode;     //为头结点申请空间
	if (!L) return false; // L为空指针,生成头结点失败

	L->next = L; //头结点指向自己
	L->date = -1;
	return true;
}

如果上面那段话不太明白,可以先跳过,等写完所有代码,然后把初始化函数参数中的 & 去掉,在 IDE 中调试一下即可得出答案。

循环链表的插入(尾插法)

其中函数的参数节点指针 node 指向新加节点。

bool InsertBack(LinkList* &L, LinkNode* node)
{
	if (!L || !node) return false; //如果链表头结点未创建或者传入的结点指针指向空

    //分两种情况,一是链表为空,只有头结点存在
	if (L->next == L)
	{
		node->next = L;
		L->next = node;
	}
	else
	{
		LinkNode* p = L;
		while (p->next != L)
		{
			p = p->next;
		} //找到最后一个节点

		p->next = node;
		node->next = L;
	}
	return true;
}

循环链表的打印

从头节点开始打印,直到又为头节点。

bool LinkPrint(LinkList* L)
{
	if (!L) return false;

	LinkNode* p = L;
	p = L ->next;
	while (p != L)
	{
		printf("%d ", p->date);
		p = p->next;
	}
	return true;
}

约瑟夫问题

解决了循环链表,就开始约瑟夫问题了,哈哈,这故事有很多的分支,我之前听过的不是下面这种。

据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

——摘自约瑟夫问题_百度百科

 不过看完这个故事应该大概了解了约瑟夫问题,这个问题可以用循环链表解决,每一个人都是链表中一个节点,只要某个节点报数报到出圈的那个号码,就把这个节点从链表中删除。

由于原问题的人数实在太多,我这里就简化一下,一共 10 人,报数报到 9 的人出圈,并且要求求出第五轮出圈的号码,还有最后一个出圈的人的号码,所以添加了两个整型变量 loops、num来记录。

首先在进入这个函数之前已经把 10 个节点插入了链表,第 n 个节点的值为 n ,例如:第一个节点的数据域为 1 。

代码实现

其中函数参数 interval 为间隔,也就是出圈的号码 9 。

bool Joseph(LinkList* &L,int interval)
{
	if ( !L || L->next == L) return false; // 头节点未初始化或者是空链表
	if (interval < 1) return false; //报数的间隔也不能小于 1 , 1 的话还能玩,也就是一报数就死

	LinkNode* p = L , *q = NULL ; // q 指向要删除的节点, p 指向要删除的节点的前一个节点
	int loops = 0 , num = 0 ;     //loops表示进行到第几轮,num保存着出圈的人的号码

	int j = 1;

	while (L->next != L) //不为空链表
	{

		while ( j < interval ) //一直报数,除非j等于出圈的数-1,这时候指针p就指向了要删除的节点的前一个节点
		{
			if (p->next != L)
			{
				j++;
			}	
			p = p->next;
		}
		if (p->next == L) //如果p指向最后一个节点,那肯定不能删除头节点,应该删除第一个节点,所以p赋值为头节点
		{
			p = L;
		}

        //删除
		q = p->next;
		num = q->date;
		p->next = q->next;
        delete q;

		j = 1;
        loops++;

		cout <<endl<< "这是第"<<loops<<"轮:" ;
		LinkPrint(L);
		
		if (loops == 5)
		{
			printf("\n第5轮出圈的是:%d", num);
		}
	}

	printf("最后一个出圈的是:%d\n", num);
	return true;
}

 可能有人会想为什么其中循环的条件不是 i <= j 呢?因为在单向链表中,你删除一个节点前,必须要让删除节点的上一个节点的next 指针指向删除节点的下一个节点。

所以指向要删除的节点的上一个节点就方便很多了

如下图:

完整代码以及运行结果

#define _CRT_SECURE_NO_WARNINGS 1

#include <iostream>

using namespace std;
//结点结构体
typedef struct _LoopLinkList
{
	int date;
	struct _LoopLinkList* next;
}LinkNode,LinkList;

//初始化
bool initLinkList(LinkList* &L)
{ 
	L = new LinkNode;
	if (!L) return false; 

	
	L->next = L; //指向自己
	L->date = -1;
	return true;
}


//尾插法
bool InsertBack(LinkList* L, LinkNode* node)
{
	if (!L || !node) return false; 


	if (L->next == L) //链表为空,只有头结点
	{
		node->next = L;
		L->next = node;
	}
	else
	{
		LinkNode* p = L;
		while (p->next != L)
		{
			p = p->next;
		}
		p->next = node;
		node->next = L;
	}
	return true;
}

//打印
bool LinkPrint(LinkList* L)
{
	if (!L) return false;

	LinkNode* p = L;
	p = L ->next;
	while (p != L)
	{
		printf("%d ", p->date);
		p = p->next;
	}
	return true;
}



//解决joseph问题
bool Joseph(LinkList*& L, int interval)
{
	if (!L || L->next == L) return false; // 头节点未初始化或者为空链表
	if (interval < 1) return false;  //报数的间隔

	LinkNode* p = L, * q = NULL; 
	int loops = 0, num = 0;     //loops表示进行到第几轮,num保存着最后一个出圈的人的号码

	int j = 1;

	while (L->next != L) 
	{

		while (j < interval) 
		{
			if (p->next != L)
			{
				j++;
			}
			p = p->next;
		}
		if (p->next == L) 
		{
			p = L;
		}

		//此时 p 指向要删除节点的前一个节点(不为最后一个节点)
		q = p->next;
		num = q->date;
		p->next = q->next;
		delete q;

		j = 1;
		loops++;

		cout << endl << "这是第" << loops << "轮:";
		LinkPrint(L);

		if (loops == 5)
		{
			printf("\n第5轮出圈的是:%d", num);
		}
	}

	printf("最后一个出圈的是:%d\n", num);
	return true;
}
int main(void)
{
	LinkList* L = NULL ;
	LinkNode* e = NULL;

	//1.初始化循环链表
	initLinkList(L);
	
	//2.尾插循环链表
	for (int i = 1; i <= 10; i++)
	{
		e = new LinkNode;
		e->date = i;
		e->next = NULL;
		InsertBack(L, e);
	}


	//3.打印链表
	LinkPrint(L);

	Joseph(L, 9);
	return 0;
}

---------------------------------------------------------------------------------------------------------------------------------

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值