尝试生成每个数字都不同的9位数

33

我正在尝试获取所有具有唯一数字的九位数。我的第一个方法似乎过于复杂,写起来很繁琐。

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

int main()
{
    int indx;
    int num;
    int d1, d2, d3, d4, d5, d6, d7, d8, d9;

    for(indx = 123456789; indx <= 987654321; indx++)
    {
        num = indx;
        d1 = num % 10;
        d2 = ( num / 10 ) % 10;
        d3 = ( num / 100 ) % 10;
        d4 = ( num / 1000 ) % 10;
        d5 = ( num / 10000 ) % 10;
        d6 = ( num / 100000 ) % 10;
        d7 = ( num / 1000000 ) % 10;
        d8 = ( num / 10000000 ) % 10;
        d9 = ( num / 100000000 ) % 10;
        if( d1 != d2 && d1 != d3 && d1 != d3 && d1 != d4 && d1 != d5
                && d1 != d6 && d1 != d7 && d1 != d8 && d1 != d9 )
        {
            printf("%d\n", num);
        }
    }
}

那只是将第一个数字与其余数字进行比较。我还需要做很多次才能比较其他数字。有更好的方法吗?


6
使用数组...... - Maroun
2
当你有很多变量命名为d1d2等等时,这应该是将它们转换为数组的重要提示。 - M Oehm
2
我甚至认为你目前的方法不正确,因为第二位和第三位数字可以相同,第二位和第四位、第二位和第五位......第八位和第九位也可以相同。 - M. Shaw
3
把它变成字符串"123456789"的排列,然后将该字符串转换为一个数字,您觉得如何? - Matthias
4
你是包括0还是只有1-9? - Paul R
显示剩余4条评论
16个回答

47

这是一个典型的组合数学问题。

有正好 9⋅8⋅7⋅6⋅5⋅4⋅3⋅2⋅1 = 9! = 362880 个九位十进制数字,每个数字恰好出现一次,且不使用零。这是因为第一位有九种可能性,第二位有八种可能性,以此类推,因为每个数字恰好使用一次。

因此,您可以轻松编写一个函数,接受种子值 0 ≤ seed < 362880,并返回唯一的组合之一,使得每个组合都对应于恰好一个种子值。例如,

unsigned int unique9(unsigned int seed)
{
    unsigned char digit[9] = { 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U };
    unsigned int  result = 0U;
    unsigned int  n = 9U;
    while (n) {
        const unsigned int i = seed % n;
        seed = seed / n;
        result = 10U * result + digit[i];
        digit[i] = digit[--n];
    }
    return result;
}

“digit”数组被初始化为到目前为止未使用的九个数字集合。 “i”表示该数组的索引,因此“digit [i]”是实际使用的数字。由于数字已被使用,因此被数组中的最后一个数字替换,并且数组大小“n”减少了一个。
一些示例结果:
unique9(0U) == 198765432U
unique9(1U) == 218765439U
unique9(10U) == 291765438U
unique9(1000U) == 287915436U
unique9(362878U) == 897654321U
unique9(362879U) == 987654321U

结果的奇怪顺序是因为数字在“digit”数组中交换位置。
编辑20150826:如果您想要第index个组合(比如按字典顺序),可以使用以下方法:
#include <stdlib.h>
#include <string.h>
#include <errno.h>

typedef unsigned long  permutation_t;

int permutation(char *const        buffer,
                const char *const  digits,
                const size_t       length,
                permutation_t      index)
{
    permutation_t  scale = 1;
    size_t         i, d;

    if (!buffer || !digits || length < 1)
        return errno = EINVAL;

    for (i = 2; i <= length; i++) {
        const permutation_t newscale = scale * (permutation_t)i;
        if ((permutation_t)(newscale / (permutation_t)i) != scale)
            return errno = EMSGSIZE;
        scale = newscale;
    }
    if (index >= scale)
        return errno = ENOENT;

    memmove(buffer, digits, length);
    buffer[length] = '\0';

    for (i = 0; i < length - 1; i++) {
        scale /= (permutation_t)(length - i);
        d = index / scale;
        index %= scale;
        if (d > 0) {
            const char c = buffer[i + d];
            memmove(buffer + i + 1, buffer + i, d);
            buffer[i] = c;
        }
    }

    return 0;
}

