繁体   English   中英

如何用递归替换循环

[英]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的数组,循环索引abcde ,变量keysalthash以及库调用crypt

我注意到存在终止条件。 如果密文等于要通过蛮力搜索解密的哈希,程序将打印当前密钥并退出。 这些都是不能在纯函数中出现的副作用,尽管我们可以将它们合并,但实际上我会做的就是返回Just keyNothing如果没有匹配项)。 如果key的类型名为Key ,则返回类型为Maybe Key

参数ae分别从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 ,但是您需要递归解决方案,因此让我们编写一个辅助函数。 递归助手的常规名称是gobruteForce' 我会去go ,因为它是短。 由于它是一个嵌套的局部函数,因此可以引用参数salthash 稍后,我将在使用它的函数内部移动整个键空间的列表的定义:

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的值。 在这种情况下, xKeysaltSalt ,因此必须有一些使用KeySalt并返回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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM