我们先看一下对该问题的描述:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。
解决该问题通常有两种方法:
- 模拟法(模拟整个游戏的运行过程)
- 循环链表
- 数组
- 数学公式法(直接通过公式推导得出结果,不关心具体过程)
谈不上哪种更简单,只在于思考问题、解决问题的方式不同而已。
- 模拟法的时间复杂度为O(mn),当n和m很大时,程序的将很难在短时间内得到结果。
- 模拟法的一个优点是:程序的设计思路很清晰。
- 数学公式法的时间复杂度为O(n)。
- 数学方法虽然使最终的程序编写起来很简单,但前提是,你得有足够强的抽象思维能力,能够得出最终的公式。
下面我们给出以上各种方法的实现:
循环链表模拟
确切说来,我们具体使用的是单向循环链表。我们从1,2,3,…n给每个人编号。一旦他出列,我们就将他从链表中删去,直到剩下最后一个人,即为获胜者。
typedef struct node {
int number; /* 编号 */
struct node *next;
} Node;
这里需要注意的是删除操作。我们都知道,每个节点都有一个next指针指向它,为了将删除所有位置的节点看作一种通用的情况,我们需要一个指向next域的指针,即一个二级指针。而且在删除过程中,要注意保证链表不断裂。
int main(void)
{
Node *head, **np, *tmp;
int n, m, k;
scanf("%d%d%d", &n, &m, &k);
head = NULL;
if ((head = creat_cll(head, n)) == NULL)
return 1; /* 初始化链表 */
for (np = &head->next; *np != head; np = &(*np)->next)
if ((*np)->number == k) /* 找到开始报数的人 */
break;
k = 1;
while ((*np)->next != *np) { /* 剩下最后一个节点,循环终止 */
if (k++ == m) { /* 删除应该出列的人 */
tmp = (*np)->next;
free(*np);
*np = tmp;
k = 1;
} else
np = &(*np)->next;
}
printf("%d\n", (*np)->number); /* 打印出获胜者 */
return 0;
}
下面的操作类似于单链表的创建,只是最后我们需要把链表的首尾连接起来而已。
/* creat_cll函数:创建一个单向循环链表,并初始化 */
Node *creat_cll(Node *head, int n)
{
Node *new, *pre;
if ((pre = new = head = malloc(sizeof(Node))) == NULL)
return NULL;
new->number = n;
while (--n > 0) {
if ((new = malloc(sizeof(Node))) == NULL)
return NULL;
new->next = pre;
pre = new;
new->number = n;
}
head->next = new; /* 连接链表的首尾 */
return head;
}
循环链表还有一种方法,就是采用惰性删除
意思就是我们并不真正地删除链表节点,而只是将它标记为已删除状态。因为我们的编号都是大于0的数,所以一旦某个人出列,我们只需将他的number置为0即可。
具体过程如下:
int main(void)
{
Node *head, *p;
int n, m, k;
scanf("%d%d%d", &n, &m, &k);
head = NULL;
if ((head = creat_cll(head, n)) == NULL)
return 1;
for (p = head->next; p != head; p = p->next)
if (p->number == k) /* 找到开始报数的人 */
break;
k = 1;
while (n > 1) {
if (k++ == m) {
p->number = 0;
k = 1;
n--;
}
do { /* 每次遍历链表的时候,需要跳过那些已出列的节点,即number域为0的节点 */
p = p->next;
} while (!p->number);
}
printf("%d\n", p->number);
return 0;
}
最后我们并没有销毁链表,你可以自行加上该操作。
数组模拟
数组模拟方法的难点在于我们要循环使用一个线性数组,其实稍微认真思考一下,也不是很难。每当遍历到数组末尾的时候,我们就从头重新开始遍历,一旦某个人出列,就将该处的值置为0,直到剩下最后一个人。
int main(void)
{
char *p, *q,
char *q_end; /* 指向数组末尾 */
int n, m, k, i;
scanf("%d%d%d", &n, &m, &k);
if ((q = malloc(sizeof(int) * n)) == NULL)
return 1; /* 为使用的数组分配内存空间 */
for (i = 0; i < n; i++)
q[i] = i + 1; /* 初始化数组 */
i = 1;
q_end = q + n;
p = q + k - 1; /* 指向开始报数的人 */
while (n > 1) {
if (i++ == m) {
*p = 0;
i = 1;
n--;
}
do {
if (++p >= q_end) /* 到达数组末尾后,就从头重新开始遍历 */
p = q;
} while (!*p); /* 跳过已出列的人 */
}
printf("%d\n", *p);
free(q);
q = NULL;
return 0;
}