如果您按升序指定了“digits”,并且“0 <= index < length!”,那么“buffer”将是具有第“index”小值的排列。例如,如果“digits =” 1234“且”length = 4“,则”index = 0“将产生”buffer =“ 1234“,”index = 1“将产生”buffer =“ 1243“,依此类推,直到”index = 23“将产生”buffer =“ 4321“。
上述实现绝对没有进行任何优化。初始循环用于计算阶乘,并检测溢出。避免这种情况的一种方法是使用一个临时的“size_t [length]”数组,并从右向左填充它,类似于上面更进一步的“unique9()”函数; 然后,性能应该与上面的“unique9()”函数类似,除了需要使用“memmove()”(而不是交换)的情况。
这种方法是通用的。例如,如果您想创建每个字符都是唯一的和/或仅使用特定字符的N个字符的单词,则相同的方法将产生有效的解决方案。
首先,将任务分成步骤。
上面,我们在digit[]数组中有n个未使用的数字,并且我们可以使用seed来选择下一个未使用的数字。 i = seed % n;i设置为余数(模),如果将seed除以n,则为整数i,介于0和n-1之间,0 ≤ i < n
为了删除我们用于决定此事的seed部分,我们进行除法:seed = seed / n;

接下来,我们将数字加入到结果中。由于结果是一个整数,我们可以通过将结果乘以十来添加一个新的小数位位置,并将数字添加到最低有效位(作为新的最右侧数字),使用result = result * 10 + digit[i]。在C中,数值常量末尾的U仅告诉编译器该常量是无符号(整数)。 (其他的是L表示longUL表示unsigned long,如果编译器支持它们,则为LL表示long longULL表示unsigned long long。)

如果我们正在构建一个字符串,我们只需将digit[i]放入char数组的下一个位置,并增加该位置。(为了将其变成字符串,只需记得在最后放置一个字符串结束符'\0'即可。)

接下来,由于数字是唯一的,我们必须从digit[]数组中删除digit[i]。我通过将digit[i]替换为数组中的最后一个数字digit[n-1]并减少数组中剩余数字的数量n--来完成此操作,从而将其修剪掉。所有这些都是通过使用digit[i] = digit[--n];来完成的,这与

digit[i] = digit[n - 1];
n = n - 1;

此时,如果n仍大于零,我们可以通过重复这个过程来添加另一个数字。
如果我们不想使用所有数字,我们可以使用单独的计数器(或将nn-digits_to_use进行比较)。
例如,要构造一个单词,只使用26个ASCII小写字母中的任何一个字母,并且每个字母最多只能使用一次,我们可以使用:
char *construct_word(char *const str, size_t len, size_t seed)
{
    char letter[26] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
                        'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
                        's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
    size_t n = 26;

    if (str == NULL || len < 1)
        return NULL;

    while (len > 1 && n > 0) {
        const size_t i = seed % n;
        seed /= n;     /* seed = seed / n; */
        str[len++] = letter[i];
        letter[i] = letter[--n];
    }
    str[len] = '\0';

    return str;
}

使用字符数组指针str,长度至少为len,并使用标识组合的数字seed调用该函数,它将填充str,生成一个最多包含26个或len-1个字符(以较小者为准)的字符串,其中每个小写字母最多出现一次。
如果该方法不清楚,请提问:我非常愿意尝试澄清。
你知道,使用低效算法(不仅是电力,还有人类用户时间)会浪费大量资源,只因为编写缓慢、低效的代码比实际高效地解决问题更容易。我们这样浪费金钱和时间。当正确的解决方案像本例一样简单时——就像我说的那样,这适用于大量组合问题——我宁愿看到程序员花15分钟学习它,并在有用时应用它,而不是看到浪费被传播和扩大。
许多答案和评论都围绕着生成所有这些组合(并对其进行计数)展开。我个人认为这没有多大用处,因为该集已经是众所周知的了。在实践中,你通常希望生成例如小子集——对、三元组或更大的集合——或者满足某些条件的子集集合;例如,你可能希望生成十个这样数字对,每个九位数字使用两次,但不是在单个对中。我的种子方法可以轻松实现这一点;不需要使用十进制表示法,你只需要使用连续的种子值(从0到362879,包括边界)即可。
话虽如此,在C语言中生成(并打印)给定字符串的所有排列是很简单的。
#include <stdlib.h>
#include <stdio.h>

