学科分类
目录
数据结构

约瑟夫环

约瑟夫问题是循环链表的一个典型应用,其描述如下:m个人围成了一圈,从其中任一个人开始,按顺时针顺序使所有人依次从1开始报数,报到n的人出列;然后使n之后的人接着从1开始报数,再次使报到n的人出列……如此下去,求算出列的顺序,及最后留下来的人的编号。

为了更清晰的描述问题,可以将m与n设定为具体数字,如m = 8,n = 3,即8个人围坐成一圈。为这8个人编号,使编号为1的人从1开始报数,报到3的人出局;编号为4的人再从1开始报数,报到3的出局……如此重复,直到这8个人全部出局。出局过程如图1所示。

图1 约瑟夫环

第一轮:从1到3,第3个人出局。

第二轮:第4个人从1开始报数,第6个人报到3,则第6个人出局。

第三轮:第7个人从1开始报数,第1个人报到3,则第1个人出局。

第四轮:第2个人从1开始报数,第5个人报到3,则第5个人出局。

第五轮:第7个人从1开始报数,第2个人报到3,则第2个人出局。

第六轮:第4个人从1开始报数,第8个人报到3,则第8个人出局。

第七轮:第4个人从1开始报数,此时只剩下他和7,他自己报到3,则第4个人出局。

最后这一圈人只剩下7。

当数据量较小时,通过作图我们很轻易地就能找出出列顺序,但当数据量较大时,人工计算几乎是不可能的。要解决这样的问题,需要借助一定的编程算法,而我们刚刚学习过的循环链表就正好可以用来解决此问题。首先用这些数据创建一个循环链表;然后设置限制条件并循环遍历链表,当遍历到要出局的元素时,就将其删除,这样循环操作直到链表中只剩下一个结点。具体代码实现如例1所示。

例1

clist.h //头文件

 1    #ifndef _CLIST_H_
 2    #define _CLIST_H_
 3    
 4    struct Node;
 5    typedef struct Head*  pHead;  //头结点类型
 6    typedef struct Node*  pNode;  //数据结点类型
 7    //定义头结点
 8    struct Head
 9    {
 10        int length;
 11        pNode next;                 //指向下一个结点的指针
 12    };
 13    //定义数据结点
 14    struct Node
 15    {
 16        int data;
 17        pNode next;                 //指向后继结点的指针
 18    };
 19    pHead ClistCreate();              //创建循环链表
 20    int getLength(pHead ph);         //获取循环链表的长度
 21    int IsEmpty(pHead ph);             //判断链表是否为空
 22    int ClistInsert(pHead ph, int pos, int val); //在链表的pos位置插入元素val
 23    void print(pHead ph); //打印循环链表中的元素
 24    
 25    #endif

clist.c //文件

 26    #include "clist.h"
 27    #include <stdio.h>
 28    #include <stdlib.h>
 29    
 30    pHead ClistCreate() //创建循环链表
 31    {
 32        pHead ph = (pHead)malloc(sizeof(struct Head)); //为头结点分配空间
 33        if (ph == NULL)
 34            printf("分配头结点失败!"); //为了方便运行结果查看,不设置return返回
 35        //创建好头结点后,初始化头结点中数据
 36        ph->length = 0;
 37        ph->next = NULL;
 38        return ph;             //将头结点返回
 39    }
 40    
 41    int IsEmpty(pHead ph)     //判断链表是否为空
 42    {
 43        if (ph == NULL)
 44            printf("传入的循环链表有误!");
 45        if (ph->length == 0) //如果长度为0,则链表为空
 46            return 1;
 47        else
 48            return 0;
 49    }
 50    
 51    int ClistInsert(pHead ph, int pos, int val) //在链表的pos位置插入元素val
 52    {
 53    
 54        if (ph == NULL || pos < 0 || pos > ph->length)
 55        {
 56            printf("插入元素时,参数传入有误!\n");
 57        }
 58    
 59        pNode pval = NULL;
 60        //参数传入无误,则为新元素val分配结点
 61        pval = (pNode)malloc(sizeof(struct Node));
 62        pval->data = val;             //将值val保存到此结点中
 63    
 64        //接下来要判断在哪个位置插入元素,先判断链表是否为空
 65        if (IsEmpty(ph))             //如果链表为空
 66        {
 67            ph->next = pval;          //直接将结点插入到头结点后
 68            pval->next = pval;         //将第一个结点指向它自己
 69        }
 70        else //循环链表不为空,则分为在头部插入(即头结点后)和普通位置插入
 71        {
 72            pNode pRear = ph->next;
 73            if (pos == 0) //在第一个位置(头结点后)插入
 74            {
 75                //在0号位置插入,需要先找到尾结点
 76                while (pRear->next != ph->next)     //循环结束的条件
 77                {
 78                    pRear = pRear->next;             //pCur指针向后移动
 79                }
 80                //while循环结束后,pRear指向尾结点
 81                //然后插入元素
 82                pval->next = ph->next;
 83                ph->next = pval;
 84                pRear->next = pval;                 //这三个步骤顺序不能更改
 85            }
 86            else //如果不是0号位置插入,则和单链表无区别
 87            {
 88                pNode pCur = ph->next;
 89                for (int i = 1; i < pos; i++)         //就要遍历链表找到要插入的位置
 90                {
 91                    pCur = pCur->next;                 //pCur指针向后移
 92                }
 93                //循环结束后,此时pCur指向的是要插入的位置
 94                pval->next = pCur->next;             //指针断开再连接的过程
 95                pCur->next = pval;
 96            }
 97        }
 98        ph->length++;
 99        return 1;
 100    }
 101    
 102    void print(pHead ph) //打印循环链表中的元素
 103    {
 104        if (ph == NULL || ph->length == 0)
 105        {
 106            printf("参数传入有误!");
 107        }
 108    
 109        pNode pTmp = ph->next;
 110    
 111        for (int i = 0; i < ph->length ; i++)
 112        {
 113            printf("%d ", pTmp->data);
 114            pTmp = pTmp->next;
 115        }
 116        printf("\n");
 117    }

