约瑟夫环的C语言实现

引子

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

进入正题

约瑟夫环问题,在计算机界是一个经典的循环链表问题,题意是:已知 n 个人(分别用编号 1,2,3,…,n 表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列;他的下一个人又从 1 开始,还是顺时针开始报数,数到 m 的那个人又出列;依次重复下去,直到圆桌上剩余一个人。

如图所示,假设此时圆周周围有 5 个人,要求从编号为 3 的人开始顺时针数数,数到 2 的那个人出列:


约瑟夫环

出列顺序依次为:
编号为 3 的人开始数 1,然后 4 数 2,所以 4 先出列;
4 出列后,从 5 开始数 1,1 数 2,所以 1 出列;
1 出列后,从 2 开始数 1,3 数 2,所以 3 出列;
3 出列后,从 5 开始数 1,2 数 2,所以 2 出列;
最后只剩下 5 自己,所以 5 胜出。

手敲代码实现如下

#include "stdlib.h"

#define OK       1
#define ERROR    0

typedef int ElemType;
typedef int Status;

typedef struct Node{
    ElemType data;
    struct Node *next;
}Node;

typedef struct Node * LinkList;

/*
1、初始化单向循环链表(带入节点总数)
 */
Status initJosehuList(LinkList *list, int count){
    
    LinkList tempNode,lastNode = NULL;
    int i = 1;
    while (i <= count) {
        //创建一个节点
        tempNode = malloc(sizeof(Node));
        if (tempNode == NULL) {
            return ERROR;
        }
        tempNode->data = I;
        if (*list == NULL) {
            //首元节点插入
            *list = tempNode;
            tempNode->next = tempNode;
        }
        else {
            //尾节点插入
            tempNode->next = lastNode->next;
            lastNode->next = tempNode;
        }
        //记录尾节点
        lastNode = tempNode;
        I++;
    }
    return OK;
}

/*
2、遍历链表
循环链表的遍历最好用do while语句,因为需要先做一次p = p->next,判断p->next等于首元节点的时候就是找到尾节点了
*/
void ListTraverse(LinkList list){
    
    printf("遍历链表:");
    LinkList p = list;
    if (!p) {
        printf("这是一个空链表");
    }
    else {
        do {
            printf("%d ",p->data);
            p = p->next;
        } while (p != list);
    }
    printf("\n");
}

int JosehuMethod(LinkList *list,int startIndex,int num){
    
    if (*list == NULL || startIndex < 1 || num < 1) {
        return 0;
    }
    //找到最后面的那个人,因为他是index为1时第一个数数的人的前一个人
    LinkList preNode;
    for (preNode = *list; preNode->next != *list; preNode = preNode->next);
    //记录要出列的人
    LinkList locaNode;
    //第几次出列
    int serialNum = 1;
    //记录报到几号了
    int newNum = 1;
    //判断是否只留下一个人了
    while ((*list)->next != *list) {
        //1、先找到开始报数的人
        if (startIndex > 1) {
            preNode = preNode->next;
            startIndex --;
            continue;
        }
        //2、找到后,让他开始报数
        if (newNum < num) {
            newNum ++;
            preNode = preNode->next;
        }
        else {
            //3、找到要出列的人,将他出列
            newNum = 1;
            locaNode = preNode->next;
            //判断是否首元节点,要特殊处理下
            if (locaNode == *list) {
                *list = locaNode->next;
            }
            preNode->next = locaNode->next;
            printf("第%d个出列的人是:%d\n",serialNum,locaNode->data);
            serialNum ++;
            free(locaNode);
        }
    }
    printf("最后留下的人是:%d\n",(*list)->data);
    return (*list)->data;
}

int main(int argc, const char * argv[]) {
    
    //初始化
    LinkList list;
    int count;
    printf("输入队列数量:");
    scanf("%d",&count);
    initJosehuList(&list, count);
    //遍历数据
    ListTraverse(list);
    
    int startIndex;
    int num;
    printf("输入开始报数的位置:");
    scanf("%d",&startIndex);
    printf("输入出列的报数序号:");
    scanf("%d",&num);
    //执行约瑟夫方法
    JosehuMethod(&list, startIndex, num);
    
    printf("\n");
    return 0;
}

输出效果如下

结果.png

推荐阅读更多精彩内容