【问题标题】:How to solve this without using queues, stacks, or arrays?如何在不使用队列、堆栈或数组的情况下解决这个问题?
【发布时间】:2015-06-07 20:37:58
【问题描述】:

最近我接受了一次采访,并被提出了以下问题。诀窍是在没有队列、堆栈或数组的情况下解决这个问题。我无法回答这个问题。不用说,我没有得到这份工作。你会如何解决这个问题。

给你一副包含 N 张牌的牌组。拿着甲板时:

  1. 从牌堆中取出最上面的牌,放在桌子上
  2. 从顶部取出下一张牌并将其放在牌组底部 在你的手中。
  3. 继续第 1 步和第 2 步,直到所有牌都在桌上。这是一个 圆形。
  4. 从桌子上拿起甲板并重复步骤 1-3,直到甲板 是按照原来的顺序。

编写一个程序来确定需要多少轮 套牌回到原来的顺序。这将涉及创建数据 结构来表示卡片的顺序。不要使用数组。 该程序只能用 C 语言编写。它应该采取一些 卡片组中的卡片作为命令行参数并将结果写入 标准输出。请确保程序正确编译和运行(否 伪代码)。这不是一个技巧问题。应该是公平的 直截了当。

【问题讨论】:

  • 你甚至需要一个数据结构吗?嗯......为什么有人会问这样一个很可能与任何类型的工作没有任何关系的问题?
  • 第一行是 "The trick is..." 但最后一行是 "This is not a trick question" .或者,也许 "trick" 的含义是模棱两可的,因为它涉及一副纸牌。很高兴你没有得到这份工作。
  • 为什么投反对票? :(
  • @gnat - 我的错。评论已删除。
  • 答案是轮换组长度的最小公倍数。例如,给定 N=11,轮换组的长度为 5,3,3,因此需要 15 轮才能将甲板恢复正常。问题是,“你如何找到轮换组的长度?”

标签: c arrays stack queue


【解决方案1】:

我没有看到任何明显的方法来找出循环组 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 绘制一个漂亮的有向图。

【讨论】:

  • 很好的答案@Nominal Animal!你能在某处分享完整的源代码吗?谢谢!
  • @Bill:当然——虽然上面没有显示的内容不多。基于列表的版本的来源是here,基于数组的版本的来源是here(在我自己的网站上)。
【解决方案2】:

将牌组恢复到原始状态所需的轮数等于轮换组长度的最小公倍数 (LCM)[1]

举个简单的例子,考虑一副标有ABC 的三张牌。套用题中的流程,套牌会经过以下顺序,3轮后回到起始位置。

ABC     original
BCA     after 1 round
CAB     after 2 rounds
ABC     after 3 rounds the deck is back to the original order

请注意,在每一轮中,第一张牌会走到牌组的尽头,另外两张牌会向前移动一个位置。换句话说,甲板每轮旋转 1 个位置,三轮后它又回到原来的位置。

举一个更有趣的例子,考虑一副 11 张牌。前几轮的牌组状态是

ABCDEFGHIJK
FJBHDKIGECA
KCJGHAEIDBF
ABCIGFDEHJK

请注意,在第一轮中,A 移动到 K 所在的位置,K 移动到 F 所在的位置,F 移动到 A 所在的位置。所以 A、F、K 组成了一个大小为 3 的旋转组。如果我们忽略其他字母,只观察 A、F 和 K,我们会看到 AFK 每三轮回到原来的位置。

同样BCJ 为 3 人一组,DEGHI 为 5 人一组。由于有些牌每 3 轮返回其原始位置,而其他牌每 5 轮返回,因此这副牌将在LCM(3,5) = 15 回合后恢复其原始状态。

[1] 维基百科将它们称为cyclic groups。不确定这对任何人都有多大用处,除了要注意 OP 的问题属于称为 group theory 的数学类别。


计算 LCM

数字列表array[i] 的最小公倍数 (LCM) 被定义为最小数字 product 使得列表中的每个数字均分到乘积中,即 product % array[i] == 0 对所有 @987654332 @。

为了计算 LCM,我们从 product = 1 开始。然后对于每个array[i],我们计算productarray[i] 的最大公约数(GCD)。然后将product 乘以array[i] 除以GCD。

例如,如果到目前为止的产品是 24,下一个数字是 8,那么 gcd(24,8)=8 和我们计算 product=product * 8/8。换句话说,乘积不会改变,因为 8 已经被 24 整除。如果下一个数字是 9,那么 gcd(24,9)=3,所以 product=product * 9/3 = 72。请注意,8,9 和 24 都平分为 72。

这种计算 LCM 的方法消除了因式分解的需要(进而消除了计算素数列表的需要)。

int computeLCM( int *array, int count )
{
    int product = 1;
    for ( int i = 0; i < count; i++ )
    {
        int gcd = computeGCD( product, array[i] );
        product = product * (array[i] / gcd);
    }
    return( product );
}

int computeGCD( int a, int b )
{
    if ( b == 0 )
        return( a );
    else
        return( computeGCD( b, a % b ) );
}

【讨论】:

  • 这真的很酷!!!群论是一个非常大的话题。我能在不知道所有这些的情况下找到这些组吗?是否可以从牌组的大小推断出将有多少组以及它们的大小?
  • @flashburn:有可能,但至少我必须使用数组。
  • @user3386109:当有两个以上的组时,您将如何定义它?计算不是问题(我的回答有效);使用素因子分解并记录任何组大小中每个素因子的最高功率都可以正常工作。我只是很难在我的回答中解释它。例如,一副 19 张牌有五个循环:一组 10、一组 5、一组 2 和两组 1。我们知道结果是 10 轮,所以LCM(10,5,2,1) = 10
  • 如果不是很明显,我基本上假设LCM(n1,n2,..,nN) = LCM(nN,LCM(nN-1,...LCM(n2,n1))),这归结为那些n的素数的乘积,每个素数都提高到最高幂,它是一个因数.呃。很明显,我不是数学家。
  • @flashburn NominalAnimal 发布的基于数组的方法是我用来查找组长度的方法。或许可以找到一种纯粹的数学方法来计算群长度,但这并不明显。所以我把它作为一个练习留给读者;)我的意图只是让你朝着正确的方向开始。
