Comment déterminer le temps d'exécution d'une fonction récursive double?


15

Étant donné une fonction arbitrairement double récursive, comment calculer son temps d'exécution?

Par exemple (en pseudocode):

int a(int x){
  if (x < = 0)
    return 1010;
  else
    return b(x-1) + a(x-1);
}
int b(int y){
  if (y <= -5)
    return -2;
  else
    return b(a(y-1));
}

Ou quelque chose de ce genre.

Quelles méthodes pourrait-on ou devrait-on utiliser pour déterminer quelque chose comme ça?


2
Est-ce des devoirs?
Bernard

5
Non, c'est l'heure d'été et j'aime apprendre. Je pense aller de l'avant au lieu de laisser mon cerveau se transformer en bouillie.
if_zero_equals_one

11
OK, j'ai compris. Pour ceux qui votent pour la migration vers Stack Overflow: c'est sur le sujet ici, et hors sujet sur Stack Overflow. Programmers.SE est destiné aux questions conceptuelles sur tableau blanc; Stack Overflow est destiné aux questions de mise en œuvre, de problème pendant le codage.

3
Merci, c'est la raison pour laquelle je l'ai fait ici en premier lieu. De plus, il vaut mieux savoir pêcher que recevoir un poisson.
if_zero_equals_one

1
Dans ce cas particulier, il s'agit généralement d'une récursion infinie car b (a (0)) invoque une infinité d'autres termes b (a (0)). Cela aurait été différent s'il s'agissait d'une formule mathématique. Si votre configuration avait été différente, cela aurait fonctionné différemment. Tout comme en mathématiques, en cs, certains problèmes ont une solution, certains non, certains faciles, d'autres non. Il existe de nombreux cas mutuellement récursifs où la solution existe. Parfois, pour ne pas faire exploser une pile, il faudrait utiliser un modèle de trampoline.
Job

Réponses:


11

Vous continuez de changer votre fonction. Mais continuez à choisir ceux qui fonctionneront pour toujours sans conversion ..

La récursivité devient compliquée, rapide. La première étape de l'analyse d'une fonction doublement récursive proposée consiste à essayer de la retracer sur certaines valeurs d'échantillon, pour voir ce qu'elle fait. Si votre calcul entre dans une boucle infinie, la fonction n'est pas bien définie. Si votre calcul entre dans une spirale qui continue à augmenter les nombres (ce qui se produit très facilement), il n'est probablement pas bien défini.

Si le suivi donne une réponse, vous essayez alors de trouver un modèle ou une relation de récurrence entre les réponses. Une fois que vous avez cela, vous pouvez essayer de comprendre son temps d'exécution. Le comprendre peut être très, très compliqué, mais nous avons des résultats tels que le théorème maître qui nous permettent de trouver la réponse dans de nombreux cas.

Attention, même avec une seule récursivité, il est facile de trouver des fonctions dont on ne sait pas calculer le temps d'exécution. Par exemple, considérez ce qui suit:

def recursive (n):
    if 0 == n%2:
        return 1 + recursive(n/2)
    elif 1 == n:
        return 0
    else:
        return recursive(3*n + 1)

On ignore actuellement si cette fonction est toujours bien définie, sans parler de son exécution.


5

L'exécution de cette paire de fonctions particulière est infinie car aucune ne revient sans appeler l'autre. La valeur de retour aest toujours dépendant de la valeur de retour d'un appel bqui toujours appelle a... et c'est ce qu'on appelle une récursion infinie .


Ne cherche pas les fonctions particulières ici. Je cherche un moyen général de trouver le temps d'exécution des fonctions récursives qui s'appellent.
if_zero_equals_one

1
Je ne suis pas sûr qu'il existe une solution dans le cas général. Pour que Big-O ait un sens, vous devez savoir si l'algorithme s'arrêtera jamais. Il existe certains algorithmes récursifs où vous devez exécuter le calcul avant de savoir combien de temps cela prendra (par exemple, déterminer si un point appartient à l'ensemble de Mandlebrot ou non).
jimreed le

