循环链表介绍
先不急着看约瑟夫问题是什么,先了解循环链表的结构,那什么是循环链表?
循环,顾名思义,从链表中第一个节点出发,还会遇到第一个节点,形成循环的一环。也就是说链表中最后一个节点的下一个节点是第一个节点,但是头节点也是一个节点,所以最后一个节点的下一个节点应该是头节点才对。
如下图,有两种情况:
其中更大的长方形是节点的数据域,更小的长方形是节点的指针域,指向链表中的下一个节点。
循环链表的代码实现
因为解决约瑟夫问题,只需要初始化、插入、打印功能,所以别的功能就没实现。
节点定义
首先,循环链表和普通链表节点的结构体定义一模一样,那就写出来吧。
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;
}
---------------------------------------------------------------------------------------------------------------------------------