Comment implémenter la programmation dynamique dans Swift

Dans notre exploration des algorithmes, nous avons appliqué de nombreuses techniques pour produire des résultats. Certains concepts ont utilisé des modèles spécifiques à iOS tandis que d'autres ont été généralisés. Bien que cela n’ait pas été explicitement mentionné, certaines de nos solutions ont utilisé un style de programmation particulier appelé programmation dynamique. Bien que simple en théorie, son application peut parfois être nuancée. Appliquée correctement, la programmation dynamique peut avoir un effet puissant sur la manière dont vous écrivez du code. Dans cet essai, nous allons présenter le concept et la mise en œuvre de la programmation dynamique.

Garder pour plus tard

Si vous avez acheté quelque chose sur Amazon.com, vous connaissez le terme du site - "Enregistrer pour plus tard". Comme le suggère la phrase, les acheteurs ont la possibilité d'ajouter des articles à leur panier ou de les enregistrer dans une "liste de souhaits" pour les visionner ultérieurement. Lors de l'écriture d'algorithmes, nous sommes souvent confrontés à un choix similaire d'actions à effectuer (effectuer des calculs) lors de l'interprétation des données ou de stockage des résultats pour une utilisation ultérieure. Les exemples incluent la récupération de données JSON à partir d'un service RESTful ou l'utilisation du Core Data Framework:

Dans iOS, les modèles de conception peuvent nous aider à gagner du temps et à coordonner le traitement des données. Les techniques spécifiques comprennent les opérations multithreads (par exemple, Grand Central Dispatch), les notifications et la délégation. La programmation dynamique (DP), en revanche, n’est pas nécessairement une technique de codage unique, mais plutôt une réflexion sur les actions (par exemple, les sous-problèmes) qui surviennent lorsque la fonction est utilisée. La solution de DP résultante peut différer selon le problème. Dans sa forme la plus simple, la programmation dynamique s'appuie sur le stockage et la réutilisation des données pour accroître l'efficacité des algorithmes. Le processus de réutilisation des données est également appelé mémorisation et peut prendre de nombreuses formes. Comme nous le verrons, ce style de programmation offre de nombreux avantages.

Fibonacci revisité

Dans l'essai sur la récursivité, nous avons comparé la construction de la séquence classique des valeurs de Array à l'aide de techniques à la fois itératives et récursives. Comme discuté, ces algorithmes ont été conçus pour produire une séquence Array, pas pour calculer un résultat particulier. En prenant cela en compte, nous pouvons créer une nouvelle version de Fibonacci pour renvoyer une seule valeur Int:

func fibRecursive (n: Int) -> Int {
    si n == 0 {
        retourne 0
    }
    
    si n <= 2 {
        retour 1
    }
    
    retour fibRecursive (n: n-1) + fibRecursive (n: n-2)
}

À première vue, il semble que cette fonction apparemment petite serait également efficace. Cependant, après une analyse plus poussée, nous constatons que de nombreux appels récursifs doivent être faits pour que le résultat puisse être calculé. Comme indiqué ci-dessous, fibRecursive ne pouvant pas stocker les valeurs calculées précédemment, ses appels récursifs augmentent de manière exponentielle:

Fibonacci mémoized

Essayons une technique différente. Conçu comme une fonction Swift imbriquée, fibMemoized capture la valeur de retour Array à partir de sa sous-fonction fibSequence pour calculer une valeur finale:

extension Int {
    
    // version mémoisée
    fonction de mutation fibMemoized () -> Int {
        
        // construit une séquence de tableau
        func fibSequence (_ sequence: Array  = [0, 1]) -> Array  {
            
            var final = Tableau  ()
            
            // copie mutée
            sortie var = séquence
            
            let i: Int = output.count
            
            // set condition de base - temps linéaire O (n)
            si je == moi {
                retour de sortie
            }
            
            let results: Int = sortie [i - 1] + sortie [i - 2]
            output.append (résultats)
            
            // set itération
            final = fibSequence (sortie)
            retour final
            
        }
        
        
        // calcul du produit final - temps constant O (1)
        laisser les résultats = fibSequence ()
        laissez répondre: Int = résultats [résultats.endIndex - 1] + résultats [résultats.endIndex - 2]
        retour réponse
        
    }
}

Même si fibSquence inclut une séquence récursive, son cas de base est déterminé par le nombre de positions de tableau demandées (n). En termes de performances, nous disons fibSequence s'exécute en temps linéaire ou en O (n). Cette amélioration des performances est obtenue en mémorisant la séquence Array nécessaire au calcul du produit final. En conséquence, chaque permutation de séquence est calculée une fois. L’avantage de cette technique est visible lors de la comparaison des deux algorithmes présentés ci-dessous:

Les plus courts chemins

La mémorisation de code peut également améliorer l’efficacité d’un programme au point de rendre des questions apparemment difficiles ou presque insolubles. Un exemple de ceci peut être vu avec l’algorithme de Dijkstra et les chemins les plus courts. Pour passer en revue, nous avons créé une structure de données unique nommée Path dans le but de stocker des métadonnées de parcours spécifiques:

// la classe de chemin maintient les objets qui constituent la "frontière"
chemin de classe {
    
    var total: Int
    destination var: sommet
    var précédent: Path?
   // initialisation d'objet
    init () {
        destination = Vertex ()
        total = 0
    }
}

Ce qui rend Path utile, c’est sa capacité à stocker des données sur les nœuds précédemment visités. Similaire à notre algorithme de Fibonacci révisé, Path stocke les pondérations Edge cumulées de tous les sommets traversés (total) ainsi qu'un historique complet de chaque sommet visité. Utilisé efficacement, cela permet au programmeur de répondre à des questions telles que la complexité de la navigation vers un sommet de destination particulier, si la traversée a effectivement abouti (pour trouver la destination), ainsi que la liste des nœuds visités. Selon la taille et la complexité du graphe, ne pas disposer de ces informations peut signifier que l'algorithme prend trop de temps pour (re) calculer des données, qu'il devient trop lent pour être efficace ou qu'il est impossible de résoudre des questions vitales en raison de données insuffisantes.

A aimé cet essai? Lisez et découvrez mes autres contenus sur Medium ou obtenez le livre complet au format EPUB, PDF ou Kindle.