这是涉及combinatorics 的一个非常典型的问题示例。
正好有 9⋅8⋅7⋅6⋅5⋅4⋅3⋅2⋅1 = 9! = 362880 个九位十进制数,其中每个数字只出现一次,根本不使用零。这是因为第一个数字有九种可能性,第二个数字有八种可能性,依此类推,因为每个数字只使用一次。
因此,您可以轻松编写一个函数,接收 seed, 0 ≤ seed
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:如果您想要 indexth 组合(例如,按字典顺序),您可以使用以下方法:
#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 将是具有indexth 最小值的排列。例如,如果digits="1234" 和length=4,那么index=0 将产生buffer="1234",index=1 将产生buffer="1243",依此类推,直到index=23 将产生buffer="4321"。
上面的实现绝对没有以任何方式优化。初始循环是计算阶乘,带有溢出检测。避免这种情况的一种方法是使用临时size_t [length] 数组,并从右到左填充它,类似于上面的unique9();那么,性能应该类似于上面的unique9(),除了memmove()s 这需要(而不是交换)。
这种方法是通用的。例如,如果您想创建 N 个字符的单词,其中每个字符都是唯一的,并且/或者只使用特定字符,那么相同的方法将产生一个有效的解决方案。
首先,将任务拆分为多个步骤。
上面,digit[] 数组中还有 n 未使用的数字,我们可以使用 seed 选择下一个未使用的数字。
如果
seed 除以
n,
i = seed % n; 将i 设置为余数(模数)。因此,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 数组中的下一个位置,并增加该位置。 (要将其变成字符串,请记住在最后放置一个字符串结尾的 nul 字符 '\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; /* seed = seed / n; */
str[len++] = letter[i];
letter[i] = letter[--n];
}
str[len] = '\0';
return str;
}
用str 指向至少有len 个字符的字符数组调用函数,seed 是标识组合的数字,它将用最多26 个字符串填充str或len-1 字符(以较少者为准),其中每个小写字母最多出现一次。
如果您觉得该方法不清楚,请询问:我非常想尝试澄清一下。
您会发现,使用低效的算法会损失大量资源(不仅是电力,还包括人类用户的时间),这只是因为编写缓慢、低效的代码更容易,而不是真正有效地解决手头的问题。我们这样浪费金钱和时间。当 正确 解决方案像这种情况一样简单时——就像我说的那样,这会扩展到大量的组合问题——我宁愿看到程序员花 15 分钟学习它,并在有用的时候应用它,而不是看到浪费的传播和扩展。
许多答案和 cmets 都围绕生成所有这些组合(并计算它们)。我个人认为这并没有多大用处,因为该系列已经众所周知。在实践中,您通常希望生成例如小子集——对、三胞胎或更大的集合——或满足某些标准的子集;例如,您可能希望生成十对这样的数字,每个九位数字使用两次,但不是一对。我的种子方法很容易做到这一点;而不是十进制表示,而是使用连续的种子值(0 到 362879,包括在内)。
也就是说,在 C 中生成(并打印)给定字符串的所有 permutations 很简单:
#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)=n⋅a(n-1)+1(序列A002627 ),在这种特殊情况下调用 623530。一般来说,a(n)≤(1-e)n!,即a(n)n!,所以调用次数为O(N!) ,关于置换的项目数的阶乘。与调用相比,循环体的迭代次数减少了 623529 次。
逻辑相当简单,使用与第一个代码 sn-p 中相同的数组方法,只是这次数组的“修剪掉”部分实际上用于存储置换后的字符串。换句话说,我们将每个剩余的字符与下一个要修剪的字符交换(或附加到最终字符串),进行递归调用,并恢复这两个字符。因为每次递归调用后都会撤消每次修改,所以缓冲区中的字符串在调用后与之前相同。就好像它从一开始就没有被修改过一样。
上面的实现确实假设一个字节的字符(并且不能正确使用例如多字节 UTF-8 序列)。如果要使用 Unicode 字符或某些其他多字节字符集中的字符,则应使用宽字符。除了类型改变,改变打印字符串的函数,其他的都不需要改变。