Pas toujours, an'appelle que bsi le nombre transmis est> = 0. Mais oui, il y a une boucle infinie.
btilly

1
@btilly l'exemple a été changé après avoir posté ma réponse.
jimreed le

1
@jimreed: Et cela a encore été changé. Je supprimerais mon commentaire si je le pouvais.
btilly

4

La méthode la plus évidente consiste à exécuter la fonction et à mesurer le temps nécessaire. Cependant, cela ne vous indique que le temps nécessaire à une entrée particulière. Et si vous ne savez pas à l'avance que la fonction se termine, difficile: il n'y a aucun moyen mécanique de déterminer si la fonction se termine - c'est le problème de l' arrêt , et c'est indécidable.

Trouver le temps d'exécution d'une fonction est également indécidable, selon le théorème de Rice . En fait, le théorème de Rice montre que même décider si une fonction s'exécute dans le O(f(n))temps est indécidable.

Donc, le mieux que vous puissiez faire en général est d'utiliser votre intelligence humaine (qui, à notre connaissance, n'est pas liée par les limites des machines de Turing) et d'essayer de reconnaître un modèle ou d'en inventer un. Une manière typique d'analyser le temps d'exécution d'une fonction consiste à transformer la définition récursive de la fonction en une équation récursive sur son temps d'exécution (ou un ensemble d'équations pour les fonctions récurrentes mutuellement):

T_a(x) = if x ≤ 0 then 1 else T_b(x-1) + T_a(x-1)
T_b(x) = if x ≤ -5 then 1 else T_b(T_a(x-1))

Et ensuite? Vous avez maintenant un problème mathématique: vous devez résoudre ces équations fonctionnelles. Une approche qui fonctionne souvent consiste à transformer ces équations sur les fonctions entières en équations sur les fonctions analytiques et à utiliser le calcul pour les résoudre, en interprétant les fonctions T_aet en T_btant que fonctions génératrices .

Sur la génération de fonctions et d'autres sujets mathématiques discrets, je recommande le livre Concrete Mathematics , de Ronald Graham, Donald Knuth et Oren Patashnik.


1

Comme d'autres l'ont souligné, l'analyse de la récursivité peut devenir très difficile très rapidement. Voici un autre exemple d'une telle chose: http://rosettacode.org/wiki/Mutual_recursion http://en.wikipedia.org/wiki/Hofstadter_sequence#Hofstadter_Female_and_Male_sequences il est difficile de calculer une réponse et un temps d'exécution pour ceux-ci. Cela est dû à ces fonctions mutuellement récursives ayant une "forme difficile".

Quoi qu'il en soit, regardons cet exemple simple:

http://pramode.net/clojure/2010/05/08/clojure-trampoline/

(declare funa funb)
(defn funa [n]
  (if (= n 0)
    0
    (funb (dec n))))
(defn funb [n]
  (if (= n 0)
    0
    (funa (dec n))))

Commençons par essayer de calculer funa(m), m > 0:

funa(m) = funb(m - 1) = funa(m - 2) = ... funa(0) or funb(0) = 0 either way.

Le temps d'exécution est:

R(funa(m)) = 1 + R(funb(m - 1)) = 2 + R(funa(m - 2)) = ... m + R(funa(0)) or m + R(funb(0)) = m + 1 steps either way

Prenons maintenant un autre exemple, un peu plus compliqué:

Inspiré par http://planetmath.org/encyclopedia/MutualRecursion.html , qui est une bonne lecture en soi, regardons: "" "Les nombres de Fibonacci peuvent être interprétés par récurrence mutuelle: F (0) = 1 et G (0 ) = 1, avec F (n + 1) = F (n) + G (n) et G (n + 1) = F (n). "" "

Alors, quel est le temps d'exécution de F? Nous irons dans l'autre sens.
Eh bien, R (F (0)) = 1 = F (0); R (G (0)) = 1 = G (0)
Maintenant R (F (1)) = R (F (0)) + R (G (0)) = F (0) + G (0) = F (1)
...
Il n'est pas difficile de voir que R (F (m)) = F (m) - par exemple, le nombre d'appels de fonction nécessaires pour calculer un nombre de Fibonacci à l'indice i est égal à la valeur d'un nombre de Fibonacci à l'indice i. Cela supposait que l'addition de deux nombres ensemble était beaucoup plus rapide qu'un appel de fonction. Si ce n'était pas le cas, alors ce serait vrai: R (F (1)) = R (F (0)) + 1 + R (G (0)), et l'analyse de cela aurait été plus compliquée, éventuellement sans solution de forme fermée facile.

La forme fermée de la séquence de Fibonacci n'est pas forcément facile à réinventer, sans parler d'exemples plus compliqués.


0

La première chose à faire est de montrer que les fonctions que vous avez définies se terminent et pour quelles valeurs exactement. Dans l'exemple que vous avez défini

int a(int x){
  if (x < = 0)
    return 1010;
  else
    return b(x-1) + a(x-1);
}
int b(int y){
  if (y <= -5)
    return -2;
  else
    return b(a(y-1));
}

bne se termine que y <= -5parce que si vous insérez une autre valeur, vous aurez un terme du formulaire b(a(y-1)). Si vous faites un peu plus d'expansion, vous verrez qu'un terme du formulaire b(a(y-1))mène finalement au terme b(1010)qui mène à un terme b(a(1009))qui mène à nouveau au terme b(1010). Cela signifie que vous ne pouvez pas brancher de valeur aqui ne satisfait pas, x <= -4car si vous le faites, vous vous retrouvez avec une boucle infinie où la valeur à calculer dépend de la valeur à calculer. Donc, essentiellement, cet exemple a un temps d'exécution constant.

Donc, la réponse simple est qu'il n'y a pas de méthode générale pour déterminer le temps d'exécution des fonctions récursives car il n'y a pas de procédure générale qui détermine si une fonction définie de manière récursive se termine.


-5

Runtime comme dans Big-O?

C'est simple: O (N) - en supposant qu'il existe une condition de terminaison.

La récursivité est simplement une boucle, et une simple boucle est O (N), peu importe le nombre de choses que vous faites dans cette boucle (et appeler une autre méthode n'est qu'une autre étape de la boucle).

Là où cela deviendrait intéressant, c'est si vous avez une boucle dans une ou plusieurs des méthodes récursives. Dans ce cas, vous vous retrouveriez avec une sorte de performance exponentielle (multipliant par O (N) à chaque passage dans la méthode).


2
Vous déterminez les performances Big-O en prenant l'ordre le plus élevé de toute méthode appelée et en le multipliant par l'ordre de la méthode appelante. Cependant, une fois que vous commencez à parler de performances exponentielles et factorielles, vous pouvez ignorer les performances polynomiales. Je pense qu'il en va de même pour la comparaison des gains exponentiels et factoriels: factoriels. Je n'ai jamais eu à analyser un système à la fois exponentiel et factoriel.
Anon

5
Ceci est une erreur. Les formes récursives de calcul du nième nombre de Fibonacci et du tri rapide sont O(2^n)et O(n*log(n)), respectivement.
unpythonic

1
Sans faire de preuve de fantaisie, je voudrais vous diriger vers amazon.com/Introduction-Algorithms-Second-Thomas-Cormen/dp/… et essayer de jeter un œil à ce site SE cstheory.stackexchange.com .
Bryan Harrington

4
Pourquoi les gens ont-ils voté cette horriblement mauvaise réponse? L'appel d'une méthode prend un temps proportionnel au temps que prend cette méthode. Dans ce cas, la méthode aappelle bet bappelle a, vous ne pouvez donc pas simplement supposer que l'une ou l'autre méthode prend du temps O(1).
btilly

2
@Anon - L'affiche demandait une fonction arbitrairement double récursive, pas seulement celle illustrée ci-dessus. J'ai donné deux exemples de récursivité simple qui ne correspondent pas à votre explication. Il est trivial de convertir les anciennes normes en une forme "double récursive", une qui était exponentielle (correspondant à votre mise en garde) et une qui ne l'est pas (non couverte).
2011 non pythonique à 20h42
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.