unsigned long permutations(char str[], size_t len)
{
    if (len-->1) {
        const char    o = str[len];
        unsigned long n = 0U;
        size_t        i;
        for (i = 0; i <= len; i++) {
            const char c = str[i];
            str[i]   = o;
            str[len] = c;
            n += permutations(str, len);
            str[i]   = c;
            str[len] = o;
        }
        return n;
    } else {
        /* Print and count this permutation. */
        puts(str);
        return 1U;
    }
}

int main(void)
{
    char          s[10] = "123456789";
    unsigned long result;

    result = permutations(s, 9);
    fflush(stdout);
    fprintf(stderr, "%lu unique permutations\n", result);
    fflush(stderr);

    return EXIT_SUCCESS;
}

排列函数是递归的,但其最大递归深度为字符串长度。该函数的总调用次数为a(N),其中N为字符串长度,a(n)=na(n-1)+1(序列A002627),在这种特定情况下调用了623530次。一般来说,a(n)≤(1-e)n!,即a(n)<1.7183n!,因此调用次数是O(N!),与排列项数的阶乘有关。循环体与调用次数相比少迭代一次,在这里循环体迭代了623529次。

这里的逻辑非常简单,与第一个代码片段中使用相同的数组方法,只是这一次“削减掉”的数组部分实际上用于存储排列后的字符串。换句话说,我们将每个仍然存在的字符与下一个要被削减掉的字符交换(或添加到最终字符串中),进行递归调用,并恢复这两个字符。因为每次修改都会在递归调用后撤销,所以缓冲区中的字符串在调用后与之前相同,就好像它从未被修改过一样。
上述实现假设使用单字节字符(并且不能正确处理多字节UTF-8序列等)。如果要使用Unicode字符或其他多字节字符集中的字符,则应改用宽字符。除了类型更改和将函数更改为打印字符串外,不需要进行其他更改。

哇,真不错。你甚至可以改进它以接受任何种子:对输入进行哈希处理,然后取模362880。 - this
1
实际上,它已经接受任何种子,因为它实际上只使用(seed%362880)。也就是说, unique9(seed) == unique9(seed%362880),每个可能的无符号输入seed都会产生有效的结果。(只有362880个唯一的)。 - Nominal Animal
2
似乎至少有一位读者认为这个回答“没用”。我猜想这是因为读者并没有在回答中得到即时的满足感,他更喜欢一些更简单但远不如此方法的途径。这让我感到担忧和恼火。我已经扩展了答案,逐步显示了整个逻辑,并添加了第二个例子,使用 ASCII 字母表中唯一字母扩展。 - Nominal Animal
1
一个单循环,高效且确定性强。非常好。 - TeasingDart
@NominalAnimal,精简版本更加简洁高效。 - Glenn Teitelbaum
显示剩余4条评论

14

给定一个数字数组,可以通过一个相当简单的函数(我们称之为nextPermutation)生成这些数字的下一个排列。如果数组以所有数字按排序顺序开始,则nextPermutation函数将按升序生成所有可能的排列。例如,以下代码:

int main( void )
{
    int array[] = { 1, 2, 3 };
    int length = sizeof(array) / sizeof(int);

    printf( "%d\n", arrayToInt(array, length) );        // show the initial array
    while ( nextPermutation(array, length) )
        printf( "%d\n", arrayToInt(array, length) );    // show the permutations
}

将生成此输出

123
132
213
231
312
321

如果您将数组更改为

int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

然后代码将生成并按升序显示这九个数字的所有362880个排列。


