如何创建任意数量的嵌套循环?

3

我正在尝试编写一个程序,在匹配特定字符串之前测试每个可能的字符串,并给出尝试的次数。以这个例子为例:

a = {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', 5: 'f', 6: 'g', 7: 'h', 8: 'i', 9: 'j', 10: 'k', 11: 'l', 12: 'm',
     13: 'n', 14: 'o', 15: 'p', 16: 'q', 17: 'r', 18: 's', 19: 't', 20: 'u', 21: 'v', 22: 'w', 23: 'x', 24: 'y',
     25: 'z'}

word = 'bike'

found = False
counter = 0
for i in range(len(a)):
    for j in range(len(a)):
        for k in range(len(a)):
            for m in range(len(a)):
                string = f'{a[i]}{a[j]}{a[k]}{a[m]}'
                counter += 1
                print(string, counter)
                if string == word:
                    found = True
                    break
            if found:
                break
        if found:
            break
    if found:
        break


输出结果大致如下:
aaaa 1
aaab 2
aaac 3
aaad 4
aaae 5
aaaf 6
...
bijz 23244
bika 23245
bikb 23246
bikc 23247
bikd 23248
bike 23249

如果你知道这个单词总是四个字符,那么可以使用此代码,但如果长度未知怎么办?当长度未知时,应该如何创建这个代码?我在想是否有一些递归函数可以实现这一点,但一切都没有进展。我正在尝试实现的程序将产生以下输出:

a 1
b 2
c 3
...
aa 27
ab 28
ac 29
...
ba #
bb #
bc #
...
aaa #
aab #
aac #
3个回答

2
注意:本答案中所有函数的返回值都是23248,因为我喜欢从0开始计数。如果您更喜欢从1开始计数,并且想要得到23249作为答案,则只需在函数中添加+1即可。
第一种方法:编写自己的increment_word函数:
您可以编写一个计算下一个单词的函数来迭代单词。例如,bikd之后的下一个单词应该是bike,cdzz之后的下一个单词应该是ceaa。
由于Python中的字符串是不可变的,因此为了方便起见,我们将使用字符列表而不是字符串。
def increment_word(w):
  i = len(w) - 1
  while (w[i] == 'z'):
    w[i] = 'a'
    i = i - 1
  w[i] = chr(ord(w[i]) + 1)

请注意,只有在调用至少包含一个非“z”字母的单词时,才保证该函数能正常工作。用户需要自行避免在 'zzz' 后请求下一个单词。
现在我们可以解决你的问题:
def find_word(w):
  candidate = ['a' for _ in w]
  w = list(w)
  count_attempts = 0
  while candidate != w:
    increment_word(candidate)
    count_attempts += 1
  return count_attempts

第二种方法:使用 itertools.product

通常情况下,在Python中需要迭代复杂的数据结构时,已经有人编写了你需要的循环结构,并且它在 itertools 包中。如果没有,那么可能在 itertools recipes 或者 more_itertools 中。

在这种情况下,你可以使用 itertools.product

from string import ascii_lowercase
from itertools import product

def find_word(w):
  for count_attempts, candidate in enumerate(product(*[ascii_lowercase]*len(w))):
    if all(x == y for x,y in zip(w, candidate)):
      return count_attempts

请注意,我们使用的是 string.ascii_lowercase 而不是自己打出整个字母表。有人很好心地教 Python 字母表。我们没有必要过度热情,重新编写字母表(有遗漏导致一切都崩溃的风险)。
第三个想法:使用递归而不是迭代
任何复杂的循环都可以使用递归来模拟。请注意,Python 对于递归来说是一种相当糟糕的语言——特别是,在 Python 中,递归函数 tend to be pretty slow,而且如果递归太深,程序可能会崩溃,因为 Python 不会优化尾调用。但是,如果您需要使用其他语言,您应该考虑这个选项。
def find_word(word):
  return find_word_aux(word, 'a'*len(word), 0)

def find_word_aux(word, candidate, count_attempts):
  if candidate == word:
    return count_attempts
  else:
    i,c = max((i,c) for i,c in enumerate(candidate) if c != 'z')
    return find_word_aux(word, candidate[:i] + chr(ord(c)+1) + 'a'*(len(word)-i-1), count_attempts + 1)

