Развертывание древовидной рекурсии в итеративную программу - PullRequest
2 голосов
/ 03 августа 2020

Я написал библиотеку, которая может генерировать произвольные строки с учетом объекта spe c (https://github.com/rgrannell1/revexp), и я хочу преобразовать функцию, которая читает spe c из рекурсивного алгоритма, в итерационный алгоритм. Я сталкиваюсь с ошибками stackoverflow из-за глубины спецификаций, которые я просматриваю.

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

Вот пример объекта spe c.

const data = {
  every: [
    {
      digit: { zero: false }
    },
    {
      repeat: {
        value: { digit: {} }
      }
    }
  ]
}

и минимальный пример того, как алгоритм в настоящее время проходит spe c и генерирует строку, соответствующую spe c.

const fromSpec = (data) => {
  if (data.every) {
    return fromSpec.every(data)
  } else if (data.digit) {
    return fromSpec.digit()
  } else if (data.repeat) {
    return fromSpec.repeat(data.repeat)
  }
}

fromSpec.digit = () => {
  return Math.floor(Math.random() * 10)
}

fromSpec.every = part => {
  let message = ''

  for (const elem of part.every) {
    message += fromSpec(elem)
  }

  return message
}



fromSpec.repeat = part => {
  let message = ''
  
  // -- just using fixed repeat for the example
  for (let ith = 0; ith < 10; ++ith) {
    message += fromSpec(part.value)
  }

  return message
}

const result = fromSpec(data)
result // 1034856872

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

Ответы [ 2 ]

1 голос
/ 06 августа 2020

Естественно написать RevExp.toString как рекурсивную программу, поскольку ожидается, что она будет обрабатывать рекурсивно структурированный ввод. Однако, поскольку рекурсивные программы могут приводить к созданию глубоких стеков, нередко сводят рекурсивный процесс к итеративному.

Хорошие программисты знают, что поддержание сложности имеет большое значение для поддержания нашего рассудка. Я хочу сохранить свою рекурсивную программу, и я хочу, чтобы компьютер выполнял сглаживание процесса за меня. Можно мне пирог и съесть его?

Вот еще один способ решить проблему -

// example1.js

import { concat, upper, digit, str, toString } from './RevExp.js'

const licensePlate =
  concat(upper(), upper(), upper(), str("-"), digit(), digit(), digit())

console.log(toString(licensePlate))
console.log(toString(licensePlate))
console.log(toString(licensePlate))
// RFX-559
// VKT-794
// KSF-823

Давайте начнем писать модуль RevExp. Мы начнем с создания конструкторов для каждого из наших типов выражений -

// RevExp.js

const str = (value = "") =>
  ({ type: str, value })

const lower = () =>
  ({ type: lower })

const upper = () =>
  ({ type: upper })

const digit = ({ zero = true } = {}) =>
  ({ type: digit, zero })

const concat = (...exprs) =>
  ({ type: concat, exprs })

Теперь давайте поработаем над RevExp.toString -

// RevExp.js (continued)

import { inRange } from './Rand.js'

const toString = (e) =>
{ switch (e.type)
  { case str:
      return String(e.value)
    case lower:
      return String.fromCharCode(inRange(97, 122))
    case upper:
      return String.fromCharCode(inRange(65, 90))
    case digit:
      return e.zero
      ? String(inRange(0, 9))
      : String(inRange(1, 9))
    case concat:
      return e.exprs.reduce((r, v) => r + toString(v), "")
    default: throw Error(`unsupported expression type: ${e.type}`)
  }
}

export { lower, upper, digit, alpha, repeat, concat, str, toString }

Должно быть возможно создавать сложные выражения, комбинируя несколько простых выражений. И мы представляем некоторые новые типы, такие как alpha и repeat -

// example2.js

import { alpha, digit, repeat, concat, str, toString } from './RevExp.js'

const segment =
  concat(alpha(), digit(), alpha(), digit(), alpha())

const serial =
  concat
    ( repeat
        ( concat(segment, str("-"))
        , { count: 4 }
        )
    , segment
    )

console.log(toString(serial))
console.log(toString(serial))
console.log(toString(serial))
// F3Q7U-b6k8Q-R8e3A-a2q3M-j0a9k
// g6G3w-h2O3O-b8O3k-L4p1y-m5I0y
// m6E0M-A4C2y-K3g0M-d7X7j-w8v5G

И добавляем соответствующую поддержку в модуль RevExp -

// RevExp.js (enhanced)

import { inRange, sample } from './Rand.js'

const str = // ...
const lower = // ...
const upper = // ...
const digit = // ...
const concat = // ...

const alpha = () =>
  oneOf(upper(), lower())

const oneOf = (...exprs) =>
  ({ type: oneOf, exprs })

const repeat = (expr = {}, { count = 10 } = {}) =>
  ({ type: repeat, expr, count })

const toString = (e) =>
{ switch (e.type)
  { case str: // ...
    case lower: // ...
    case upper: // ...
    case digit: // ...
    case concat: // ...
    case oneOf:
      return toString(sample(e.exprs))
    case repeat:
      return toString(concat(...Array(e.count).fill(e.expr)))
    default: // ...
  }
}

export { /* ..., */ alpha, oneOf, repeat }

Теперь преобразуем рекурсивную программу в итерационную. И без необходимости думать о стеке или изменении состояния при запуске программы!

// RevExp.js (stack-safe)

// ...
import * as Str from './Str.js'
import { loop, recur, call } from './TailRec.js'

// ...
const toString = (e = {}) =>
  loop(toStringTailRec, e)

const toStringTailRec = e =>
{ switch (e.type)
  { case str: // ...
    case lower: // ...
    case upper: // ...
    case digit: // ...
    case concat:
      return e.exprs.length
        ? call
            ( Str.concat
            , recur(e.exprs[0])
            , recur(concat(...e.exprs.slice(1)))
            )
        : Str.empty
    case oneOf:
      return recur(sample(e.exprs))
    case repeat:
      return recur(concat(...Array(e.count).fill(e.expr)))
    default: throw Error(`unsupported expression type: ${e.type}`)
  }
}

export { /*...*/, toString } // <-- don't export toStringTailRec helper

А вот оставшиеся модули: Str, Rand и TailRec -

// Str.js

const empty =
  ""

const concat = (a = "", b = "") =>
  a + b

export { empty, concat }
// Rand.js

const rand = (n = 2) =>
  Math.floor(Math.random() * n)

const inRange = (min = 0, max = 1) =>
  rand(max - min + 1) + min

const sample = (t = []) =>
  t[rand(t.length)]

export { rand, inRange, sample }

Написание модулей - важный фактор в создании повторно используемого кода. Этот модуль TailRec был написан в другом сообщении и может быть повторно использован без изменений 1 , чтобы удовлетворить потребности нашей программы.

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

// TailRec.js

const identity = x =>
  x

const call = (f, ...values) =>
  ({ type: call, f, values })

const recur = (...values) =>
  ({ type: recur, values })

const loop = (f, ...init) =>
{ const aux1 = (e, k) =>
    e.type === recur
      ? call(aux, e.values, r => call(aux1, f(...r), k))
  : e.type === call
      ? call(aux, e.values, r => call(aux1, e.f(...r), k))
  : call(k, e)

  const aux = (exprs, k) =>
    call
      ( exprs.reduce
          ( (mr, e) =>
              k => call(mr, r => call(aux1, e, x => call(k, [ ...r, x ])))
          , k => call(k, [])
          )
      , k
      )

  return run(aux1(f(...init), identity))
}

const run = r =>
{ while (r && r.type === call)
    r = r.f(...r.values)
  return r
}

export { loop, call, recur }

В конце концов, подход здесь практически такой же, как и у вас. Однако вместо представления выражений, записывающих JS объектов вручную, мы используем функции, которые можно параметризовать и составить, и которые могут обрабатывать утомительную и ненадежную сборку за нас. Сохранение рассудка программиста -

// example2.js

// ...
console.log(serial)
{ type: concat
, exprs:
    [ { type: repeat
      , expr:
          { type: concat
          , exprs:
              [ { type: concat
                , exprs:
                    [ { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
                    , { type: digit, zero: true }
                    , { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
                    , { type: digit, zero: true }
                    , { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
                    ]
                }
              , { type: str, value: "-" }
              ]
          }
      , count: 4
      }
    , { type: concat
      , exprs:
        [ { type: concat
          , exprs:
              [ { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
              , { type: digit, zero: true }
              , { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
              , { type: digit, zero: true }
              , { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
              ]
          }
        ]
      }
    ]
}

Я надеюсь, что это было замечено как захватывающий способ взглянуть на ту же проблему с другой точки зрения. Если вы в конечном итоге используете модуль TailRec, см. Дополнительные объяснения в исходной публикации. Я буду рад ответить на любые дополнительные вопросы.

1. Незначительные изменения форматирования и переименование переменных для согласованности с этим ответом

1 голос
/ 04 августа 2020

Следующий пример изменяет код для использования структуры данных стека. Данные в стеке обрабатываются постепенно, с возможностью добавления новых данных на каждой итерации.

const fromSpec = (data) => {
    const stack = [data];
    let message = '';
    while (stack.length > 0) {
        const item = stack.pop();
        // Assumption based on the code in the question:
        //   'every', 'digit', and 'repeat' keys are mutually exclusive.
        if (item.every) {
            // Add items in reverse order, so that items are popped off the stack
            // in the original order.
            for (let i = item.every.length - 1; i >= 0; --i) {
                stack.push(item.every[i]);
            }
        } else if (item.digit) {
            message += String(Math.floor(Math.random() * 10));
        } else if (item.repeat) {
            for (let i = 0; i < 10; ++i) {
                stack.push(item.repeat.value);
            }
        }
    }
    return message;
}

Для более сложного сценария может потребоваться альтернативный подход (например, когда узел в дереве требует обработки как 1 ), когда он впервые встречается при обходе и 2) после того, как все его дочерние элементы были пройдены).

Следующие ссылки могут быть релевантными.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...