Является ли такая структура функций хвостовой рекурсивной? - PullRequest
0 голосов
/ 28 декабря 2018

Является ли такая структура функции хвостовой рекурсивной?

function foo(data, acc) {
    ...
    return foo(data, foo(data, x));
}

По определению рекурсивная функция является хвостовой рекурсивной, когда рекурсивный вызов является последним выполняемым функцией.В этом примере последнее, что делает функция, это вызывает foo и возвращает его значение, однако перед этим она использует возвращаемое значение вложенной функции foo.Таким образом, я запутался.

Редактировать: Рассмотрим язык схемы и простую функцию, которая умножает элементы в данном списке:

Пример 1:

(define (foo list) (helper list 1) )

(define (helper list acc)
  (cond ((null? list) acc)
   ((not (pair? list)) (* list acc))
   ((list? (car list)) (helper (car list) (helper (cdr list) acc)))
   (else (helper (cdr list) (* acc (car list)))))
)

Пример 2: Является ли это чисто хвостовой рекурсией?

(define (foo list) (helper list 1) )

(define (helper list acc)
    (cond ((null? list) acc)
    ((not (pair? list)) (* list acc))
    ((list? (car list)) (helper (cdr list) (* (foo (car list)) acc)))
    (else (helper (cdr list) (* acc (car list))))))

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

Ответы [ 4 ]

0 голосов
/ 29 декабря 2018

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

В вашем примере:

function foo(data, acc) {
    ...
    return foo(data, foo(data, x));
}

или

(define (foo data acc)
  (foo data (foo data x)))

Есть два вызова: внутренний (foo data x) не в хвостовом контексте, а внешний (foo data ...).

Спецификация хвостовых контекстов в схеме R5RS приведена в [1].

В итоге: проверить, является ли конкретный вызов хвостовым, является синтаксической проверкой.

Является ли ваша функция "рекурсивной"?Это зависит от того, как вы определяете «хвостовую рекурсивную функцию».Если вы имеете в виду «все рекурсивные вызовы должны быть хвостовыми», то нет.Если вы имеете в виду «все оценки тела заканчиваются рекурсивным вызовом» - тогда да.

Теперь также важно поведение функции во время выполнения.Какой вычислительный процесс будет происходить, когда вы будете оценивать вызов функции?Объяснение немного сложное, поэтому я оставлю ссылку и просто оставлю ссылку на: [2] SICP «1.2 Процедуры и процессы, которые они генерируют».

[1] http://www.dave -reed.com/Scheme/r5rs_22.html [2] https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-11.html#%_sec_1.2

0 голосов
/ 29 декабря 2018

Нет, это не хвостовая рекурсия, потому что foo вызывается из положения хвоста -

function foo(data, acc) {
    ...
                     // foo not in tail position here
    return foo(data, foo(data, x));
}

Давайте разберемся с этим, используя конкретную программу, такую ​​как fibonacci -

const fibonacci = n =>
  n < 2
    ? n                   // non tail call!
    : fibonacci (n - 2) + fibonacci (n - 1)
    
console .log (fibonacci (10)) // 55

Выше рекурсивный fibonacci может порождать два вызова на fibonacci, каждый из которых может порождать два больше вызововдо fibonacci.Без переписывания этого невозможно, чтобы оба вызова были в хвостовой позиции.Мы можем обойти эту проблему, используя вспомогательную функцию с дополнительным параметром, then ниже -

const helper = (n, then) =>
{ if (n < 2)
    return then (n)                // tail
  else 
    return helper (n - 2, a =>     // tail
             helper (n - 1, b =>   // tail
               then (a + b)        // tail
           ))
}

const fibonacci = n =>
{ return helper (n, x => x)        // tail
}
    
console .log (fibonacci (10)) // 55

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

const identity = x =>
  x

const fibonacci = (n, then = identity) =>
{ if (n < 2)
    return then (n)                  // tail
  else 
    return fibonacci (n - 2, a =>    // tail
             fibonacci (n - 1, b =>  // tail
               then (a + b)          // tail
           ))
}

console .log (fibonacci (10))
// 55

fibonacci (10, res => console .log ("result is", res))
// result is: 55

Хвост рекурсивный или нет, fibonacci выше - это экспоненциальный процесс, который делает его очень медленным даже при небольших значениях n. линейный процесс стал возможным благодаря представлению состояния нашего вычисления с использованием дополнительных параметров a и b -

