Как заменить циклы рекурсией - PullRequest
0 голосов
/ 29 сентября 2018

Я только начинаю с рекурсивного программирования - и поскольку я слышал, насколько это эффективно для решения проблем, я хотел бы попробовать его на простом алгоритме дешифрования, который я написал несколько дней назад.

Я знаю, что может быть трудно понять, что делает каждая итерация, но может ли рекурсия сделать этот цикл более «элегантным» и «алгоритмическим»?

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 голосов
/ 29 сентября 2018

В вашем случае рекурсия хороша, когда вы не ограничиваете глубину вложенных циклов, то есть ищете произвольные слова с набором букв.

Итак, следующие функции выполняют рекурсивный поиск возможности слов в наборе 'букв', возвращают

Вот рекурсивная функция, которая имитирует ваши циклы:

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;
}
0 голосов
/ 29 сентября 2018

Хотя я не согласен со всеми, кто отговаривал вас использовать рекурсию в этом примере, я хотел написать ее рекурсивно, поскольку считаю, что это разумный вопрос.

Вот моя попытка написать это рекурсивно.Делая это, мне нужно написать цикл только один раз, поскольку рекурсия обрабатывает внешний цикл.Я взял некоторые вольности, поэтому он не совсем эквивалентен вашему коду, но я думаю, что в принципе он одинаков (тестирование всех комбинаций с 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),Я пытаюсь сохранить целое число, эквивалентное key в idx.Если я увеличиваю 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;
        }
    }
}
0 голосов
/ 29 сентября 2018

Хорошо, давайте попробуем переписать это как функциональную программу.Я пойду дальше и использую 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] work , слишком сложен, чтобы использовать его в качестве примера того, насколько хорошим может быть функциональный код, , но, по крайней мере, я могу написать его интуитивно в качестве понимания списка.

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, поэтому должна существовать некоторая функция crypt, которая принимает Key и Salt и возвращает Hash.

В демонстрационных целях я сделаю простое перечисление каждой возможной пары ключ / соль от (AAAAA, 0x0000) → 0 до (ZZZZZ, 0xFFFF) → 778 657 857 535.

Помещениевсе вместе, мы получаем:

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, соответствует примеру кода, который вы написали, мы видим, что этот код довольно прост и достаточно быстр.

Итак, пара быстрых тестов.Если наш хеш равен 0x48080, последние шестнадцать бит, 0x8080, были просто нашей солью.(Никогда не пишите криптографическую хэш-функцию, подобную этой!) Остальные биты 0x4 означают номер четыре ключа, где ноль - 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')

PS

Один из комментариев возразил, чтоязык, который я использовал, был не C. Теперь я люблю C, но, честно говоря, это не самый подходящий инструмент для этой работы.Ваша хвостовая рекурсивная функция будет иметь семь или восемь параметров и столько же особых случаев (или «обманывать», используя глобальные переменные и циклы).

Если бы я хотел использовать этот вид функциональной идиомы в C-подобномязыком, эффективно и лаконично, я бы написал это на C # с помощью LINQ и, возможно, yield return.Вот пример перевода, который я ранее опубликовал итеративного кода на C, в квазифункциональный код на C #.И - вот эталоны скорости его выполнения.

Реализация AC # может быть очень похожей: перечислить все пространство ключей в виде асинхронной последовательности и отсканировать его для первого подходящего ключахеш с заданной солью.

Вы можете изменить вышеуказанную программу на Haskell, и она уменьшит bruteForce до одной строки, вызов функции высшего порядка:

import Data.List (find)

bruteForce2 salt hash = find (\x -> crypt x salt == hash) allKeys
0 голосов
/ 29 сентября 2018

Если вы можете выполнить задачу без рекурсии, это хорошая идея, чтобы решить ее таким образом.Если вы хотите узнать о рекурсии, проверьте некоторые проблемы, такие как факториал или Фибоначчи.У них также есть итеративные решения, но они гораздо больше подходят для рекурсии, чем проблема, с которой вы здесь столкнулись.В этом случае очень ясно, что делает ваш алгоритм, и рекурсия сделает его излишне трудным для понимания.Вот одно улучшение, которое вы можете сделать, однако

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