这是一个典型的组合数学问题。
有正好 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
表示long
,UL
表示unsigned long
,如果编译器支持它们,则为LL
表示long long
,ULL
表示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
仍大于零,我们可以通过重复这个过程来添加另一个数字。
如果我们不想使用所有数字,我们可以使用单独的计数器(或将
n
与
n-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;
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 {
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)=n⋅a(n-1)+1(序列A002627),在这种特定情况下调用了623530次。一般来说,a(n)≤(1-e)n!,即a(n)<1.7183n!,因此调用次数是O(N!),与排列项数的阶乘有关。循环体与调用次数相比少迭代一次,在这里循环体迭代了623529次。
这里的逻辑非常简单,与第一个代码片段中使用相同的数组方法,只是这一次“削减掉”的数组部分实际上用于存储排列后的字符串。换句话说,我们将每个仍然存在的字符与下一个要被削减掉的字符交换(或添加到最终字符串中),进行递归调用,并恢复这两个字符。因为每次修改都会在递归调用后撤销,所以缓冲区中的字符串在调用后与之前相同,就好像它从未被修改过一样。
上述实现假设使用单字节字符(并且不能正确处理多字节UTF-8序列等)。如果要使用Unicode字符或其他多字节字符集中的字符,则应改用宽字符。除了类型更改和将函数更改为打印字符串外,不需要进行其他更改。
d1
,d2
等等时,这应该是将它们转换为数组的重要提示。 - M Oehm