【解决方案3】:

我使用链表来解决这个问题。创建节点结构的标准方法如下:

    /*link list node structure*/
    struct Node{                    
        int card_index_number;
        struct Node* next;
    };

定义了一个函数'number_of_rotations',它接受一个整数作为函数调用的参数(牌组中的牌数)并返回一个整数值,即获得相同顺序的牌所需的轮数在甲板上。函数定义如下:

    int number_of_rotations(int number_of_cards){           // function to calculate the 
        int number_of_steps = 0;
        while((compare_list(top))||(number_of_steps==0)){   // stopping criteria which checks if the order of cards is same as the initial order 
            number_of_steps++;
            shuffle();              // shuffle process which carries out the step 1-2
        }
        return number_of_steps;
    }

当与原始顺序相比时,此函数中使用的 while 循环有一个匹配的停止标准,用于匹配牌组中的卡片顺序。该停止标准的值是使用函数“compare_list”计算的。它还利用执行步骤 1-2 的功能“洗牌”; while循环执行步骤3。用于比较卡片顺序的函数定义如下:

    int compare_list(struct Node* list_index){// function to compare the order of cards with respect to its original order
        int index = 1;
        while(list_index->next!=NULL){
            if(list_index->card_index_number!=index){
                return 1;
            }
            list_index=list_index->next;
            index++;
        }
        return 0;
    }

