如何在不使用队列,堆栈或数组的情况下解决这个问题?

时间:2015-04-02 19:08:10

标签: c arrays stack queue

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

您将获得一张包含N张牌的牌组。在拿着甲板时:

  1. 从卡座上取下顶卡并将其放在桌子上
  2. 将下一张卡从顶部拿下并放在卡座的底部 在你手中。
  3. 继续执行步骤1和2,直到所有卡片都在桌面上。这是一个 轮。
  4. 从桌子上拿起甲板,重复步骤1-3直到甲板 是原始顺序。
  5. 编写一个程序来确定放置a需要多少轮 甲板回到原来的顺序。这将涉及创建数据 结构来表示卡的顺序。不要使用数组。 该程序应仅以C语言编写。它应该需要一些 作为命令行参数的卡片中的卡片并将结果写入 标准输出。请确保程序编译并正确运行(否 伪代码)。这不是一个棘手的问题;应该是公平的 简单。

4 个答案:

答案 0 :(得分:4)

我没有看到任何明显的方法来查找循环群组的长度user3386109 mentioned没有使用任何数组。

此外,"这不是一个技巧[面试]问题" 对我来说听起来就像面试官只是想让你用C以外的其他东西模拟甲板操作阵列。

想到的直接解决方案是使用单链接或双链接列表。就个人而言,我会使用一个单独链接的列表作为卡片,并使用一个卡片结构来保存卡片中第一张和最后一张卡片的指针,因为洗牌操作会将卡片移动到卡片的顶部和底部:< / p>

#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;

应该分配最后一个&#39; power&#39;,并将其初始化为全零。我们将仅使用条目[2]到[卡片],包括在内。目的是能够将结果计算为Π(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秒来计算一张拥有10万张牌的牌组,轮数为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

请注意,出于可视化目的,我使用以下代码段来描述一轮中卡位置的变化:

    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");

只需将输出提供给例如来自Graphvizdot来绘制精确的有向图。

答案 1 :(得分:1)

将甲板恢复到其原始状态所需的回合数等于旋转组 [1] 长度的最小公倍数(LCM)。

举一个简单的例子,考虑一张标有ABC的3张牌。在问题中应用程序,甲板将按照以下顺序进行,在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,使列表中的每个数字均匀分配到产品中,即{{1所有product % array[i] == 0

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

例如,如果到目前为止的产品是24而下一个数字是8,那么array[i]我们计算gcd(24,8)=8。换句话说,产品不会改变,因为8已经均匀分为24个。如果下一个数字是9,那么product=product * 8/8,那么gcd(24,9)=3。请注意,8,9和24均均分为72.

这种计算LCM的方法消除了因子分解的需要(这反过来又消除了计算素数列表的需要)。

product=product * 9/3 = 72

答案 2 :(得分:1)

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

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

定义了一个函数&#39; number_of_rotations&#39;它接受一个整数作为函数调用的参数(卡片中卡片的数量)并返回一个整数值,该值是为了获得卡片中相同顺序的卡片而采取的轮数。该功能定义如下:

    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循环具有与牌组中的牌顺序相匹配的停止标准。使用函数&#39; compare_list&#39;计算停止标准的该值。它还利用了#shuffle&#39;执行步骤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;
        }
    }

该程序已针对许多输入案例进行了测试,并且能够准确地执行所有测试案例!

答案 3 :(得分:0)

要求&#34;不要使用阵列&#34;可以通过各种方式实现。仅仅因为问题对于求职面试来说是愚蠢的,我可能会选择双链表数据结构。

现在,今天我不是一个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))