[ Web Proxy ]
URL:
Viewing: https://da.javascript.info/recursion [Back]  [Original]

Rekursion og stak
DA

Vi nsker at gre dette open source-projekt tilgngeligt for folk over hele verden.

Hjlp med at overstte indholdet af denne tutorial til dit sprog!

    Sg p Javascript.info:
    Sg i tutorialen:
    Lyst temaMrkt tema
    DanskEnglishEspaolFranaisIndonesiaItalianoTrkeOzbek

    Lad os vende tilbage til funktioner og studere dem mere grundigt.

    Vores frste emne vil vre rekursion.

    Hvis du ikke er ny i programmering, s er det sandsynligvis kendt og du kan springe dette kapitel over.

    Rekursion er et programmeringsmnster, som er nyttigt i situationer hvor en opgave kan opdeles i flere opgaver af samme type, men mere simple. Eller nr en opgave kan formindskes til en enkel handling plus en mere simpel variant af samme opgave. Eller, som vi vil se snart, for at hndtere bestemte datastrukturer.

    Nr en funktion lser en opgave, kan den i processen kalde mange andre funktioner. En delmngde heraf er nr en funktion kalder sig selv. Det kaldes rekursion.

    To mder at tnke p

    For noget simpelt at starte med lad os skrive en funktion pow(x, n) som hver x til en naturlig potens af n. Med andre ord, multiplicerer x med sig selv n gange.

    pow(2, 2) = 4
    pow(2, 3) = 8
    pow(2, 4) = 16

    Der er to mder at implementere det.

    1. Iterativ tankegang: for-loopet:

      function pow(x, n) {
        let result = 1;
      
        // Gang resultatet med x n gange i loopet
        for (let i = 0; i < n; i++) {
          result *= x;
        }
      
        return result;
      }
      
      alert( pow(2, 3) ); // 8
    2. Rekursiv tnkning: Simplificer opgaven og kald dig selv:

      function pow(x, n) {
        if (n == 1) {
          return x;
        } else {
          return x * pow(x, n - 1);
        }
      }
      
      alert( pow(2, 3) ); // 8

    Bemrk hvordan den rekursive variant er grundiglggende anderledes.

    Nr pow(x, n) kaldes splittes udfrelsen til to forgreninger:

                  if n==1  = x
                 /
    pow(x, n) =
                 \
                  else     = x * pow(x, n - 1)
    1. Hvis n == 1, s er alt trivielt. Dette kaldes basen for rekursion, fordi den fordi den umiddelbart producerer det obviouse resultat: pow(x, 1) er lig med x.
    2. Ellers kan vi reprsentere pow(x, n) som x * pow(x, n - 1). I matematik ville man skrive xn = x * xn-1. Dette kaldes et rekursivt trin: vi transformerer opgaven til en enkel handling (multiplication med x) og et mere simpelt kald af samme opgave (pow med lavere n). Nste trin forenkler det yderligere og yderligere indtil n nr 1.

    Vi kan ogs sige at pow kalder sig selv rekursivt indtil n == 1.

    []

    For eksempel, for at udregne pow(2, 4) vil den rekursive variant gre disse trin:

    1. pow(2, 4) = 2 * pow(2, 3)
    2. pow(2, 3) = 2 * pow(2, 2)
    3. pow(2, 2) = 2 * pow(2, 1)
    4. pow(2, 1) = 2

    s rekursionen reducerer et funktionskald til et mere simpelt, og s videre, indtil resultatet bliver benlyst.

    Rekursion er normalt kortere

    En rekursiv lsning er normalt kortere end en iterativ.

    Her kan vi omskrive det samme med betingelsesoperatoren ? i stedet for if for at gre pow(x, n) mere kompakt og stadig meget lselig:

    function pow(x, n) {
      return (n == 1) ? x : (x * pow(x, n - 1));
    }

    Det maksimalt antal indlejrede kald (inklusive det frste) kaldes rekursionsdybde. I vores tilflde vil det vre prcis n.

    Den maksimale rekursionsdybde er begrnset af JavaScript-motoren. Vi kan stole p, at den er 10.000, nogle motorer tillader mere, men 100.000 er sandsynligvis uden for grnsen for de fleste af dem. Der er automatiske optimeringer, der hjlper med at optimere antallet af kald (tail calls optimizations), men de er ikke endnu understttet overalt og virker kun i simple tilflde.

    Det begrnser anvendelsen af rekursion, men den forbliver stadig meget bred. Der er mange opgaver hvor den rekursive mde at tnke giver enklere kode der er nemmere at vedligeholde.

    Eksekveringens kontekst og stak

    Lad os undersge hvordan rekursive kald fungerer. For det vil vi kigge under huden p funktioner.

    Informationen om processen for eksekvering af en krende funktion er gemt i dens eksekveringskontekst.

    Eksekveringskonteksten er en intern datastruktur, der indeholder detaljer om eksekveringen af en funktion: hvor kontrollbet er nu, de aktuelle variabler, vrdien af this (vi bruger det ikke her) og andre interne detaljer.

    Et funktionskald har prcis n eksekveringskontekst forbundet med det.

    Nr en funktion udfrer et indlejret kald, sker flgende:

    • Den nuvrende funktion pauses.
    • Den eksekveringskontekst, der er forbundet med den, huskes i en speciel datastruktur kaldet execution context stack.
    • Det indlejrede kald udfres.
    • Nr det er frdigt, hentes den gamle eksekveringskontekst fra stakken og den ydre funktion genoptages fra hvor den stoppede.

    Lad os se hvad der sker under kaldet pow(2, 3).

    pow(2, 3)

    I begyndelsen af kaldet pow(2, 3) vil eksekveringskonteksten gemme variablerne: x = 2, n = 3, og kontrollbet er p linje 1 i funktionen.

    Vi kan skitsere det som:

    • Context: { x: 2, n: 3, at line 1 } pow(2, 3)

    Det er her funktionen begynder at eksekvere. Betingelsen n == 1 er falsk, s flowet fortstter til den anden gren af if:

    function pow(x, n) {
      if (n == 1) {
        return x;
      } else {
        return x * pow(x, n - 1);
      }
    }
    
    alert( pow(2, 3) );

    Variablene er de samme, men linjen ndres, s konteksten er nu:

    • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

    For at udregne x * pow(x, n - 1), vi skal lave et indlejret kald af pow med nye argumenter pow(2, 2).

    pow(2, 2)

    For at udfre et indlejret kald, husker JavaScript den nuvrende eksekveringskontekst i execution context stack.

    Her kalder vi den samme funktion pow, men det spiller ingen rolle. Processen er den samme for alle funktioner:

    1. Den nuvrende kontekst huskes verst p stakken.
    2. En ny kontekst oprettes for underkaldet.
    3. Nr underkaldet er frdigt bliver den gamle kontekst fjernet fra stakken, og dens eksekvering genoptages.

    Her er kontekststakken efter vi er get ind i underkaldet pow(2, 2):

    • Kontekst: { x: 2, n: 2, at line 1 } pow(2, 2)
    • Kontekst: { x: 2, n: 3, at line 5 } pow(2, 3)

    Den nye nuvrende eksekveringskontekst er verst (og fed), og de tidligere huskede kontekster er nedenfor.

    Nr underkaldet er frdigt er det nemt at genoptage den gamle kontekst, fordi den beholder bde variablerne og den prcise sted i koden hvor den stoppede.

    Bemrk venligst:

    Her i billedet bruger vi ordet line, som i vores eksempel er der kun t underkald i en linje, men generelt kan en enkelt linje af kode indeholde flere underkald, f.eks. pow() + pow() + somethingElse().

    S det ville vre mere prcist at sige, at eksekveringen genoptages jeblikkeligt efter underkaldet.

    pow(2, 1)

    Processen gentages: et nyt underkald oprettes p linje 5, nu med argumenterne x=2, n=1.

    En ny eksekveringskontekst oprettes, den gamle kontekst bliver pushed verst p stakken:

    • Kontekst: { x: 2, n: 1, at line 1 } pow(2, 1)
    • Kontekst: { x: 2, n: 2, at line 5 } pow(2, 2)
    • Kontekst: { x: 2, n: 3, at line 5 } pow(2, 3)

    Der er 2 gamle kontekster og 1 nuvrende kontekst for pow(2, 1).

    Udgangen

    Med udfrelsen af pow(2, 1) vil betingelsen n == 1, i modstning til fr, vre sand. Derfor udfres den frste gren af if:

    function pow(x, n) {
      if (n == 1) {
        return x;
      } else {
        return x * pow(x, n - 1);
      }
    }

    Der er ikke flere indlejrede kald, s funktionen afslutter og returnerer 2.

    Nr funktionen afslutter, er dens eksekveringskontekst ikke lngere ndvendig, s den fjernes fra hukommelsen. Den tidligere kontekst genoprettes fra toppen af stakken:

    • Context: { x: 2, n: 2, at line 5 } pow(2, 2)
    • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

    Udfrelsen af pow(2, 2) genoptages. Den har resultatet af underkaldet pow(2, 1), s den ogs kan frdiggre evalueringen af x * pow(x, n - 1), og returnere 4.

    S den tidligere kontekst genoprettes:

    • Kontekst: { x: 2, n: 3, at line 5 } pow(2, 3)

    Nr den afslutter, har vi et resultat p pow(2, 3) = 8.

    Dybden af rekursionen i dette tilflde var: 3.

    Som vi kan se fra illustrationerne ovenfor, er rekursionsdybden lig med det maksimale antal kontekster i stakken.

    Bemrk hukommelseskravene. Kontekster tager plads. I vores tilflde krver en udregning til potens af n faktisk hukommelse for n kontekster, for alle lavere vrdier af n.

    En algoritme baseret p lkker er mere hukommelsesvenlig:

    function pow(x, n) {
      let result = 1;
    
      for (let i = 0; i < n; i++) {
        result *= x;
      }
    
      return result;
    }

    Denne udgave af pow bruger kun en enkelt kontekst der ndrer i og result i processen. Dens hukommelseskrav er sm, faste og afhnger ikke af n.

    Enhver rekursion kan omskrives som en lkke. Versionen baseret p lkker er ofte mere effektiv.

    men nogle gange er omskrivningen ikke-triviel, isr nr en funktion bruger forskellige rekursive underkald afhngigt af betingelser og fletter deres resultater eller nr forgreningen er mere kompleks. Endelig kan optimeringen vre undvendig til en grad hvor det ikke er vrd vrd at investere i.

    Rekursion kan give en kortere kode der nemmere at forst og understtte. Optimeringer er ikke ndvendige overalt. Det vigtigste vi har brug for er god kode, og her kan rekursion vre at foretrkke (nr man har knkket koden il at forst den).

    Rekursive traversals

    Et anden god brug af rekursion er rekursive traversal. Jeg tror ikke der er et godt direkte dansk ord, men det betyder noget i stil med gennemlb af alle elementer i en struktur.

    Forestil dig et firma. I det kan medarbejdernes struktureres i et objekt i stil med dette:

    let company = {
      sales: [{
        name: 'John',
        salary: 1000
      }, {
        name: 'Alice',
        salary: 1600
      }],
    
      development: {
        sites: [{
          name: 'Peter',
          salary: 2000
        }, {
          name: 'Alex',
          salary: 1800
        }],
    
        internals: [{
          name: 'Jack',
          salary: 1300
        }]
      }
    };

    Med andre ord et firma har afdelinger.

    • En afdeling kan have et array af medarbejdere. For eksempel har salgsafdelingen (sales) 2 medarbejdere: John og Alice.

    • Eller en afdeling kan opdeles i underafdelinger, som development har to grene: sites og internals. Hver af dem har deres egen medarbejderliste.

    • Det er ogs muligt at en underafdeling vokser og opdeles i underunderafdelinger (eller hold).

      For eksempel kan sites-afdelingen i fremtiden opdeles i hold for siteA og siteB. Og de, potentielt, kan opdeles end mere. Det er ikke p billedet, bare noget at have i tankerne.

    Lad os nu forestille os at vi vil have en funktion der lgger alle lnninger (salaries) sammen. Hvordan gr vi det?

    En iterativ tilgang er ikke nem da strukturen ikke er simpel. En frste id kunne vre at gennemlbe company med en nested subloop over 1. niveau afdelinger. Men s skal vi have flere nested subloops for at gennemlbe medarbejderne i 2. niveau afdelinger som sites Og s en subloop inden i dem for 3. niveau afdelinger som mske vil opst i fremtiden? Hvis vi stter 3-4 nested subloops i koden for at gennemlbe et enkelt objekt, bliver det ret hurtigt svrt at hndtere.

    Lad os prve rekursion.

    Som vi kan se, nr vores funktion fr en afdeling den skal sammentlle, er der to mulige tilflde:

    1. Enten er den en simpel afdeling med et array af medarbejdere da kan vi summe lnningerne i en simpel loop.
    2. Eller den er et objekt med N underafdelinger da kan vi lave N rekursive kald for at f summen for hver af de underafdelinger og kombinere resultaterne.

    Det frste tilflde er basen for rekursionen, det trivielle tilflde, nr vi fr et array.

    Det andet tilflde nr vi fr et objekt er det rekursive trin. En kompleks opgave opdeles i underopgaver for mindre afdelinger. De kan i sin tur opdeles igen, men snarest eller senest vil opdeltningen slutte ved (1).

    Algoritmen er sandsynligvis endnu lettere at lse fra koden:

    let company = { // det samme objekt, komprimeret
      sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 1600 }],
      development: {
        sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }],
        internals: [{name: 'Jack', salary: 1300}]
      }
    };
    
    // Funktionen der udfrer jobbet
    function sumSalaries(department) {
      if (Array.isArray(department)) { // Tilflde (1)
        return department.reduce((prev, current) => prev + current.salary, 0); // sum the array
      } else { // Tilflde (2)
        let sum = 0;
        for (let subdep of Object.values(department)) {
          sum += sumSalaries(subdep); // rekursivt kald til underafdelinger. Sammentl alle deres returnerede resultater
        }
        return sum;
      }
    }
    
    alert(sumSalaries(company)); // 7700

    Koden er kort og (forhbentlig) nem at forst. Det er kraften i rekursionen. Den virker ogs for ethvert niveau af underafdelingsindlejring.

    Her er et diagram over kaldene:

    []

    Her kan vi mske nemmere se principperne: for et objekt {...} udfres subkald, mens arrays [...] er blade i rekursions-tret og giver umiddelbare resultater.

    Bemrk at koden bruger et par af de smarte muligheder som vi har gennemget tidligere:

    • Metoden arr.reduce forklaret i kapitlet Array-metoder returnerer summen af et array.
    • Lkken for(val of Object.values(obj)) itererer over et objekt og Object.values returnerer et array af dens vrdier.

    Rekursive strukturer

    En rekursiv (recursivt defineret) datastruktur er en struktur der replikerer sig selv i dele.

    Vi har lige set det i eksemplet med en virksomhedsstruktur ovenfor.

    En virksomheds afdeling er:

    • Enten et array af medarbejdere.
    • Eller et objekt med underafdelinger.

    For webudviklere er der et meget mere velkendt eksempel: HTML og XML dokumenter.

    I et HTML dokument kan et HTML-tag indeholde en liste af:

    • Tekststykker.
    • HTML-kommentarer.
    • Andre HTML-tags (der i sig selv kan indeholde tekststykker/kommentarer eller andre tags etc).

    Det er igen en rekursiv definition.

    For bedre at forst dette vil vi gennemg en mere kompleks rekursiv struktur kaldet Linked list som kan vre en bedre alternativ til arrays i nogle tilflde.

    Linked list

    Forestil dig, vi vil gemme en ordnet liste af objekter.

    Den naturlige valg er et array:

    let arr = [obj1, obj2, obj3];

    Men der er et problem med arrays. Operationerne slet element og indst element er dyre. For eksempel vil arr.unshift(obj) vre ndt til at renummerere alle elementer for at skabe plads for det nye element obj. Hvis det er et stort array, s tager det tid; det samme vil ske med arr.shift().

    Den eneste modifikation der ikke krver masser af renummerering er de operationer der arbejder med slutningen af arrayet: arr.push/pop. S et array kan vre ret langsomt for store ker, nr vi skal arbejde med starten.

    Alternativt, hvis vi virkelig har brug for hurtig indsttelse/sletning, kan vi vlge en anden datastruktur kaldet en linked list.

    Et linked list element er rekursivt defineret som et objekt med:

    • value vrdien af elementet.
    • next en reference til det nste linked list element eller null hvis det er slutningen.

    For eksempel er her en linked list med 4 elementer:

    let list = {
      value: 1,
      next: {
        value: 2,
        next: {
          value: 3,
          next: {
            value: 4,
            next: null
          }
        }
      }
    };

    Her er en grafisk reprsentation af listen:

    []

    En alternativ kode for oprettelse af den samme linked list kunne vre:

    let list = { value: 1 };
    list.next = { value: 2 };
    list.next.next = { value: 3 };
    list.next.next.next = { value: 4 };
    list.next.next.next.next = null;

    Her kan vi tydeligere se at der er flere objekter, hvor hvert objekt har value og next som peger p naboen. Variablen list er det frste objekt i kden, s ved at flge next-pegerne fra det kan vi n ethvert element.

    Listen kan nemt opdeles i flere dele og senere genoprettes:

    let secondList = list.next.next;
    list.next.next = null;
    []

    For at genoprette den oprindelige liste, skal vi bare forbinde den frste del med den anden:

    list.next.next = secondList;

    Og vi kan indstte eller fjerne elementer hvor som helst.

    For eksempel, for at indstte et nyt element i starten af listen, skal vi opdatere hovedet af listen:

    let list = { value: 1 };
    list.next = { value: 2 };
    list.next.next = { value: 3 };
    list.next.next.next = { value: 4 };
    
    // indst et nyt element i starten af listen
    list = { value: "new item", next: list };
    []

    For at fjerne et element fra midten, ndre next af det forrige element:

    list.next = list.next.next;
    []

    Her fr vi list.next til at hoppe over 1 til vrdien 2. Vrdien 1 er nu fjernet fra kden. Hvis den ikke er gemt et andet sted, vil den automatisk blive fjernet fra hukommelsen.

    I modstning til arrays sker der ingen massiv omnummerering, vi kan nemt rearrangere elementer.

    Naturligvis er denne slags lister ikke altid bedre end arrays. Ellers ville alle bruge kun lister.

    Den store ulempe er at vi ikke nemt kan tilg et element ved dets nummer. I et array er det nemt: arr[n] er en direkte reference. Men i listen skal vi starte fra det frste element og g next N gange for at n det Nte element.

    Men vi har ikke altid brug for sdanne operationer. For eksempel, nr vi har brug for en k eller endda en deque den ordnede struktur der skal tillade meget hurtigt tilfjelse/fjernelse af elementer fra begge ender, men adgang til midten ikke er ndvendig.

    Lister kan forbedres:

    • Vi kan tilfje en egenskab prev i tilfjelse til next for at referere til det forrige element, s man nemt kan g baglns.
    • Vi kan ogs tilfje en variabel kaldet tail som refererer til det sidste element i listen (og opdatere den nr man tilfjer/fjerner elementer fra slutningen).
    • Datastrukturen kan justeres i forhold til specifikke behov.

    Opsummering

    Terms:

    • Rekursion er et begreb i programmering der henviser til at en funktion kalder sig selv. Rekursive funktioner kan bruges til at lse opgaver p en elegant mde.

      Nr en funktion kalder sig selv, kaldes det et rekursions-trin. Basis for rekursion er funktionsargumenter, der gr opgaven s simpel, at funktionen ikke laver yderligere kald til sig selv.

    • En recursivt defineret datastruktur er en datastruktur der kan defineres ved hjlp af sig selv.

      For eksempel kan en linked list defineres som en datastruktur bestende af et objekt der refererer til en liste (eller null).

      list = { value, next -> list }

      Trer (hierarkier) som HTML element-trer eller trer der viser afdelinger i en virksomhed er ogs rekursivt definerede. De har grene og hver gren kan have andre grene.

      Rekursive funktioner kan bruges til at gennemlbe dem, som vi har set i eksemplet med sumSalary.

    Enhver rekursiv funktion kan omskrives til en iterativ funktion. Og det er nogle gange ndvendigt for at optimere ydeevnen. Men for mange opgaver er en rekursiv lsning hurtig nok og nemmere at skrive og vedligeholde.

    Opgaver

    vigtighed: 5

    Skriv en funktion sumTo(n) som beregner summen af tallene 1 + 2 + ... + n.

    For eksempel:

    sumTo(1) = 1
    sumTo(2) = 2 + 1 = 3
    sumTo(3) = 3 + 2 + 1 = 6
    sumTo(4) = 4 + 3 + 2 + 1 = 10
    ...
    sumTo(100) = 100 + 99 + ... + 2 + 1 = 5050

    Opret tre variationer af lsningen:

    1. Ved brug af et for-loop.
    2. Ved brug af rekursion, da sumTo(n) = n + sumTo(n-1) for n > 1.
    3. Ved brug af formlen Differensrkke. Det engelske opslag aritmetisk progression giver en dybere forklaring, hvis du har brug for det.

    Her er et eksempel p resultatet:

    function sumTo(n) { /*... din kode ... */ }
    
    alert( sumTo(100) ); // 5050

    P.S. Hvilken lsning er hurtigst? Og hvilken er langsomst? Hvorfor?

    P.P.S. Kan vi bruge rekursion til at regne sumTo(100000)?

    lsning

    Lsningen ved brug af et for-loop:

    function sumTo(n) {
      let sum = 0;
      for (let i = 1; i <= n; i++) {
        sum += i;
      }
      return sum;
    }
    
    alert( sumTo(100) );

    Lsningen der bruger rekursion:

    function sumTo(n) {
      if (n == 1) return 1;
      return n + sumTo(n - 1);
    }
    
    alert( sumTo(100) );

    Lsningnen der bruger formlen: sumTo(n) = n*(n+1)/2:

    function sumTo(n) {
      return n * (n + 1) / 2;
    }
    
    alert( sumTo(100) );

    P.S. Naturligvis er formlen den hurtigste lsning. Den bruger kun 3 operationer for ethvert tal n. Matematikken hjlper!

    Loop-varianten er den anden i hastighed. I bde den rekursive og loop-variant summerer vi de samme tal. Men rekursionen involverer indlejrede kald og stak-hndtering. Det tager ogs ressourcer, s det er langsommere.

    P.P.S. Nogle motorer understtter tail call optimering: hvis et rekursivt kald er det sidste i funktionen uden andre beregninger udfrt, s vil den ydre funktion ikke behve at genoptage eksekveringen, s motoren behver ikke at huske dens eksekveringskontekst. Det fjerner byrden p hukommelsen. Men hvis JavaScript-motoren ikke understtter tail call optimering (de fleste gr ikke), vil der vre en fejl: maksimal stakstrrelse overskredet, fordi der normalt er en begrnsning p den totale stakstrrelse.

    vigtighed: 4

    Fakultet(engelsk factorial) af et naturligt tal er tallet ganget med tallet minus n, s med tallet minus to, og s videre til 1. Fakultetet af n betegnes som n!

    Vi kan skrive en definition af fakultet som denne:

    n! = n * (n - 1) * (n - 2) * ...*1

    Vrdier af fakultet for forskellige n:

    1! = 1
    2! = 2 * 1 = 2
    3! = 3 * 2 * 1 = 6
    4! = 4 * 3 * 2 * 1 = 24
    5! = 5 * 4 * 3 * 2 * 1 = 120

    Opgaven er at skrive en funktion factorial(n) som beregner n! ved hjlp af rekursive kald.

    alert( factorial(5) ); // 120

    Hint: n! kan skrives som n * (n-1)! For eksempel: 3! = 3*2! = 3*2*1! = 6

    lsning

    Grundlggende kan fakultetet n! skrives som n * (n-1)!.

    Med andre ord kan resultatet af factorial(n) beregnes som n ganget med resultatet af factorial(n-1). Og kaldet for n-1 kan rekursivt falde ned, og ned, til 1.

    function factorial(n) {
      return (n != 1) ? n * factorial(n - 1) : 1;
    }
    
    alert( factorial(5) ); // 120

    Basis af rekursion er tallet 1. Vi kan ogs gre 0 til basis her, det betyder ikke meget, men giver en ekstra rekursiv trin:

    function factorial(n) {
      return n ? n * factorial(n - 1) : 1;
    }
    
    alert( factorial(5) ); // 120
    vigtighed: 5

    Sekvensen af Fibonacci-tal (engelsk Fibonacci numbers) har formlen Fn = Fn-1 + Fn-2. Med andre ord er det nste tal summen af de to foregende tal.

    Frste to tal er 1, s 2(1+1), s 3(1+2), 5(2+3) og s videre: 1, 1, 2, 3, 5, 8, 13, 21....

    Fibonacci-tallene er relateret til det gyldne snit (engelsk Golden Ratio) og mange naturlige fnomener omkring os.

    Skriv en funktion fib(n) som returnerer det n-te Fibonacci-tal.

    Et eksempel p brug er:

    function fib(n) { /* din kode */ }
    
    alert(fib(3)); // 2
    alert(fib(7)); // 13
    alert(fib(77)); // 5527939700884757

    P.S. Funktionen skal vre hurtig. Kaldet til fib(77) br tage ikke mere end et sekund.

    lsning

    Den frste lsning vi kunne prve er den rekursive.

    Fibonacci-tallene er rekursive per definition:

    function fib(n) {
      return n <= 1 ? n : fib(n - 1) + fib(n - 2);
    }
    
    alert( fib(3) ); // 2
    alert( fib(7) ); // 13
    // fib(77); // vil vre meget langsom!

    Men for store vrdier af n er det meget langsomt. For eksempel kan fib(77) f motoren til at hnge op i nogle sekunder og spise alle CPU-ressourcer.

    Det skyldes, at funktionen laver for mange underkald. De samme vrdier evalueres igen og igen.

    For eksempel, lad os se en del af beregningerne for fib(5):

    ...
    fib(5) = fib(4) + fib(3)
    fib(4) = fib(3) + fib(2)
    ...

    Her kan vi se at fib(3) er ndvendig bde for fib(5) og fib(4). S fib(3) vil blive kaldt og evalueret to gange helt uafhngigt.

    Her er hele rekursions-tret:

    []

    Vi kan tydeligt se at fib(3) evalueres to gange og fib(2) evalueres tre gange. Det samlede antal beregninger vokser meget hurtigere end n, hvilket gr det enormt endda for n=77.

    Vi kan optimere det ved at huske allerede evaluerede vrdier: hvis en vrdi som f.eks. fib(3) beregnes n gang, s kan vi bare genbruge den i fremtidige beregninger.

    En anden variant ville vre at give op p rekursion og bruge et helt andet loop-baseret algoritme.

    I stedet for at g fra n og ned til lavere vrdier kan vi oprette en lkke der starter fra 1 og 2, s fr vi fib(3) som summen af dem, s fib(4) som summen af de to foregende vrdier, s fib(5) og gr op og op, indtil det fr den nskede vrdi. P hver trin behver vi kun at huske de to foregende vrdier.

    Her er trinene for den nye algoritme i detaljer.

    Starten:

    // a = fib(1), b = fib(2), disse vrdier er per definition 1
    let a = 1, b = 1;
    
    // Hent c = fib(3) som summen af dem
    let c = a + b;
    
    /* nu har vi fib(1), fib(2), fib(3)
    a  b  c
    1, 1, 2
    */

    Nu vil vi gerne have fib(4) = fib(2) + fib(3)`.

    Lad os skifte variablerne: a,b vil f fib(2),fib(3), og c vil f deres sum:

    a = b; // nu a = fib(2)
    b = c; // nu b = fib(3)
    c = a + b; // c = fib(4)
    
    /* nu har vi sekvensen:
       a  b  c
    1, 1, 2, 3
    */

    Det nste trin giver endnu et nummer i sekvensen:

    a = b; // now a = fib(3)
    b = c; // now b = fib(4)
    c = a + b; // c = fib(5)
    
    /* nu er sekvensen:
          a  b  c
    1, 1, 2, 3, 5
    */

    Og sdan fortstter vi indtil vi fr den nskede vrdi. Det er meget hurtigere end rekursion og involverer ingen duplikerede beregninger.

    Den fulde kode:

    function fib(n) {
      let a = 1;
      let b = 1;
      for (let i = 3; i <= n; i++) {
        let c = a + b;
        a = b;
        b = c;
      }
      return b;
    }
    
    alert( fib(3) ); // 2
    alert( fib(7) ); // 13
    alert( fib(77) ); // 5527939700884757

    Lkken starter med i=3, fordi de frste to sekvensvrdier er hardkodet i variablerne a=1, b=1.

    Denne tilgang kaldes dynamisk programmering (p engelsk dynamic programming bottom-up).

    vigtighed: 5

    Lad os sige, vi har en enkeltstrenget linked list (som beskrevet i kapitlet Rekursion og stak):

    let list = {
      value: 1,
      next: {
        value: 2,
        next: {
          value: 3,
          next: {
            value: 4,
            next: null
          }
        }
      }
    };

    Skriv en funktion printList(list) som udskriver listens elementer t ad gangen.

    Opret to varianter af lsningen: ved brug af en lkke og ved brug af rekursion.

    Hvad er bedst: med rekursion eller uden?

    lsning
    Lkkebaseret lsning

    Lkkebaseret lsning

    Den lkkebaserede variant af lsningen:

    let list = {
      value: 1,
      next: {
        value: 2,
        next: {
          value: 3,
          next: {
            value: 4,
            next: null
          }
        }
      }
    };
    
    function printList(list) {
      let tmp = list;
    
      while (tmp) {
        alert(tmp.value);
        tmp = tmp.next;
      }
    
    }
    
    printList(list);

    Bemrk venligst, at vi bruger en midlertidig variabel tmp til at gennemg listen. Teknisk set kunne vi have brugt en funktionsparameter list i stedet:

    function printList(list) {
    
      while(list) {
        alert(list.value);
        list = list.next;
      }
    
    }

    Men det vil ikke vre klog. I fremtiden kan vi have brug for at udvide en funktion, gre noget andet med listen. Hvis vi ndrer list, s mister vi den evne.

    Nr vi nu taler om gode variabelnavne, er list her listen selv. Det frste element i den. Og det br forblive som det er. Det er klart og plideligt.

    Modsat er rollen for tmp udelukkende en midlertidig variabel til listetraversering, ligesom i i for-lkken.

    Rekursiv lsning

    Rekursiv lsning

    Den rekursive variant af printList(list) flger en simpel logik: for at udskrive en liste skal vi udskrive det nuvrende element list, og derefter gre det samme for list.next:

    let list = {
      value: 1,
      next: {
        value: 2,
        next: {
          value: 3,
          next: {
            value: 4,
            next: null
          }
        }
      }
    };
    
    function printList(list) {
    
      alert(list.value); // udskriver den aktuelle vrdi
    
      if (list.next) {
        printList(list.next); // gr det samme for resten af listen
      }
    
    }
    
    printList(list);

    Hvad er bedst?

    Teknisk set er lkken mere effektiv. Disse to varianter gr det samme, men lkken bruger ikke ressourcer p indlejrede funktionskald.

    Modsat er den rekursive variant kortere og nogle gange lettere at forst.

    vigtighed: 5

    Udskriv en enkeltstrenget linked list fra det forrige opgave Udskriv en enkeltstrenget linked list i omvendt rkkeflge.

    Lav to lsninger: ved brug af en lkke og ved brug af rekursion.

    lsning
    Ved brug af rekursion

    Ved brug af rekursion

    Den recursive logik er en lille smule svr her.

    Vi skal frst udskrive resten af listen og derefter udskrive det nuvrende element:

    let list = {
      value: 1,
      next: {
        value: 2,
        next: {
          value: 3,
          next: {
            value: 4,
            next: null
          }
        }
      }
    };
    
    function printReverseList(list) {
    
      if (list.next) {
        printReverseList(list.next);
      }
    
      alert(list.value);
    }
    
    printReverseList(list);
    Ved brug af en lkke

    Ved brug af en lkke

    Den lkkebaserede variant er ogs en lille smule mere kompliceret end direkte output.

    Der er ingen mde at f den sidste vrdi i vores list. Vi kan ogs ikke g tilbage.

    F det vi kan gre er at gennemg elementerne i den direkte rkkeflge og huske dem i et array, og derefter udskrive det vi huskede i omvendt rkkeflge:

    let list = {
      value: 1,
      next: {
        value: 2,
        next: {
          value: 3,
          next: {
            value: 4,
            next: null
          }
        }
      }
    };
    
    function printReverseList(list) {
      let arr = [];
      let tmp = list;
    
      while (tmp) {
        arr.push(tmp.value);
        tmp = tmp.next;
      }
    
      for (let i = arr.length - 1; i >= 0; i--) {
        alert( arr[i] );
      }
    }
    
    printReverseList(list);

    Bemrk at den rekursive lsning gr prcis det samme: den flger listen, husker elementerne i kden af indlejrede kald (i eksekveringsstakken), og udskriver dem derefter.

    Tutorial-oversigt

    Kommentarer

    ls dette fr du kommenterer
    • Hvis du har forslag til forbedringer - s opret venligst et GitHub-issue eller en pull request i stedet for at kommentere.
    • Hvis du ikke forstr noget i artiklen - s uddyb venligst.
    • For at indstte f ord kode, brug <code>-taggen, for flere linjer - omslut dem i <pre>-tag, for mere end 10 linjer - brug en sandbox (plnkr, jsbin, codepen)

    Web Proxy Viewer  |  New URL  |  Original Page