函数shuffle定义如下:

    void shuffle(){
        struct Node* table_top= (struct Node*)malloc(sizeof(struct Node));  //pointer to the card on top of the card stack on the table
        struct Node* table_bottom  = (struct Node*)malloc(sizeof(struct Node));  //pointer to the card bottom of the card stack on the table
        struct Node* temp1 = (struct Node*)malloc(sizeof(struct Node));  //pointer used to maneuver the cards for step 1-2
        table_bottom=NULL;
        while(1){
            temp1 = top->next;
            if(table_bottom==NULL){  // step 1: take the card from top of the stack in hand and put it on the table
                table_bottom=top;
                table_top=top;
                table_bottom->next=NULL;
            }
            else{
                top->next=table_top;
                table_top=top;
            }
            top=temp1;  // step 2: take the card from top of the stack in hand and put it behind the stack
            if(top==bottom){  // taking the last card in hand and putting on top of stack on the table
                top->next=table_top;
                table_top=top;
                break;
            }
            temp1 = top->next;
            bottom->next=top;
            bottom=top;
            bottom->next=NULL;
            top=temp1;
        }
        top=table_top; //process to take the stack of cards from table back in hand
        bottom=table_bottom;  //process to take the stack of cards from table back in hand
        table_bottom=table_top=temp1=NULL;  // reinitialize the reference pointers
    }

这部分是附加的。下面这些函数用于生成卡组中卡片的链表,另一个函数用于按顺序打印卡组中卡片的索引。

    void create_list(int number_of_cards){
        int card_index = 1;
        //temp and temp1 pointers are used to create the list of the required size 
        struct Node* temp = (struct Node*)malloc(sizeof(struct Node));      
        while(card_index <= number_of_cards){
            struct Node* temp1 = (struct Node*)malloc(sizeof(struct Node)); 
            if(top==NULL){
                top=temp1;
                temp1->card_index_number=card_index;
                temp1->next=NULL;
                temp=top;
            }
            else{
                temp->next=temp1;
                temp1->card_index_number=card_index;
                temp1->next=NULL;
                bottom=temp1;
            }
            temp=temp1;
            card_index++;
        }
        //printf("\n");
    }


    void print_string(){                    // function used to print the entire list
        struct Node* temp=NULL;
        temp=top;
        while(1){
            printf("%d ",temp->card_index_number);
            temp=temp->next;
            if(temp==NULL)break;
        }
    }

这个程序已经针对许多输入用例进行了测试,并且它对所有测试用例的执行都准确!

