我没有看到任何明显的方法来找出循环组 user3386109 mentioned 的长度,无需使用任何数组。
此外,“这不是一个技巧 [interview] 问题” 在我看来,面试官只是想让你在 C 中使用数组以外的东西来模拟甲板操作。
想到的直接解决方案是使用单链表或双链表。就个人而言,我会为卡片使用单链表,并使用卡片组结构来保存卡片组中第一张和最后一张卡片的指针,因为洗牌操作会将卡片移动到卡片组的顶部和底部:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
struct card {
struct card *next;
long face; /* Or index in the original order */
};
typedef struct deck {
struct card *top;
struct card *bottom;
} deck;
#define EMPTY_DECK { NULL, NULL }
我会使用的套牌操作函数是
static void add_top_card(deck *const d, struct card *const c)
{
if (d->top == NULL) {
c->next = NULL;
d->top = c;
d->bottom = c;
} else {
c->next = d->top;
d->top = c;
}
}
static void add_bottom_card(deck *const d, struct card *const c)
{
c->next = NULL;
if (d->top == NULL)
d->top = c;
else
d->bottom->next = c;
d->bottom = c;
}
static struct card *get_top_card(deck *const d)
{
struct card *const c = d->top;
if (c != NULL) {
d->top = c->next;
if (d->top == NULL)
d->bottom = NULL;
}
return c;
}
由于没有get_bottom_card()函数,所以没有必要使用双向链表来描述卡片。
洗牌操作本身非常简单:
static void shuffle_deck(deck *const d)
{
deck hand = *d;
deck table = EMPTY_DECK;
struct card *topmost;
while (1) {
topmost = get_top_card(&hand);
if (topmost == NULL)
break;
/* Move topmost card from hand deck to top of table deck. */
add_top_card(&table, topmost);
topmost = get_top_card(&hand);
if (topmost == NULL)
break;
/* Move topmost card from hand deck to bottom of hand deck. */
add_bottom_card(&hand, topmost);
}
/* Pick up the table deck. */
*d = table;
}
deck 结构类型带有指向卡片列表两端的指针的好处是避免在 shuffle_deck() 中进行线性搜索以找到手牌组中的最后一张牌(用于快速附加到手牌组)。我进行的一些快速测试表明,否则线性搜索会成为瓶颈,将运行时间增加大约一半。
一些结果:
Cards Rounds
2 2
3 3
4 2
5 5
6 6
7 5
8 4
9 6
10 6
11 15
12 12
13 12
14 30
15 15
16 4
20 20
30 12
31 210
32 12
50 50
51 42
52 510 (one standard deck)
53 53
54 1680
55 120
56 1584
57 57
80 210
81 9690
82 55440
83 3465
84 1122
85 5040
99 780
100 120
101 3360
102 90
103 9690
104 1722 (two decks)
156 5040 (three decks)
208 4129650 (four decks)
但是,使用数组,可以轻松找出循环长度,并使用这些长度来计算所需的轮数。
首先,我们创建一个图表或映射整个回合中牌位如何变化:
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
size_t *mapping(const size_t cards)
{
size_t *deck, n;
if (cards < (size_t)1) {
errno = EINVAL;
return NULL;
}
deck = malloc(cards * sizeof *deck);
if (deck == NULL) {
errno = ENOMEM;
return NULL;
}
for (n = 0; n < cards; n++)
deck[n] = n;
n = cards;
while (n > 2) {
const size_t c0 = deck[0];
const size_t c1 = deck[1];
memmove(deck, deck + 2, (n - 2) * sizeof *deck);
deck[n-1] = c0;
deck[n-2] = c1;
n--;
}
if (n == 2) {
const size_t c = deck[0];
deck[0] = deck[1];
deck[1] = c;
}
return deck;
}
上面的函数返回一个索引数组,对应于卡片在每一个完整回合后结束的位置。因为这些索引表示卡片位置,所以每一轮都执行完全相同的操作。
该功能没有优化甚至非常高效;它使用memmove() 将甲板顶部保持在阵列的开头。相反,可以将数组的初始部分视为循环缓冲区。
如果您难以将该功能与原始说明进行比较,其目的是始终取两张最上面的牌,并将第一张移至结果牌组的顶部,将第二张牌移至手牌组的底部。如果只剩下两张牌,第一张牌首先进入结果牌组,第二张牌最后。如果只剩下一张牌,它显然会进入结果牌组。在函数中,数组中的第一个n条目是手牌,最后一个cards-n是桌子。
要找出循环的数量,我们只需要遍历上图或映射中的每个循环:
size_t *cycle_lengths(size_t *graph, const size_t nodes)
{
size_t *len, i;
if (graph == NULL || nodes < 1) {
errno = EINVAL;
return NULL;
}
len = malloc(nodes * sizeof *len);
if (len == NULL) {
errno = ENOMEM;
return NULL;
}
for (i = 0; i < nodes; i++) {
size_t c = i;
size_t n = 1;
while (graph[c] != i) {
c = graph[c];
n++;
}
len[i] = n;
}
return len;
}
这个功能也可以增强不少。这会遍历每个周期该周期时间中的位置数,而不是仅遍历每个周期一次,并将周期长度分配给周期中的所有参与者。
对于接下来的步骤,我们需要知道直到并包括卡片数量的所有素数。 (包括,因为我们可能只有一个循环,所以我们可能看到的最大长度是套牌中的卡片数量。)一个简单的选择是使用位图和Sieve of Eratosthenes:
#ifndef ULONG_BITS
#define ULONG_BITS (sizeof (unsigned long) * CHAR_BIT)
#endif
unsigned long *sieve(const size_t limit)
{
const size_t bytes = (limit / ULONG_BITS + 1) * sizeof (unsigned long);
unsigned long *prime;
size_t base;
prime = malloc(bytes);
if (prime == NULL) {
errno = ENOMEM;
return NULL;
}
memset(prime, ~0U, bytes);
/* 0 and 1 are not considered prime. */
prime[0] &= ~(3UL);
for (base = 2; base < limit; base++) {
size_t i = base + base;
while (i < limit) {
prime[i / ULONG_BITS] &= ~(1UL << (i % ULONG_BITS));
i += base;
}
}
return prime;
}
由于可能只有一个循环,覆盖所有卡片,因此您需要为上述函数提供卡片数量 + 1。
让我们看看上面是如何工作的。让我们定义一些我们需要的数组变量:
size_t cards; /* Number of cards in the deck */
unsigned long *prime; /* Bitmap of primes */
size_t *graph; /* Card position mapping */
size_t *length; /* Position cycle lengths, for each position */
size_t *power;
最后一个“power”应该被分配并初始化为全零。我们将仅使用条目 [2] 到 [cards],包括在内。目的是能够将结果计算为 ∏(p^power[p]), p=2..cards。
首先生成映射,然后计算循环长度:
graph = mapping(cards);
length = cycle_lengths(graph, cards);
要计算轮数,我们需要对循环长度进行因式分解,并计算每个因数在长度中的最高幂的乘积。 (我不是数学家,所以如果有人能正确/更好地解释这一点,我们将不胜感激。)
也许实际的代码描述得更好:
size_t p, i;
prime = sieve(cards + 1);
for (p = 2; p <= cards; p++)
if (prime[p / ULONG_BITS] & (1UL << (p % ULONG_BITS))) {
/* p is prime. */
for (i = 0; i < cards; i++)
if (length[i] > 1) {
size_t n = 0;
/* Divide out prime p from this length */
while (length[i] % p == 0) {
length[i] /= p;
n++;
}
/* Update highest power of prime p */
if (power[p] < n)
power[p] = n;
}
}
结果,在size_t不够大的情况下使用浮点数学,
double result = 1.0;
for (p = 2; p <= cards; p++) {
size_t n = power[p];
while (n-->0)
result *= (double)p;
}
我已经验证了这两种解决方案对于最多 294 张卡片的一副牌产生了完全相同的结果(对于 295 张卡片来说,慢速的非阵列解决方案花费的时间太长了,我等不及了)。
后一种方法对于大型套牌也适用。例如,在这台笔记本电脑上大约需要 64 毫秒才能找出使用 10,000 张卡片组需要 2^5*3^3*5^2*7^2*11*13*17*19*23* 29*41*43*47*53*59*61 = 515,373,532,738,806,568,226,400 轮得到原始订单。
(由于精度有限,使用双精度浮点数打印带零位小数的结果会产生稍小的结果,515,373,532,738,806,565,830,656。)
用了将近 8 秒计算出一副牌有 100,000 张牌的回合数为 2^7*3^3*5^3*7*11^2*13*17*19*23*31*41 *43*61*73*83*101*113*137*139*269*271*277*367*379*541*547*557*569*1087*1091*1097*1103*1109 ≃ 6.5*10^70 .
请注意,出于可视化目的,我使用以下 sn-p 来描述一轮中的牌位变化:
printf("digraph {\n");
for (i = 0; i < cards; i++)
printf("\t\"%lu\" -> \"%lu\";\n", (unsigned long)i + 1UL, (unsigned long)graph[i] + 1UL);
printf("}\n");
只需将该输出提供给例如dot from Graphviz 绘制一个漂亮的有向图。