如何用递归替换循环

时间:2018-09-29 03:04:19

标签: c algorithm loops recursion iteration

我才刚开始进行递归编程-既然我已经听说过它对于解决问题有多么强大,我想尝试一下我几天前编写的简单解密算法。< / p>

我知道可能很难计算出每个迭代在做什么,但是递归可以使这个循环更“优雅”和“算法”吗?

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;
                    }
                }
            }
        }
    }
}

4 个答案:

答案 0 :(得分:6)

如果无需递归就可以完成任务,那么以这种方式解决它是一个好主意。如果您想了解递归,请查看一些问题,例如阶乘或斐波那契。这些还具有迭代解决方案,但比您在此处遇到的问题更适合于递归。在这种情况下,很清楚您的算法在做什么,递归将使它变得不必要地难以理解。这是您可以做出的一项改进

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;
                    }
                }
            }
        }
    }
} 

答案 1 :(得分:1)

尽管我不同意在本示例中不鼓励您使用递归的每个人,但由于我认为这是一个合理的问题,因此我想递归地编写它。

这是我递归编写的尝试。这样,我只需要编写一次循环,因为外部循环是由递归处理的。我采取了一些自由措施,因此它并不完全等同于您的代码,但我认为原则上是相同的(针对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个),这将是一个更好的方法。我想做的是维护一个等于keyidx的整数。如果我递增idx[0]且它是== length,我知道我需要重设idx[0] = 0并尝试递增idx[1],依此类推。每次更改idx[i]时,我都会对key[i]的等效更改。每次我对idx / key进行新的排列时,我都会进行strcmp测试,以查看是否找到了正确的密钥。

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;
        }
    }
}

答案 2 :(得分:0)

在您不限制嵌套循环深度的情况下,即在查找带有一组字母的任意单词时,递归效果很好。

因此,以下功能对“字母”集中的单词进行了递归搜索,返回

这是一个模拟循环的递归函数:

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;
}

答案 3 :(得分:-1)

好的,让我们尝试将其重写为功能程序。我会继续使用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的数组,变量{ {1}},keysalt,以及库调用hash

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

参数Maybe Keya分别从0到e枚举。在一个功能程序中,我们可以设置这五个单独的参数,但是,我将声明length - 1的类型别名Key(五个字符)。

接下来,我们可以依次定义从(Char, Char, Char, Char, Char)('A','A','A','A','A')('A','A','A','A','B')的整个键空间的列表。不幸的是,使('Z','Z','Z','Z','Z')这样的范围可以工作is a little too complicated to use as an example of how nice functional code can be,的样板,但至少我可以直观地将其作为列表理解来编写。

[firstKey..lastKey]

请注意,由于Haskell是一种延迟评估的语言,因此它仅计算其实际使用的值。整个列表不是预先生成的。实际上,GHC完全可以避免创建链接列表对象。

外部函数的参数在循环的迭代之间不变,应该是我们外部函数的参数,我们将其称为allKeys = [(a, b, c, d, e) | a <- ['A'..'Z'], b <- ['A'..'Z'], c <- ['A'..'Z'], d <- ['A'..'Z'], e <- ['A'..'Z'] ] 。为了简单起见,我们假定一个无符号的16位盐和一个无符号的64位哈希。为简单起见,它将蛮力搜索整个密钥空间,而不是对其进行分区。

bruteForce

有多种写import Data.Word (Word16, Word64) type Salt = Word16 type Hash = Word64 bruteForce :: Salt -> Hash -> Maybe Key 的方法,但是您要求递归解决方案,所以让我们编写一个辅助函数。递归助手的常规名称为bruteForcego。我会选择bruteForce',因为它更短。由于它是嵌套的局部函数,因此可以引用参数gosalt。稍后,我将在使用它的函数中移动整个键空间的列表的定义:

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 xx,而Keysalt,因此必须有一些Salt函数使用cryptKey并返回Salt

出于演示目的,我将对从(AAAAA,0x0000)→0到(ZZZZZ,0xFFFF)→778 657 857 535的每个可能的键/盐对进行简单的枚举。

将它们放在一起,我们得到:

Hash

当您记住只有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) 下的内容与您编写的代码示例相对应时,我们会发现该代码非常简单,而且速度相当快。

因此,进行了一些快速测试。如果我们的哈希为0x48080,则最后16位0x8080就是我们的盐。 (切勿编写这样的加密哈希函数!)其余的位0x4表示密钥编号4,其中零为AAAAA,即AAAAE。在REPL中进行测试:

bruteForce

检查往返转换:

*Crack> bruteForce 0x8080 0x48080
Just ('A','A','A','A','E')

PS

一条评论反对我使用的语言不是C。现在,我喜欢C,但是坦率地说,它不是从事这项工作的正确工具。您的尾部递归函数将具有七个或八个参数以及许多特殊情况(或者通过使用全局和循环来“欺骗”)。

如果我想高效且简洁地在类似C的语言中使用这种功能习惯用法,我将使用LINQ和*Crack> crypt ('N','I','F','T','Y') 0xCEED 398799326957 *Crack> bruteForce 0xCEED 398799326957 Just ('N','I','F','T','Y') 用C#编写。这是C中的迭代代码到C#中的准功能代码的an example of a translation I’ve posted previously。还有here are the benchmarks的运行速度。

C#的实现可能非常相似:将整个键空间枚举为异步序列,然后对其进行扫描以查找与给定的盐匹配哈希值的第一个键。

您可以更改上面的Haskell程序来做到这一点,并将yield return减少为一行,即高阶函数调用:

bruteForce