[英]How to Replace For Loops with Recursion
我才刚刚开始递归编程-既然我听说这对解决问题有多么强大,所以我想尝试一下我几天前编写的简单解密算法。
我知道可能很难计算出每个迭代在做什么,但是递归可以使这个循环更“优雅”和“算法”吗?
for (int e = 0; e < length; e++)
{
for (int d = 0; d < length; d++)
{
for (int c = 0; c < length; c++)
{
for (int b = 0; b < length; b++)
{
for (int a = 1; a < length; a++)
{
key[0] = letters[a];
key[1] = letters[b];
key[2] = letters[c];
key[3] = letters[d];
key[4] = letters[e];
if (strcmp(crypt(key, salt), hash) == 0)
{
printf("%s\n", key);
return 0;
}
}
}
}
}
}
如果您可以不递归地完成任务,那么以这种方式解决它是个好主意。 如果您想了解递归,请查看一些问题,例如阶乘或斐波那契。 这些还具有迭代解决方案,但比您在此处遇到的问题更适合于递归。 在这种情况下,很清楚您的算法在做什么,递归将使它变得不必要地难以理解。 这是您可以做的一项改进
for (int e = 0; e < length; e++)
{
key[4] = letters[e];
for (int d = 0; d < length; d++)
{
key[3] = letters[d];
for (int c = 0; c < length; c++)
{
key[2] = letters[c];
for (int b = 0; b < length; b++)
{
key[1] = letters[b];
for (int a = 1; a < length; a++)
{
key[0] = letters[a];
if (strcmp(crypt(key, salt), hash) == 0)
{
printf("%s\n", key);
return 0;
}
}
}
}
}
}
尽管在本示例中,我并不反对阻止您使用递归的每个人,但是我想递归地编写它,因为我认为这是一个合理的问题。
这是我递归编写的尝试。 这样,我只需要编写一次循环,因为外部循环是由递归处理的。 我采取了一些自由措施,因此它并不完全等同于您的代码,但我认为原则上是相同的(针对hash
测试所有组合),并显示了如何递归编写此代码的基本思想。 我假设您有一种知道strcmp
检查安全的方法。
int recur(int cur, int klength, char *key, char *letters, int length, char *salt, char *hash)
{
if (cur == klength)
{
if (strcmp(crypt(key, salt), hash))
{
return 1;
}
else
{
return 0;
}
}
else
{
for (int i = 0; i < length; i++)
{
key[cur] = letters[i];
int j = recur(cur+1, klength, key, letters, length, salt, hash);
if (!j)
{
return 0;
}
}
return 1;
}
}
然后我会这样称呼
recur(5, 0, ...)
做你写的5个循环。 这不是很优雅,但是我认为很清楚为什么如果将密钥扩展为需要10个循环(如果在10000个循环时堆栈会很糟糕),那么这样做可能会更优雅。
话虽如此,我首先想到的不是查看“代码递归”,而是“那些外部循环看起来很相似,因此我想摆脱其中的一些循环”。 我下面的代码不是很漂亮(嘿,它很晚了!),但是我认为原则上,如果您认为可能需要将要测试的字符数增加到10个(或10000个),这将是一个更好的方法。 我想做的是维护一个等于idx
key
的整数。 如果我增加idx[0]
并且它== length
我知道我需要重置idx[0] = 0
并尝试增加idx[1]
, idx[1]
。每次更改idx[i]
我都会对key[i]
进行相同的更改key[i]
。 每次我对idx / key进行新的排列时,我都会进行strcmp
测试,以查看是否找到了正确的IDX /密钥。
int ksize = 5;
int idx[ksize];
for (int i = 0; i < ksize; ++i)
{
idx[i] = 0;
key[i] = letters[0];
}
for (int done = 0; !done; )
{
if (strcmp(crypt(key, salt), hash) == 0)
{
printf("%s\n", key);
return 0;
}
for (int i = 0; ; i++)
{
if (++idx[i] == length)
{
idx[i] = 0;
}
key[i] = letters[idx[i]];
if (idx[i]) // We incremented idx[i] and it wasn't reset to 0, so this is a new combination to try
{
break;
}
else if (i == ksize-1) // We incremented idx[ksize-1] and it was reset to 0, so we've tried all possibilities without returning
{
done++;
break;
}
}
}
在这种情况下,当您不限制嵌套循环的深度时,即在寻找带有字母集合的任意单词时,递归是好的。
因此,以下函数对“字母”集中的单词进行递归搜索,然后返回
这是一个模拟循环的递归函数:
int recursion(int keyIndex, char* key, char letters[], int length, const char *word) {
int depth = strlen(word);
int i;
for (i = 0; i < length; i++) {
key[keyIndex] = letters[i];
if (keyIndex == depth-1) {
if (strncmp(key, word, depth) == 0) {//crypt(key, salt), hash) == 0){
key[depth] = 0;
printf("found: %s\n", key);
return 0;
}
}
else {
int recStatus = recursion(keyIndex+1, key, letters, length, word);
if (recStatus == 0)
return 0;
}
}
return 1;
}
这是相同功能的更好实现。 在您的情况下,它可能不起作用,因为您需要为'crypt'使用完整的最终字符串,但是它可以用于查找简单的单词。
int betterRecursion(int keyIndex, char *letters, int length, const char *word) {
int depth = strlen(word);
int i;
for (i = 0; i < length; i++) {
if (word[keyIndex] == letters[i]) {
if (keyIndex == depth-1) {
printf("found: %s\n", word);
return 0;
}
else
return betterRecursion(keyIndex+1, letters, length, word);
}
}
return 1;
}
当然还有调用它们的主要功能:
int main() {
char key[256];
char *letters = "salt or not";
if(recursion(0, key, letters, strlen(letters), "salt") != 0)
printf("not found\n");
if (betterRecursion(0, letters, strlen(letters), "or not") != 0)
printf("not found\n");
return 0;
}
好吧,让我们尝试将其重写为功能程序。 我将继续使用Haskell,因为它是一个更好的工具,不仅适用于所有工作,而且不适用于所有特定工作。 C并非真正旨在优雅地完成这样的示例。
让我们从内而外开始。 循环的内部内容如下:
key[0] = letters[a];
key[1] = letters[b];
key[2] = letters[c];
key[3] = letters[d];
key[4] = letters[e];
if (strcmp(crypt(key, salt), hash) == 0)
{
printf("%s\n", key);
return 0;
}
这取决于一个名为letters
的数组,循环索引a
, b
, c
, d
和e
,变量key
, salt
和hash
以及库调用crypt
。
我注意到存在终止条件。 如果密文等于要通过蛮力搜索解密的哈希,程序将打印当前密钥并退出。 这些都是不能在纯函数中出现的副作用,尽管我们可以将它们合并,但实际上我会做的就是返回Just key
或Nothing
如果没有匹配项)。 如果key
的类型名为Key
,则返回类型为Maybe Key
。
参数a
到e
分别从0到length - 1
枚举。 在一个功能程序中,我们可以使这五个参数分开,但是我将声明Key
为类型别名(Char, Char, Char, Char, Char)
(五个字符)。
接下来,我们可以定义整个键空间的列表,从('A','A','A','A','A')
然后是('A','A','A','A','B')
到('Z','Z','Z','Z','Z')
为止。 不幸的是,进行[firstKey..lastKey]
类的范围的样板太复杂了,以至于无法用作功能代码的示例,但至少我可以直观地将其编写为列表理解。
allKeys = [(a, b, c, d, e) | a <- ['A'..'Z'],
b <- ['A'..'Z'],
c <- ['A'..'Z'],
d <- ['A'..'Z'],
e <- ['A'..'Z'] ]
请注意,由于Haskell是一种延迟评估的语言,因此它仅计算其实际使用的值。 整个列表不是预先生成的。 实际上,GHC完全可以避免创建链接列表对象。
在循环的迭代之间不变的外部函数参数可能应该是外部函数的参数,我们将其称为bruteForce
。 为了简单起见,我们假定一个无符号的16位盐和一个无符号的64位哈希。 为简单起见,它将蛮力搜索整个键空间,而不是对其进行分区。
import Data.Word (Word16, Word64)
type Salt = Word16
type Hash = Word64
bruteForce :: Salt -> Hash -> Maybe Key
有多种不同的编写bruteForce
,但是您需要递归解决方案,因此让我们编写一个辅助函数。 递归助手的常规名称是go
或bruteForce'
。 我会去go
,因为它是短。 由于它是一个嵌套的局部函数,因此可以引用参数salt
和hash
。 稍后,我将在使用它的函数内部移动整个键空间的列表的定义:
bruteForce :: Salt -> Hash -> Maybe Key
bruteForce salt hash = go allKeys
where
go :: [Key] -> Maybe Key
-- Terminating case: we exhausted the key space without finding a match.
go [] = Nothing
-- Terminating case: we found a match.
go (x:_) | crypt x salt == hash = Just x
-- Tail-recursive case: no match so far.
go (x:xs) = go xs
正如您可能已经注意到的,还剩下一件遗失的物品。 此尾递归函数调用crypt x salt
并将结果与hash
进行比较,如果相等,则返回Just x
的值。 在这种情况下, x
是Key
, salt
是Salt
,因此必须有一些使用Key
和Salt
并返回Hash
crypt
函数。
出于演示目的,我将对每个可能的键/盐对进行简单的枚举,从(AAAAA,0x0000)→0到(ZZZZZ,0xFFFF)→778657857535。
放在一起,我们得到:
module Crack (bruteForce, crypt) where
import Data.Word (Word16, Word64)
type Key = (Char, Char, Char, Char, Char)
type Salt = Word16
type Hash = Word64
bruteForce :: Salt -> Hash -> Maybe Key
bruteForce salt hash = go allKeys
where
allKeys = [(a, b, c, d, e) | a <- ['A'..'Z'],
b <- ['A'..'Z'],
c <- ['A'..'Z'],
d <- ['A'..'Z'],
e <- ['A'..'Z'] ]
go :: [Key] -> Maybe Key
-- Terminating case: we exhausted the key space without finding a match.
go [] = Nothing
-- Terminating case: we found a match.
go (x:_) | crypt x salt == hash = Just x
-- Tail-recursive case: no match so far.
go (x:xs) = go xs
crypt :: Key -> Salt -> Hash
crypt (a, b, c, d, e) salt = let
a' = fromLetter a
b' = fromLetter b
c' = fromLetter c
d' = fromLetter d
e' = fromLetter e
fromLetter x = fromIntegral ( fromEnum x -
fromEnum 'A' )
in
(((((a'*26 + b')*26 + c')*26 + d')*26) + e')*65536 +
(fromIntegral salt)
当您记住只有bruteForce
下的bruteForce
与您编写的代码示例相对bruteForce
,我们看到该代码非常简单且相当快。
因此,进行了一些快速测试。 如果我们的哈希为0x48080,则最后16位0x8080就是我们的盐。 (切勿编写这样的加密哈希函数!)其余的位0x4表示密钥编号4,其中零为AAAAA,即AAAAE。 在REPL中对此进行测试:
*Crack> bruteForce 0x8080 0x48080
Just ('A','A','A','A','E')
检查往返转换:
*Crack> crypt ('N','I','F','T','Y') 0xCEED
398799326957
*Crack> bruteForce 0xCEED 398799326957
Just ('N','I','F','T','Y')
评论之一反对我使用的语言不是C。现在,我喜欢C,但是坦率地说,它不是完成这项工作的正确工具。 您的尾递归函数将具有七个或八个参数以及许多特殊情况(或者使用全局变量和循环来“欺骗”)。
如果我想高效且简洁地在类似C的语言中使用这种功能习语,我将使用LINQ用C#编写它,并可能yield return
。 这是我之前发布的将C中的迭代代码转换为C#中的准功能代码的示例 。 这是运行速度的基准 。
AC#的实现可能非常相似:将整个键空间枚举为异步序列,然后对其进行扫描以查找与给定的盐匹配的哈希值的第一个键。
您可以更改上面的Haskell程序来做到这一点,并将bruteForce
减少为一行,即高阶函数调用:
import Data.List (find)
bruteForce2 salt hash = find (\x -> crypt x salt == hash) allKeys
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.