Joseph.c //测试文件

 118    #define _CRT_SECURE_NO_WARNINGS
 119    #include "clist.h"
 120    #include <stdio.h>
 121    #include <stdlib.h>
 122    
 123    int main()
 124    {
 125        int m, n;
 126        printf("请输入约瑟夫环的总人数:\n");
 127        scanf("%d", &m);
 128        if (m <= 0)
 129        {
 130            printf("请输入正确的数字!\n");
 131            return 0;
 132        }
 133        printf("请输入出局者的报数:\n");
 134        scanf("%d", &n);
 135        if (n <= 0)
 136        {
 137            printf("请输入正确的数字!\n");
 138            return 0;
 139        }
 140    
 141        //根据输入的m创建链表
 142        pHead ph = NULL;
 143        ph = ClistCreate();
 144        if (ph == NULL)
 145        {
 146            printf("创建循环链表失败!\n");
 147            return 0;
 148        }
 149    
 150        //插入元素
 151        for (int i = m; i > 0; i--)
 152        {
 153            ClistInsert(ph, 0, i); //使用头插法从m到1倒序插入
 154        }
 155    
 156        print(ph);
 157        printf("出局顺序:\n");
 158        //插入元素后,就循环遍历链表
 159        pNode node = ph->next; //node指针指向第一个结点
 160        while (node->next != node) //循环结束条件,结点指向其自身,此时剩最后一个结点
 161        {
 162            for (int i = 1; i < n - 1; i++) //i < n - 1,报到n就重新开始
 163            {
 164                node = node->next; 
 165            }
 166            //for循环结束后,node指针指向待出局的结点的前驱结点
 167            pNode pTmp = node->next;         //pTmp指向要被踢出的结点
 168            
 169            //接下来先要判断这个结点是0号位置的结点还是其他位置的结点
 170            if (pTmp == ph->next)             //如果此结点在0号位置
 171            {
 172                ph->next = pTmp->next;         //头结点也要作处理
 173                node->next = pTmp->next;
 174                printf("%d  ", pTmp->data);
 175                free(pTmp);
 176                ph->length--;
 177            }
 178            else  //如果此结点在其他位置
 179            {
 180                node->next = pTmp->next;
 181                printf("%d  ", pTmp->data);
 182                free(pTmp);
 183                ph->length--;
 184            }
 185            node = node->next;
 186        }
 187        node->next = node;  //循环结束,只剩下node一个结点,让其指向自身
 188        printf("\n");
 189    
 190        printf("链表中最后留下的是 ");
 191        print(ph);
 192    
 193        system("pause");
 194        return 0;
 195    }

运行结果如图2所示。

图2 例1运行结果

在例1中,创建一个循环链表,以循环链表为基础来计算这一圈人的出局序列,并求出最后留下来的人。解决约瑟夫问题并没有用到循环链表的全部算法,因此在本例中只实现了此问题涉及的操作。首先创建一个循环链表,使用每位参与者信息初始化该链表;然后开始遍历,第159~187行代码是将报数为n的结点删除,第160行代码的循环条件是node->next != node,即当链表中只剩一个结点时终止循环;while循环里用一个for循环(第162-165行代码)来控制报数情况,报到n就将此结点删除。

点击此处
隐藏目录