牛逼!一行代码解决约瑟夫环问题!

问题描述

 

编号为1-N的N个士兵围坐在一起形成一个圆圈,从编号为 1 的士兵开始依次报数(1,2,3…这样依次报),数到3的士兵会被杀死出列,之后的士兵再从 1 开始报数。直到最后剩下一士兵,求这个士兵的编号。

 

01

数组

 

首先申请一个n个元素的数组空间,全部设置为0,表示所有人都在圈内,之后设立一个计数器,从零开始计数,记到指定数目时有人出圈,将计数器清零,该人出圈(数组对应的元素改为1),从下一个人开始继续计数。

 

#include <stdio.h>

int main()
{
  int num, person;
  int flag[1024] = {0};        //标志位,表示人在不在圈内
  int k = 0, i = 1;            //k计数器 i数组下标,初始化成1,表示第一个人

  printf("Pleaes input...\n");
  scanf("%d", &num);

  person = num;

  while (1)
  {
    if (flag[i] == 0)       //人在圈内
    {
      k++;
      if (k == 3)        //报数为3
      {
        k = 0;         //k回到0
        flag[i] = 1;   //踢出
        person--;
      }
    }
    i++;
    if (i == num + 1)
    {
      i = 1;
    }
    if (1 == person)
    {
      break;
    }
  }

  for (i = 1; i <= num; i++)
  {
    if (flag[i] == 0)
    {
      printf("%d\n", i);
      break;
    }
  }

  return 0;
}

 

02

单向循环链表

 

用链表解决约瑟夫环的方法和数组相同,只是选择了不同的数据结构而已。把所有士兵编号后放入单向循环链表,从0开始报数,数字3则把节点删除。当链表长度为1时,留在链表中的节点就是最后存活的士兵。

 

#include <stdio.h>
#include <stdlib.h>

struct Node
{
    int data;                      //数据域
    struct Node *next;             //指针域
};
typedef struct Node Node;

/*
日期:20200202
函数描述:根据士兵人数创建没有头节点的单向循环链表
函数参数:士兵人数
函数返回值:返回第一个节点的地址
*/
Node *CreateLink(int p)
{
    if (p <= 0)
    {
        return NULL;
    }

    Node *h = (Node *)malloc(sizeof(Node) * 1);
    if (NULL == h)
    {
        return NULL;
    }
    h->data = 1;                   //节点的编号
    h->next = h;                   //形成循环链表

    Node *q = h;
    int i;
    for (i = 0; i < p - 1; i++)
    {
        Node *m = (Node *)malloc(sizeof(Node) * 1);
        if (NULL == m)
        {
            return NULL;
        }
        m->data = i + 2;
        m->next = h;
        q->next = m;
        q = m;
    }

    return h;
}


/*
日期:20200202
函数描述:根据节点地址求链表的长度
函数参数:某个节点的地址(不一定是第一个节点)
函数返回值:返回链表长度
*/
int LinkLength(Node *h)
{
    int length = 1;
    Node *p = h->next;

    while (p != h)
    {
        length++;
        p = p->next;
    }

    return length;
}

/*
日期:20200202
函数描述:删除链表中的节点
函数参数:指向要删除节点的指针
函数返回值:返回删除节点的下一个节点的地址
*/
Node *DeleteNode(Node *p)
{
    Node *m = p;
    while(m->next != p)
    {
        m = m->next;
    }

    m->next = p->next;
    free(p);

    return m->next;
}

/*
日期:20200202
函数描述:约瑟夫环
函数参数:链表第一个节点的地址
函数返回值:返回最后留下士兵的编号
*/
int Joseph(Node *h)
{
    if (NULL == h)
    {
        return -1;
    }

    int k = 0;
    Node *p = h;
    while (LinkLength(p) != 1)
    {
        k++;
        if (3 == k)
        {
            k = 0;
            p = DeleteNode(p);            //节点从链表中删除 返回下一个节点的地址  
        }
        else
        {
            p = p->next;
        }
    }

    return p->data;
}

int main()
{
    int person;                    //记录人数

    printf("请输入人数:\n");
    scanf("%d", &person);

    Node *head = CreateLink(person);   //创建循环链表,返回头指针
    if (NULL == head)
    {
        printf("单链表创建失败!\n");
        exit(1);
    }

    int last = Joseph(head);                       //处理约瑟夫环

    printf("最后一个士兵是%d\n", last);
    return 0;
}

 

03

递归

 

如果我们把士兵删除后,重新给这些士兵编号的话,那么删除前和删除后,这些编号存在某种数学关系,我们只需要找出这个关系即可。

 

我们定义递归函数Joseph(n)的返回结果是存活士兵的编号,显然当n = 1时,Joseph(n) = 1。假如我们能够找出Joseph(n) 和Joseph(n-1) 之间的关系的话,我们就可以用递归的方式来解决了。我们假设人员数为n, 报数到3的人就自杀。则刚开始的编号为:

 

 

进行了一次删除之后,删除了编号为 m 的节点。删除之后,就只剩下n - 1个节点了,对删除后的节点重新编号:

 

 

假设old为删除之前的节点编号,new为删除了一个节点之后的编号,则old与new之间的关系为:

 

old = (new + 2) % n + 1;

 

这样,我们就得出Joseph(n) 与Joseph(n - 1)之间的关系了,而Joseph(1) = 1。于是得出以下代码:

 

int Joseph(int n)
{
    if (1 == n) return 1;
    return (Joseph(n - 1) + 2) % n + 1; 
}

 

当然,我们写这篇文章还有一部分装X的成分在,所以干脆让代码更简洁一点,一行代码搞定:

 

#include <stdio.h>

int Joseph(int n)
{
    return (n == 1) ? n : ((Joseph(n - 1) + 2) % n + 1); 
}

int main()
{
    int person;

    printf("请输入人数:\n");
    scanf("%d", &person);

    printf("最后一个士兵 %d\n", Joseph(person));

    return 0;
}

 

卧槽,以后面试官让你手写约瑟夫问题,你就扔这一行代码给它。

 

更多内容,关注公众号 学益得智能硬件

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值