const fibonacci = (n, a = 0, b = 1) =>
  n === 0
    ? a                            // tail
    : fibonacci (n - 1, b, a + b)  // tail
  
console .log
  ( fibonacci (10)   // 55
  , fibonacci (20)   // 6765
  , fibonacci (100)  // 354224848179262000000
  )

Иногда вам нужно будет использовать дополнительные параметры состояния, иногда вам нужно будет использовать вспомогательные функции или продолжения, такие как then.

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


В ваш отредактированный вопрос вы включили программу Scheme, которая может умножить вложенный список чисел.Сначала мы показываем метод then

(define (deep-mult xs (then identity))
  (cond ((null? xs)
         (then 1))
        ((list? (car xs))
         (deep-mult (car xs) ;; tail
                    (λ (a)
                      (deep-mult (cdr xs) ;; tail
                                 (λ (b)
                                   (then (* a b)))))))
        (else
         (deep-mult (cdr xs) ;; tail
                    (λ (a)
                      (then (* a (car xs))))))))

(deep-mult '((2) (3 (4) 5))) ;; 120

Вы можете использовать параметр состояния acc, как мы это делали и во втором методе, но поскольку входные данные могут быть вложенными, мы должны использовать thenтехника сглаживания потенциальных двух вызовов deep-mult -

(define (deep-mult xs (acc 1) (then identity))
  (cond ((null? xs)
         (then acc)) ;; tail
        ((list? (car xs))
         (deep-mult (car xs) ;; tail
                    acc
                    (λ (result)
                      (deep-mult (cdr xs) result then)))) ;; tail
        (else
         (deep-mult (cdr xs) ;; tail
                    acc
                    (λ (result) then (* result (car xs)))))))

(deep-mult '((2) (3 (4) 5)))
;; 120

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

Возможно, разумным решением этой конкретной проблемы является использование append в случае вложенного списка

(define (deep-mult xs (acc 1))
  (cond ((null? xs)
         acc)
        ((list? (car xs))
         (deep-mult (append (car xs) ;; tail
                            (cdr xs))
                    acc))
        (else
         (deep-mult (cdr xs) ;; tail
                    (* acc (car xs))))))

(deep-mult '((2) (3 (4) 5))) 
;; 120

Однако, append - это дорогостоящая операция со списками, и эта процедура может иметь плохую производительность для списков, которые вложены очень глубоко.Конечно, есть и другие решения.Посмотрите, что вы можете придумать и задать дополнительные вопросы.Я поделюсь решением, которое, как мне кажется, даст больше преимуществ и меньше недостатков.

0 голосов
/ 29 декабря 2018

Как правило, преобразуйте свою функцию в форму SSA , назвав все промежуточные объекты, а затем посмотрите, находится ли каждый вызов вашего foo в хвостовой позиции, т.е. самое последнее, что нужно сделать.

Как вы можете спросить?Они могут, если каждый находится в своей собственной ветви условного выражения, и это условное условие само находится в хвостовой позиции.

О ваших отредактированных функциях lisp.Оба не являются хвостовыми рекурсивами, даже последний, где helper вызывает foo в не-хвостовой позиции, потому что foo в конечном итоге также вызовет helper.Да, для полной рекурсивности необходимо, чтобы ни один вызов в нехвостовой позиции не приводил к вызову самой функции .Но если он был в хвостовой позиции, то все в порядке.Хвост - это прославленный goto, который является целью здесь.

Но вы можете кодировать этот хвост-рекурсивно, пройдявходной вложенный список nay tree структура данных, как

(define (foo list) (helper list 1) )

(define (helper list acc)
  (cond 
    ((null? list)  acc)
    ((not (pair? list))  (* list acc))
    ((null? (car list))  (helper (cdr list) acc))
    ((not (pair? (car list)))  (helper (cdr list) (* (car list) acc)))
    (else  (helper (cons (caar list)
                     (cons (cdar list) (cdr list))) acc))))
0 голосов
/ 29 декабря 2018

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

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

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

  1. иметь рекурсивный вызов foo(data, x) с использованием истинной рекурсии, но
  2. перезапускает пространство стека для оценки foo(data, /* result of that call */).

Так ваша функция чисто хвостовая рекурсивная?Нет, потому что ветвь рекурсии.Но может ли хороший компилятор оптимизировать один из этих рекурсивных вызовов?Да.

...