请注意,这最终与increment_word版本非常相似。不幸的是,在我的机器上使用默认python参数,它仅适用于aaabmg之间的单词。对于bmh之后的任何单词,它都会崩溃并显示异常RecursionError: maximum recursion depth exceeded in comparison。如果您将此代码翻译为优化尾调用的语言(如OCaml、Haskell或C),则它将适用于任何单词。
第四个想法:使用组合数学来立即解决问题
不必一个一个地迭代单词,您可以尝试使用乘法计数批次单词。例如,很容易看出有:
  • 26 * 26 * 26 = 26^3 = 17576 个单词在aaaaazzz之间;
  • 8 * 26 * 26 = 5408 个单词在baaabhzz之间;
  • 10 * 26 = 260 个单词在biaabijz之间;
  • 5 个单词在bikabike之间。

总计:在aaaabike之间共有23249个单词。

这给我们提供了一个Python程序:
def find_word(w):
  count_attempts = 0
  for i,c in enumerate(w):
    n = ord(c) - ord('a')
    count_attempts += n * 26**(len(w) - i - 1)
  return count_attempts

请注意,此处的for循环是针对w的字符而不是所有可能的单词进行的;因此我们只迭代4次而不是23249次。 这个函数比其他版本要快得多。

很好的回答。我想评论一下,递归本身并不慢,递归程序可以做到堆栈安全。如果你感兴趣,我在这个主题上有一些帖子(https://stackoverflow.com/search?q=user%3A633183+stack+safe),还有一些专门针对Python的(https://stackoverflow.com/search?q=user%3A633183+stack+safe+%5Bpython%5D)。 - Mulan
而且,我认为任何人都会喜欢Python的尾递归技术 - Mulan

2

这应该能够实现;您需要在productrepeat参数中输入长度,而enumerate对单词进行枚举(默认从0开始;根据您的需求,您可以添加start=1):

from itertools import product


search = "bike"

for i, item in enumerate(product(a.values(), repeat=len(search))):
     word = "".join(item)
     print(word, i)
     if word == search:
          break

它的输出结果为:

...
bikb 23245
bikc 23246
bikd 23247
bike 23248

如果你只对这个数字感兴趣,你可以将单词转化为26进制的数字,然后使用 int 函数进行转换:
alpha_to_digits = str.maketrans(
    "abcdefghijklmnopqrstuvwxyz", "0123456789abcdefghijklmnop")

word = 'bike'

d = word.translate(alpha_to_digits)
print(d, int(d, base=26))
# 18a4 23248

这段代码将单词 bike 转换为其在26进制下的数字表示(18a4),然后可以将其转换为整数(23248)。

封装成一个函数:

def number(word):
    return int(word.translate(alpha_to_digits), base=26)

谢谢。这已经足够让我开始得到我想要的输出了,但我做这个是为了学习经验。如果有一种使用递归函数来实现这个的方法,我会很高兴知道。 - Gabe Morris
文档(https://docs.python.org/3/library/itertools.html#itertools.product)或多或少地告诉你它是如何实现的。虽然它不是递归的... - hiro protagonist
1
@GabeMorris 我在我的回答中包含了一个递归版本,尽管在Python中我不推荐使用它。 - Stef
@Stef Python来说这是否太过了? - Gabe Morris
1
@GabeMorris:函数调用是Python中最慢的操作之一。此外,Python不会对尾递归进行优化,这意味着每次递归调用时调用栈都会变得越来越大,即使你像return recursive_call()这样编写代码。因此,在Python中,递归函数比迭代函数更慢,占用更多内存,并且无法处理大量输入。 - Stef

2
您可以使用 itertools.product(用于嵌套的 for 循环)+ dict.values(直接循环字母)+ enumerate(从 1 开始,以获取计数器):
from itertools import product

word = 'bike'

for counter, letters in enumerate(product(a.values(), repeat = len(word)), 1):
  string = ''.join(letters)
  print(string, counter)
  if string == word:
      break

输出:

...
...
...
bika 23245
bikb 23246
bikc 23247
bikd 23248
bike 23249

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