【讨论】:

    【解决方案4】:

    这个问题(和答案)很有趣,因为它们揭示了放弃使用一个明显有用的工具(在这种情况下,一般意义上的“容器”,包括堆栈、数组、队列、哈希图)是多么困难等)面试问题(正如它所说,不是一个技巧问题)要求在不使用任何容器的情况下解决问题。它不要求高效解决方案,但正如我们将看到的,无容器解决方案非常好(尽管不是最佳的)。

    首先,让我们考虑计算置换的周期长度。通常的计算是将decompose the permutation 转换为orbits(或“旋转/循环组”),然后计算轨道长度的最小公倍数(LCM)。在没有数组的情况下如何进行这种分解并不明显,但很容易看出如何计算单个元素的循环长度:我们只是通过连续排列跟踪单个元素的进度,计数直到我们回到原来的位置位置。

    /* Computes the cycle length of element k in a shuffle of size n) */
    static unsigned count(unsigned n, unsigned k) {
      unsigned count = 1, j = permute(n, k);
      while (j != k) {
        j = permute(n, j));
        ++count;
      }
      return count;
    }
    

    这足以解决问题,因为数字列表的 LCM 不会因多次包含相同的数字而改变,因为数字及其本身的 LCM 就是数字本身。

    /* Compute the cycle length of the permutation for deck size n */
    unsigned long long cycle_length(int n) {
      unsigned long long period = count(n, 0);
      for (unsigned i = k; k < n; ++k) {
        period = lcm(period, count(n, k));
      }
      return period;
    }
    

    但我们可以做得更好:假设我们只计算从它们的最小元素开始的循环。由于每个轨道都有一个唯一的最小元素,因此每个轨道都会恰好找到一次。而且对上面代码的修改很简单:

    /* Computes the cycle length of element k in a shuffle of size n)
       or returns 0 if element k is not the smallest element in the
       cycle
    */
    static unsigned count(unsigned n, unsigned k) {
      unsigned count = 1, j = permute(n, k);
      while (j > k) {
        j = permute(n, j));
        ++count;
      }
      return j == k ? count : 0;
    }
    /* Compute the cycle length of the permutation for deck size n */
    unsigned long long cycle_length(int n) {
      /* Element 0 must be the smallest in its cycle, so the following is safe */
      unsigned long long period = count(n, 0);
      for (unsigned k = 1; k < n; ++k) {
        unsigned c = count(n, k);
        if (c) period = lcm(period, c);
      }
      return period;
    }
    

    这不仅减少了所需的 LCM 计算次数,还大大减少了跟踪时间,因为一旦我们在循环中找到较小的元素,我们就会退出循环。甲板尺寸高达 20,000 的实验表明,置换的调用次数随着甲板尺寸的增加而缓慢增加,但对于甲板尺寸 14337,每个元素的最大平均调用次数为 14.2。该甲板尺寸的排列是单轨道,所以天真的算法会调用 permute 143372 (205,549,569) 次,而上述启发式算法只调用了 203,667 次。

    通过reducing by the greatest common divisor (GCD) 直接计算最小公倍数,使用Euclidean algorithm 计算GCD。还有其他算法,但这个算法简单、快速且无需容器:

    unsigned long long gcd(unsigned long long a, unsigned long long b) {
      while (b) { unsigned long long tmp = b; b = a % b; a = tmp; }
      return a;
    }
    unsigned long long lcm(unsigned long long a, unsigned long long b) {
      unsigned long long g = gcd(a, b);
      return (a / g) * b;
    }
    

    循环长度迅速增加,即使使用unsigned long long,值很快就会溢出;对于 64 位值,第一个溢出是甲板大小 1954,其循环长度为 103,720,950,649,886,529,960,或大约 266.5。由于我们不能在这种形式的 LCM 计算中使用浮点运算,因此我们需要找到一个多精度库来执行此操作,并且大多数此类库都使用数组。

    只剩下编写permute 函数本身了。当然,尝试使用某种容器来模拟套牌,但这真的没有必要;我们可以追踪单个卡片的进度。

    如果一张牌在牌堆的偶数位置(将第一张牌计为位置 0),则将立即将其放在桌子上。因为这是按顺序发生的,所以卡片 2*k* 将是放在桌子上的 kth 张卡片。 (这对应于最后一副 n 张牌中的位置 n-1-k,因为放在桌子上的第一张牌是最后一张最后一副牌中的牌。)

    奇数位置的卡片将被放置在牌组的(当前)末端;实际上,这会在某种增强型套牌中为他们提供一个新位置。由于总是从牌组中取出第二张牌,因此扩充牌组的总大小(即一轮中处理的牌数)是原始牌组大小的两倍。如果新位置是偶数,则将牌放在桌子上,仍然适用之前的公式;否则,另一个位置将应用于卡片。

    在尝试计算新位置的公式之前,这里有一个有用的观察:假设某张卡片位于 odd 位置 k,下一个位置将是k'。现在假设 k' 也是奇数,因此卡片将被放置在位置 k''。现在,k' - k 必须是偶数,因为 kk' 都是奇数。此外,在 kk' 之间的牌中恰好有一半将在达到 k' 之前被丢弃到桌子上,而另一半则是放在k'之后的甲板上。由于 k'' 必须是下一个位置,我们可以看到 k'' - k' = &half;(k' - k)。因此,一旦我们计算了第一次重定位的偏移量,计算剩余的偏移量就很简单了;我们只是将偏移量反复除以 2,直到我们得到一个奇数,此时卡片放在桌子上。

    实际上进行该计算有一点技巧,但由于除以 2 的次数很少,因此直接进行计算更简单且更容易理解。只需要计算第一个偏移量,但这很简单:卡 2*k*&plus;1 将被重新定位到位置 n&plus;k,所以第一个偏移量是n-k-1。所以这给了我们:

    /* Find the next position of card k in deck of size n */
    /* (The compiler will optimize division by 2 to a shift.) */
    unsigned permute(unsigned n, unsigned k) {
      if (k & 1) { /* If k is odd */
        unsigned delta = n - k/2 - 1;
        do { k += delta; delta /= 2; } while (k & 1);
      }
      /* k is now even; k/2 is count from the bottom of the deck */
      return n - 1 - k/2;
    }
    

    所以有完整的程序;大约 40 行,包括 cmets,并且看不到容器。很酷的部分是,它实际上比使用数组、链表等模拟纸牌的解决方案运行得快得多:我能够在 13 秒内生成所有纸牌大小高达 20,000 的非溢出循环长度,以及甲板尺寸 100,000 在 13 毫秒内的 59 个轨道长度。 (当然,我没有得到 LCM,但即便如此,与 8 秒相比还是非常有利的,就像在一个解决这个甲板大小的答案中一样。我确实验证了我的计算产生了相同的结果,通过计算 Python 中的 LCM 从轨道大小。)


    现在,假设我们确实使用了容器。什么可能是适当的用途?显然,尽管进行了所有尝试,但上述代码调用permute 的次数超出了必要的次数。如果我们知道哪些卡片是已经发现的轨道的一部分,我们可以完全避免处理它们,而不是等到循环产生更小的元素。因为在计算轨道的过程中,我们确实计算了轨道的每个元素,我们可以在大小为n 的位图中将每个元素标记为“已看到”。因此,使用n 位的容器,我们可以将对permute 的调用次数减少到n 的总数。

    数组的另一种可能用途是执行组大小的素数分解。对于单个周期长度的计算,使用 bignum 包进行 LCM 计算可能更简单,但对于不同大小的重复计算,使用素数分解的 LCM 计算可能会更好。这不需要一个非常大的数组,因为我们只需要最大甲板大小的平方根的素数。 (如果一个数不能被任何小于或等于其平方根的素数整除,那么它就是素数。)


    注意:我知道这个问题是很久以前提出的;它引起了我的注意,因为有人在答案形式中添加了评论,将问题简要地提出到我碰巧看到的主页上。但是似乎没有得到适当的回答,我很无聊,可以尝试一下编码练习;因此,这个答案。

    【讨论】:

      【解决方案5】:

      “不要使用数组”的要求可以通过多种方式实现。仅仅因为面试的问题很愚蠢,我可能会选择双链表数据结构。

      现在,今天我没有 c 编程的心情,并且有大量关于如何在 C 中编程双链表的资源......所以只是为了咯咯笑,这里有一个 F# 实现,它显示了必须做的事情生成的 C 程序,是否已编写。

      type State = { Hand : int list; Table : int list }
      
      let init n = 
          { Hand = [1..n]; Table = List.empty }
      
      let drop state =
          match state.Hand with
          | [] ->  { Hand = state.Table; Table = List.empty }
          | _ -> { Hand = state.Hand.Tail; Table = state.Hand.Head :: state.Table }
      
      let shuffle state = 
          match state.Hand with
          | [] -> { Hand = state.Table; Table = List.empty }
          | _ -> { state with Hand = state.Hand.Tail @ [state.Hand.Head];}
      
      let step state =
          state |> drop |> shuffle
      
      let countSteps n =
          let s0 = init n
          let rec count s c =
              let s1 = step s
              let c1 = if s1.Table = List.empty then c+1 else c
              // printfn "%A" s1
              if s1.Hand = s0.Hand then c1
              else count s1 c1
          count s0 0
      
      [1..20] |> List.iter (fun n -> printfn "%d -> %d" n (countSteps n))
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-04-02
        • 2023-02-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-05-23
        • 1970-01-01
        • 2019-08-08
        相关资源
        最近更新 更多