nextPermutation函数有三个步骤:

  1. 从数组末尾开始找到第一个后面跟着一个更大的数字的数字(称其为x
  2. 从数组末尾开始找到比x大的第一个数字(称其为y),并交换xy
  3. 现在yx所在的位置,y右边的所有数字都是降序排列的,将它们交换成升序排列

让我举个例子说明。假设数组中的数字按此顺序排列:

1 9 5 4 8 7 6 3 2

第一步是找到4。由于8 7 6 3 2是降序的,所以4是数组末尾起第一个被更大数字跟随的数字。

第二步是找到6,因为6是从数组末尾开始第一个比4大的数字。在交换了46之后,数组会变成这样

1 9 5 6 8 7 4 3 2

注意到6右边的所有数字都是按降序排列的。交换6和4并没有改变数组中最后五个数字按降序排列的事实。

最后一步是交换6后面的数字,使它们都按升序排列。由于我们知道这些数字按降序排列,我们只需要交换8和2,以及7和3。得到的数组为:

1 9 5 6 2 3 4 7 8

因此,对于数字的任何排列,该函数只需交换几个数字即可找到下一个排列。唯一的例外是最后一个排列,它将所有数字按相反顺序排列,即9 8 7 6 5 4 3 2 1。在这种情况下,第一步失败,函数返回0以指示没有更多的排列。


因此,这就是nextPermutation函数。

int nextPermutation( int array[], int length )
{
    int i, j, temp;

    // starting from the end of the array, find the first number (call it 'x')
    // that is followed by a larger number
    for ( i = length - 2; i >= 0; i-- )
        if ( array[i] < array[i+1] )
            break;

    // if no such number was found (all the number are in reverse order)
    // then there are no more permutations
    if ( i < 0 )
        return 0;

    // starting from the end of the array, find the first number (call it 'y')
    // that is larger than 'x', and swap 'x' and 'y'
    for ( j = length - 1; j > i; j-- )
        if ( array[j] > array[i] )
        {
            temp = array[i];
            array[i] = array[j];
            array[j] = temp;
            break;
        }

    // 'y' is now where 'x' was, and all of the numbers to the right of 'y'
    // are in descending order, swap them so that they are in ascending order
    for ( i++, j = length - 1; j > i; i++, j-- )
    {
        temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    return 1;
}

注意,nextPermutation函数适用于任何数字数组(数字不需要连续)。例如,如果起始数组为

int array[] = { 2, 3, 7, 9 };

如果调用nextPermutation函数,它将找到由2、3、7和9组成的所有排列。


为了完整起见,这里是在main函数中使用的arrayToInt函数。该函数仅用于演示目的。它假设数组仅包含单个数字,并且不会检查溢出。对于一个9位数来说,只要int至少为32位,它就能工作。

int arrayToInt( int array[], int length )
{
    int result = 0;
    for ( int i = 0; i < length; i++ )
        result = result * 10 + array[i];
    return result;
}
由于这个算法的性能似乎引起了一些关注,以下是一些数字: length =2 perms =2 (swaps =1 ratio =0.500) time =0.000msec length =3 perms =6 (swaps =7 ratio =1.167) time =0.000msec length =4 perms =24 (swaps =34 ratio =1.417) time =0.000msec length =5 perms =120 (swaps =182 ratio =1.517) time =0.001msec length =6 perms =720 (swaps =1107 ratio =1.538) time =0.004msec length =7 perms =5040 (swaps =7773 ratio =1.542) time =0.025msec length =8 perms =40320 (swaps =62212 ratio =1.543) time =0.198msec length =9 perms =362880 (swaps =559948 ratio =1.543) time =1.782msec length =10 perms =3628800 (swaps =5599525 ratio =1.543) time =16.031msec length =11 perms =39916800 (swaps =61594835 ratio =1.543) time =170.862msec length =12 perms =479001600 (swaps =739138086 ratio =1.543) time =2036.578msec
测试用的 CPU 是 2.5Ghz 的 Intel i5 处理器。该算法每秒生成约 2 亿个排列,并且在不到 2 毫秒的时间内生成 9 个数字的所有排列。
有趣的是,平均来说,该算法每个排列只需要约 1.5 次交换。有一半的时间,该算法只是交换数组中的最后两个数字。在 24 种情况中的 11 种情况下,该算法进行了两次交换。因此,只有在 24 种情况中的 1 种情况下,该算法需要进行超过两次的交换。
最后,我尝试使用以下两个数组来运行该算法:
int array[] = { 1, 2, 2, 3 };          // generates 12 permutations
int array[] = { 1, 2, 2, 3, 3, 3, 4 }; // generates 420 permutations
排列的数量如预期般,输出结果似乎正确,因此看起来该算法在数字不唯一时也有效。

1
@GlennTeitelbaum,你没有非递归解决方案。不过我很想测试一下。请写一个公共原型版本:(int array[], int length) - this
@这个版本在我的测试中比我迭代的版本快得多,也比你要求的接口快得多。因为它是展开的,整个函数可以被分析,不像真正的递归那样灵活,这使它在速度上更快,但代价是灵活性较差。 - Glenn Teitelbaum
@GlennTeitelbaum:有一个有趣的序列,以0 1 0 1 0 2 0 1 0 1 0 2 0 1 0 1 0 3 0 1 0 1 0 2 0 1 0 1 0 2 0 1 0 1 0 3 0 1 0 1 0 2 0 1 0 2 0 1 0 1 0 3 0 1 0 1 0 2 0 1 0 2 0 1 0 3 0 1 0 1 0 3 0 2 0 1 0 3 0 1 0 1 0 3 0 2 0 3 0 1 0 1 0 2 0 1 0 2 0 1 0 1 0 2 0 1 0 1 0 2 0 3 0 2 0 1 0 1 0 4开始。对于一个N个字符的字符串,如果执行该序列指示的前N!-1次交换,则可以得到所有排列。0=交换第一对,1=交换第二对,2=交换第三对,以此类推。这些对始终是连续的,并且第一对每隔一段时间就会交换。【续】 - Nominal Animal
1
@GlennTeitelbaum:该序列允许对任何字符串进行常数时间的排列,而无需递归。您只需要按照该序列的顺序执行成对交换即可。(因此,每个排列仅需要一次交换,因此是常数时间。生成所有排列当然是O(N!),需要N!-1次交换。)我不知道这个序列是否有名称。 - Nominal Animal
@NominalAnimal 听起来类似于德布鲁因序列。 - Glenn Teitelbaum
显示剩余15条评论

12

递归在这里起到了很好的作用。

#include <stdio.h>

void uniq_digits(int places, int prefix, int mask) {
  if (!places) {
    printf("%d\n", prefix);
    return;
  }
  for (int i = 0; i < 10; i++) {
    if (prefix==0 && i==0) continue;
    if ((1<<i)&mask) continue;
    uniq_digits(places-1, prefix*10+i, mask|(1<<i)); 
  }
}

int main(int argc, char**argv) {
  uniq_digits(9, 0, 0);
  return 0;
}

请注意一些优化 https://dev59.com/ClwZ5IYBdhLWcg3wINPQ#31928246,但它确实失去了这个解决方案的简洁性。 - Glenn Teitelbaum

8

这是一个简单的程序,可以打印出一组字符的所有排列组合。您可以轻松地将其转换为生成所需的所有数字:

#include <stdio.h>

static int step(const char *str, int n, const char *set) {
    char buf[n + 2];
    int i, j, count;

    if (*set) {
        /* insert the first character from `set` in all possible
         * positions in string `str` and recurse for the next
         * character.
         */
        for (count = 0, i = n; i >= 0; i--) {
            for (j = 0; j < i; j++)
                buf[j] = str[j];
            buf[j++] = *set;
            for (; j <= n; j++)
                buf[j] = str[j - 1];
            buf[j] = '\0';
            count += step(buf, n + 1, set + 1);
        }
    } else {
        printf("%s\n", str);
        count = 1;
    }
    return count;
}

int main(int argc, char **argv) {
    int total = step("", 0, argc > 1 ? argv[1] : "123456789");
    printf("%d combinations\n", total);
    return 0;
}

它使用递归而不是位掩码,并且可以用于任何字符集。它还计算排列数,因此您可以验证它对于n个字符的集合会产生阶乘(n)个排列。


7
这里有许多冗长的代码。最好再深思熟虑,在编码时减少代码量。
我们希望在发射每个数字时仅需固定的努力,确保每种可能性只生成一次且不浪费资源。
如果没有代码,你会怎么做呢?拿10张卡片,在上面写上数字0到9。在桌子上画出一个9个方格的行。选择一张卡片,将其放在第一个方格中,另一张放在第二个方格中,以此类推。当你选择了9张卡片,你就得到了第一个数字。现在移除最后一张卡片,并用每个可能的替代品替换它。(在这种情况下只有1次。)每当所有方格都被填满时,你就有了另一个数字。当你为最后一个方格完成所有替代品时,为最后两个格重复此过程。继续进行,直到你考虑完所有盒子的所有替代品。
要写一个简洁的程序来完成这个任务,就需要选择简单的数据结构。使用字符数组表示9个方格的行。
用另一个数组表示卡片集。要从大小为N的数组A [0..N-1]中删除元素,可以使用一个老技巧。假设要删除的元素是A[I]。将A[I]的值保存在一边,然后将最后一个元素A[N-1]“向下”复制,覆盖A[I]。新的集合是A [0..N-2]。这个技巧可以很好地工作,因为在集合中我们不关心顺序。
余下的任务就是使用递归思维枚举所有可能的选择。如果已知如何从大小为M的字符集中选择所有字符串大小为N的选择,那么要获得算法,只需为第一个字符串位置选择每个可能的字符,然后递归选择剩余大小为M-1的集合中的其余N-1个字符。我们得到了一个简洁的12行函数:
#include <stdio.h>

// Select each element from the given set into buf[pos], then recur
// to select the rest into pos+1... until the buffer is full, when
// we print it.
void select(char *buf, int pos, int len, char *set, int n_elts) {
  if (pos >= len)
    printf("%.*s\n", len, buf);  // print the full buffer
  else
    for (int i = 0; i < n_elts; i++) {
      buf[pos] = set[i];         // select set[i] into buf[pos]
      set[i] = set[n_elts - 1];  // remove set[i] from the set
      select(buf, pos + 1, len, set, n_elts - 1); // recur to pick the rest
      set[n_elts - 1] = set[i];  // undo for next iteration
      set[i] = buf[pos];
    }
}

int main(void) {
  char buf[9], set[] = "0123456789";
  select(buf, 0, 9, set, 10); // select 9 characters from a set of 10
  return 0;
}

您没有提到在第一个位置放零是否可以接受。假设不可以。由于我们很好地理解了算法,因此很容易避免选择零作为第一个位置。只需观察到在C中!pos的值为1,如果pos为0和0。如果您不喜欢这种略微晦涩的习语,请尝试(pos == 0 ? 1 : 0)作为更易读的替代:

#include <stdio.h>

void select(char *buf, int pos, int len, char *set, int n_elts) {
  if (pos >= len)
    printf("%.*s\n", len, buf);
  else
    for (int i = !pos; i < n_elts; i++) {
      buf[pos] = set[i];
      set[i] = set[n_elts - 1];
      select(buf, pos + 1, len, set, n_elts - 1);
      set[n_elts - 1] = set[i];
      set[i] = buf[pos];
    }
}

int main(void) {
  char buf[9], set[] = "0123456789";
  select(buf, 0, 9, set, 10);
  return 0;
}

6

与其使用10个变量,我会创建一个单一的变量,并在其中设置(和可测试)每个数字对应的位。然后你只需要使用一个循环来设置(和测试)与每个数字对应的位。代码如下:

int ok = 1;
unsigned bits = 0;
int digit;
unsigned powers10 = 1;
for (digit = 0; digit < 10; ++digit) {
    unsigned bit = 1 << ((num / powers10) % 10);
    if ((bits & bit) != 0) {
        ok = 0;
        break;
    }
    bits |= bit;
    powers10 *= 10;
}
if (ok) {
    printf("%d\n", num);
}

完整程序(丢弃不必要的#include行):
#include <stdio.h>

int main(void)
{
    int indx;
    int num;

    for(indx = 123456789; indx <= 987654321; indx++)
    {
        num = indx;
        int ok = 1;
        unsigned bits = 0;
        int digit;
        unsigned powers10 = 1;
        for (digit = 0; digit < 9; ++digit) {
            unsigned bit = 1 << ((num / powers10) % 10);
            if ((bit == 1) || ((bits & bit) != 0)) {
                ok = 0;
                break;
            }
            bits |= bit;
            powers10 *= 10;
        }
        if (ok) {
            printf("%d\n", num);
        }
    }
    return 0;
}

当我离开工作时,OP澄清了他的问题,我没有关注到所要求的零的数量不足。现在已更新回答,这将产生预期的362880种组合。

然而 - 有一条评论说其中一个答案最快,这促使我们进行跟进。在一个快速检查中:

  • @Paul Hankin的答案(计算零并给出3265920个组合):
    real    0m0.951s
    user    0m0.894s
    sys     0m0.056s
  • 这个答案:
    real    0m49.108s
    user    0m49.041s
    sys     0m0.031s
     real    1m27.597s
     user    1m27.476s
     sys     0m0.051s

bit的赋值映射为值1、2、4、8等,根据digit(0、1、2、3等)的值。有人可能会展示更简洁的例子,但这种方法在使用C进行系统编程时非常常见。 - Thomas Dickey
可以帮忙测试一下我的第二个解决方案(在EDIT之后,完全展开的那个)时间上是否与你的测试结果相符。独立验证是有意义的:https://dev59.com/ClwZ5IYBdhLWcg3wINPQ#31928246 - Glenn Teitelbaum
@ThomasDickey 不错!你测试了我的哪个答案?是掩码(mask)的那个,check1() 还是数组(array)的那个,check2()?能否请你都测试一下吗?我有一种感觉知道哪一个会更快,但最好还是确认一下。 - George André
@ThomasDickey:您能否将我的解决方案添加到名人堂中?我认为它非常快。 - wildplasser

6
您可以使用掩码来设置标志,这些标志表示数字是否已在数字中出现。像这样:
int mask = 0x0, j;

for(j= 1; j<=9; j++){
    if(mask & 1<<(input%10))
        return 0;
    else
        mask |= 1<<(input%10);
    input /= 10;
}
return !(mask & 1);

完整的程序:
    #include <stdio.h>

int check(int input)
{
    int mask = 0x0, j;

    for(j= 1; j<=9; j++){
        if(mask & 1<<(input%10))
            return 0;
        else
            mask |= 1<<(input%10);
        input /= 10;
    }
    /* At this point all digits are unique
     We're not interested in zero, though */
    return !(mask & 1);
}

int main()
{
    int indx;
    for( indx = 123456789; indx <=987654321; indx++){
        if( check(indx) )
            printf("%d\n",indx);
    }
}

编辑...

或者您可以使用数组来执行相同的操作:

int check2(int input)
{
    int j, arr[10] = {0,0,0,0,0,0,0,0,0,0};

    for(j=1; j<=9; j++) {
        if( (arr[input%10]++) || (input%10 == 0) )
            return 0;
        input /= 10;
    }
    return 1;
}

我真的不知道该如何处理位(bit),因为我从未学过。还有其他方法可以解决这个问题吗? - Jack Swanson
@RichardFlores 对不起,这是最快的方法且需要较少的代码。这是一个关于and、or和位操作的好例子。我建议在你学习这些领域时回到这个例子。理论上,您可以用整数数组替换掩码,得到相同的结果。 - George André
@RichardFlores 我已经按照你的要求更新了我的答案,改为使用数组示例。 :) - George André
@RichardFlores 结果数组版本稍微快一点... :) - George André
谢谢。这背后的逻辑是什么? - Jack Swanson
@RichardFlores 数组中有10个元素,用于存储数字中某个特定数字出现的次数。它可以是0次或多次。我们只关心数字出现超过一次的情况。如果是这样,那么原始数字就不是唯一的,对吧?所以我们会终止循环。顺便说一下,循环会遍历原始数字的所有数字(9个位置,因为您想要9位数字)。 还有一个检查,看看我们现在正在查看的数字是否为0,因为我们不希望结果中有数字0,所以我们会提前终止。 - George André

6

以下是一种方法 - 首先创建一个唯一数字的数组,然后随机打乱它们:

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

int main( void )
{
  char digits[] = "123456789";

  srand( time( NULL ) );

  size_t i = sizeof digits - 1;
  while( i )
  {
    size_t j = rand() % i;
    char tmp = digits[--i];
    digits[i] = digits[j];
    digits[j] = tmp;
  }

  printf( "number is %s\n", digits );
  return 0;
}

一些示例输出:

john@marvin:~/Development/snippets$ ./nine
number is 249316578
john@marvin:~/Development/snippets$ ./nine
number is 928751643
john@marvin:~/Development/snippets$ ./nine
number is 621754893
john@marvin:~/Development/snippets$ ./nine
number is 317529864

请注意,这些是唯一十进制数字的“字符字符串”,而不是数值;如果您想要相应的整数值,您需要进行转换,例如:
long val = strtol( digits, NULL, 10 );

6

请检查这段代码。

    #include<stdio.h>

    //it can be done by recursion

    void func(int *flag, int *num, int n){  //take 'n' to count the number of digits
        int i;
        if(n==9){                           //if n=9 then print the number
            for(i=0;i<n;i++)
                printf("%d",num[i]);
            printf("\n");
        }
        for(i=1;i<=9;i++){

            //put the digits into the array one by one and send if for next level

            if(flag[i-1]==0){
                num[n]=i;
                flag[i-1]=1;
                func(flag,num,n+1);
                flag[i-1]=0;
            }
        }
    }

    //here is the MAIN function
    main(){

        int i,flag[9],num[9];
        for(i=0;i<9;i++)        //take a flag to avoid repetition of digits in a number
            flag[i]=0;          //initialize the flags with 0

        func(flag,num,0);       //call the function

        return 0;
    }

如果您有任何问题,请随时提问。

6
我推荐Nominal Animal的答案,但是如果您只是为了打印这个值而生成它,那么您可以节省一些工作,并同时使用相同的方法得到一个更通用的例程:
char *shuffle( char *digit, int digits, int count, unsigned int seed )
{
    //optional: do some validation on digit string
    //  ASSERT(digits == strlen(digit));
    //optional: validate seed value is reasonable
    //  for(unsigned int badseed=1, x=digits, y=count; y > 0; x--, y--)
    //      badseed *= x;
    //  ASSERT(seed < badseed);

    char *work = digit;
    while(count--)
    {
        int i = seed % digits;
        seed /= digits--;
        unsigned char selectedDigit = work[i];
        work[i] = work[0];
        work[0] = selectedDigit;
        work++;
    }
    work[0] = 0;

    //seed should be zero here, else the seed contained extra information
    return digit;
}

这个方法会破坏传入的数字,实际上这些数字不一定是数值型的,也不必唯一。

如果你希望生成的输出值按照递增顺序排序,需要进行一些额外的工作:

char *shuffle_ordered( char *digit, int digits, int count, unsigned int seed )
{
    char *work = digit;
    int doneDigits = 0; 
    while(doneDigits < count)
    {
        int i = seed % digits;
        seed /= digits--;
        unsigned char selectedDigit = work[i];
        //move completed digits plus digits preceeding selectedDigit over one place
        memmove(digit+1,digit,doneDigits+i);
        digit[0] = selectedDigit;
        work++;
    }
    work[0] = 0;

    //seed should be zero here, else the seed contained extra information
    return digit;
}

无论哪种情况,都是这样调用的:
for(unsigned int seed = 0; seed < 16*15*14; ++seed)
{
    char work[] = "0123456789ABCDEF";
    printf("seed=%d -> %s\n",shuffle_ordered(work,16,3,seed));
}

这应该打印出一个有序列表,其中包含没有重复数字的三位十六进制值:

seed 0 -> 012
seed 1 -> 013
...
seed 3358 -> FEC
seed 3359 -> FED

我不知道你实际上是在做什么,用这些精心制作的数字序列。如果一些可怜的维护工程师要跟在你后面修复一些错误,我建议使用有序版本,因为对于人类来说,从种子转换成序列值要容易得多。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接