KONKRET MATEMATIK

 

af  H. B. Hansen

 

 

Det fleste der har lært "højere" matematik er opdraget med de reelle tal, så­dan­ne sager som kon­ti­nu­itet, differentialregning og integralregning. Det er imid­lertid ikke lige netop den slags matema­tik man har mest brug for når man beskæftiger sig med datalogi. I datalogien er alt di­skret - selv de reelle tal re­præsenteres jo som diskrete data, og regning med reelle tal er derfor en hel dis­ciplin for sig, kaldet numerisk matematik.

Når man begynder at interessere sig for algoritmer og deres analyse kom­mer man rigtig i be­kneb, for der slår den kontinuerte matematik overhovedet ikke til. Før 1968 famlede vi os frem, men det år udkom det første bind af Knuths bog: The Art of Computer Programming, og dér åben­ba­redes den matematik der var brug for (Knuth(1968)). Matematikerne har kendt denne mate­matik i hundredvis af år, men den er i det store og hele forblevet deres private gebet, indtil vi dataloger fik det praktiske behov. Desværre viser det sig at den særlige diskrete matematik der er tale om, er vanske­lig at tilegne sig for almindelige mennesker der er opfostret i den kontinuerte tradition, og det har sikkert været et problem for mange dataloger foruden mig selv.

Nu er der imidlertid udkommet en lærebog, Graham(1989), i specielt den­ne form for mate­ma­tik;  den hedder "Konkret Matematik", hvilket er en sam­mentrækning af kontinuert og diskret  mate­ma­tik. Det var en stor inspira­tion for mig at læse denne bog; den gav mig et skub fremad i min for­ståelse og beherskelse af algoritmeanalysens matematik, og derfor fik jeg lyst til at delagtiggøre an­dre i nogle af mine oplevelser med den konkrete matematik. Dette essay hand­ler altså om matema­tik, men jeg vil forsøge at relatere det så meget som muligt til datalo­giske problemstillinger. Der er ikke noget nyt i det jeg siger, så dette er blot en appetitvækker der måske kan inspirere én og an­den til at gå videre i den konkrete matematiks fascinerende verden på egen hånd.

 

Rekursioner

Men lad os med det samme kaste os ud i en typisk problemstilling hvor den konkrete mate­ma­tik kan finde anvendelse. Der findes en simpel sorte­rings­metode der kaldes "indsættelse"; den benytter samme princip som kort­spil­lere der ordner de kort de får på hånden. Man sidder med n-1 kort der i forvejen er ordnet i rækkefølge efter farve og størrelse, og får så det n'te kort, der skal ind i denne rækkefølge.

Datalogisk set er situationen den at man har et array A, hvor de første n-1 elementer er en sti­gende rækkefølge af hele tal, og nu ankommer et nyt tal som skal ind i A på den rette plads. Det kan gøres ved hjælp af følgende algoritme, skrevet i Pascal:

 

            i := n-1;

            while (i > 0) and (A[i] > tal) do begin

              A[i+1] := A[i]; i := i - 1

            end;

            A[i+1] := tal; n := n + 1;

 

Problemet er nu hvor mange gange while-løkken gennemløbes når alle tallene skal sorteres (algo­ritmen ovenfor må udføres for alle tallene efter tur).

Nu er denne algoritme så simpel at man nok kan besvare dette spørgsmål uden store mate­matiske udfoldelser, men lad mig gøre det på den typiske kon­krete måde for demonstrationens skyld. Den konkrete matematiker tænker således:

Lad antallet af operationer når der er n elementer i A være an. For at kom­me til dette antal må vi først sortere n-1 tal (ved hjælp af indsættelse), og derefter anbringe det n'te tal (sådan ville man fak­tisk programmere metoden i fx Prolog). Sortering af de n-1 første tal bruger an-1 operatio­ner, og an­bringelse af det n'te tal vil i gennemsnit kræve ca. n/2 gennemløb af while-løkken, hvis alle tal er li­ge sandsynlige. En gennemsnitsbetragtning siger derfor at der gæl­der følgende sammenhæng:

 

a1 = 0

 

Sådan et sæt ligninger kaldes en rekursion. En rekursion består altid af en ligning der ud­tryk­ker en generel sammenhæng, samt en eller flere start­betingelser. Opgaven består nu i at finde et afsluttet udtryk for an, dvs. en for­mel man blot kan sætte værdien af n ind i.

Man er ude efter den afsluttede formel for an fordi sådan en siger meget mere om hvordan pro­cessen arter sig end en tabel over funktionsværdierne (og så fylder den meget mindre), men sådan set kunne man godt nøjes med rekursionen hvis man vidste hvor mange elementer der skul­le sorteres, for man kan starte med startbetingelsen og arbejde sig skridtvis frem, således:

 

a1 = 0

 

 ,    o.s.v.

 

Vi ser at der fremkommer en talfølge af følgende udseende:

 

0, 2, 5, 9, 14, ...

 

hvor jeg kun har taget tællerne i brøkerne med. Studiet af sådanne talfølger er en vigtig del af den konkrete matematik. Ofte er kendskab til en talfølge nok til at løse opgaven at finde an, for der findes nemlig en bemærkelsesværdig bog der indeholder en fortegnelse over alverdens tal­følger, med angivelse af hvor man kan søge yderligere oplysninger om talfølgen, og/eller an­givelse af det n'te led. Blandt dyrkere af den konkrete matematik kaldes den blot for Sloane, fordi det er en fyr ved navn Sloane der har samlet alt materialet og udgivet det, Sloane(1973). Slår vi op i Sloane finder vi, som talfølge nr. 522:

 

1, 2, 5, 9, 14, 20, 27, ...

 

med angivelsen n(n+3)/2. Nu skal man passe lidt på, for det er ikke sikkert at Sloanes talfølge er præcis den man søger. Han angiver ikke eksplicit hvordan han num­mererer elementerne, og det første element hos Sloane er et 1-tal i stedet for et nul. Der er noget mystisk ved dette 1-tal. Det er som om det skal være det 0'te element, for det næste tal, 2-tallet, stemmer hvis man sætter n = 1:  1*(1+3)/2 = 2. På den anden side kan 1-tallet alligevel ikke være det 0'te element, for det stem­mer ikke med formlen. Disse mærkværdigheder skyldes den måde talfølgerne er systematiseret på i Sloane, således at det blive let at slå op, og man skal derfor i nogle tilfælde foretage visse justeringer af de angivne formler for at få sin egen opgave til at stemme. I vores tilfælde ser vi at man skal forskyde tallene én position således at Sloanes 2-tal for n = 1 bliver til vores 2-tal for n = 2. Løsningen er derfor:

 

 

Man kan overbevise sig om at det er den rigtige løsning man har fundet ved at benytte et indukti­ons­bevis.

 

Startbetingelse: n = 1 giver a1 = 0, ok.

Induktionsskridt:

Højre side: ,

er lig med venstresiden, QED.

 

Men hvad gør man hvis ens talfølge ikke findes i Sloane? Så må vi løse rekursionen. I simp­le tilfælde som det foreliggende kan man "se" sig frem til løsningen ved hjælp af en proces der kaldes teleskopering (efter de gamle søkikkerter, der kunne trækkes ud og klappes sammen):

 

 

Da a1 = 0 findes løsningen altså som summen:

 

½*(2 + 3 + 4 + ... + (n-1) + n)

 

Parentesen er en differensrække med n-1 led, så summen bliver:

 

 

som før.

Dette resultat kan generaliseres. En rekursion af formen:

 

an = an-1 + f(n)

ap = q

ak = 0 for alle k < p

 

hvor f(n) er en kendt funktion af n, har løsningen

 

an = q + f(p+1) + f(p+2) + f(p+3) + ... + f(n)

 

eller skrevet som den konkrete matematiker foretrækker:

 

 

 

Man ser her at optælling er en vigtig operation ved analyse af algoritmer. Der­for er det vig­tigt for en datalog at beherske operationen summation. Dette gælder ikke blot for rekursioner af denne simple form. I alle tilfælde hvor man itererer sig frem i en rekursion ender man med én eller flere summer.

 

Summer

For summer gælder der nogle regneregler der i og for sig er indlysende, men som alligevel kan forvirre begyndere. Her er de vigtigste:

 

Distributive lov:          

 

Associative lov:          

 

Kommutative lov:       

 

hvor p(k) betegner en permutation af værdierne i K (se nedenfor).

Den distributive lov siger at man kan sætte en fælles faktor uden for pa­rentes (eller gange ind). Den associative lov siger at summen af en sum er lig med summen af det ene led plus sum­men af det andet led. Endelig siger den kommutative lov at addendernes orden er ligegyl­dig.

Her må jeg nok sige et par ord om notationen. Grænserne for det løbende indeks for en sum­mation angives som et logisk prædikat under summa­tionstegnet. I ovenstående summer er k det løbende indeks, og K er den vær­dimængde som dette indeks skal tage værdier fra. Opera­toren Î er medlems­operatoren, med betydningen "alle". Hvis fx K = {5,3,2} så står ud­tryk­ket for summen a5 + a3 + a2.

Prædikatet p(k) i den kommutative lov skal betyde "en permutation af K", og denne lov siger altså blot at ovenstående eksempel også kan beregnes som a2 + a3 + a5, eller en hvilken som helst anden orden af addenderne.

Denne prædikatnotation for summationsindeks er praktisk fordi man ofte har brug for at an­give andre betingelser end lige netop heltalsintervaller (Eksempel:), samt fordi man næ­sten altid har behov for at operere på grænserne for en summation når man regner. Fx gælder der en me­get fundamental regel der siger at navnet for summationsindeks er irre­levant, altså:

 

 

Som eksempel på dette vil jeg vise hvordan Gauss’ berømte argument for sum­men af en dif­fe­rensrække tager sig ud i den konkrete matematiks sprog.

 

Opgave: find 

 

Vi skriver først

 

 

og bruger derefter navngivningsreglen, idet vi erstatter k med n-k :

 

 

Betingelsen for k kan omformes til:

 

 

 

altså de samme grænser som før (det er blot summation "bagfra"). Men nu kan vi bruge den as­sociative lov på de to udtryk for S:

 

 

 

Størrelsen (2a + bn) afhænger ikke af k; den er konstant og kan sættes uden for summations­tegnet ifølge den distributive lov. Derfor får vi:

 

 

Vi har altså fundet formlen:

 

 

eller i ord: summen af en differensrække er lig med det halve antal led gange summen af første og sidste led.

Man kan også finde summen af en kvotientrække ved manipulation af sum­formler. Her be­nytter man et generelt princip der hedder perturbation; det går ud på at lægge et ekstra led til den søgte sum, og samtidig trække det første led ud. Se her:

 

Opgave: find

 

 

Ved at løse denne ligning med hensyn til S får man:

 

 

 

hvilket er den kendte formel for summen af de n første led i en kvotientrække med kvotient q ¹1. Hvis q = 1 fås S = a(n+1), hvilket ses umiddelbart af det oprindelige udtryk for S. Mange summer kan findes ved hjælp af per­turbation.

 

Differenser

Der findes en uhyre interessant og nyttig analogi mellem de diskrete talføl­ger vi her beskæf­tiger os med, og de kontinuerte funktioner man arbejder med i differential- og integralregning. Som bekendt defineres differential­kvo­tienten af en funktion f(x) således:

 

 

I differensregning arbejder man med differenser:

 

 

 

som er den "differentialkvotient" man kan få frem hvis x'erne er hele tal og h derfor mindst kan være 1. I differentialregning lærer man at man kan differentiere en potens efter form­len:

 

 

 

men uheldigvis gælder der ikke noget tilsvarende for differenser. Men - og det er det interes­san­te - der findes en bestemt slags "potens" der opfører sig nogenlunde på samme måde som de kontinu­erte potenser i differential­reg­ning, nemlig størrelsen:

 

xm = x(x-1)(x-2) ... (x-m+1)

 

altså et "faldende" produkt af m faktorer. xm kaldes "x i m'te faldende". Der findes også en sti­gende potens, men den vil jeg ikke beskæftige mig med i dette essay. Lad os beregne D(xm):

 

D(xm) = (x+1)m - xm = (x+1)x(x-1)...(x-m+2) - x(x-1)(x-2)...(x-m+1)

          = ((x+1) - (x-m+1))(x(x-1)(x-2)...(x-m+2) = mxm-1

 

Der gælder altså en differensregel for faldende potenser, der er helt analog til differentiati­ons­reglen for almindelige potenser.

Det interessante er nu at ligesom differentiation og integration er hinandens omvendte opera­tioner i den kontinuerte matematik, så er differensdannelse og summation omvendte operationer i differensmatematikken. Der gælder nemlig følgende sætning:

 

g(x) = Df(x) hvis og kun hvis åg(x)dx = f(x) + Const

 

hvor summen er en uendelig sum med hensyn til x. Man kan i lighed med det kontinuerte tilfæl­de definere en "bestemt" sum, , og det gælder at:

 

 

Yderligere gælder der følgende sammenhæng mellem den almindelige sum fra forrige afsnit og den­ne nye "differenssum":

 

 

Altså: differenssummen er den samme som den ordinære sum hvor værdien i øvre grænse ikke medregnes.

Nu kan man så begynde at finde sammenhørende f'er og g'er, og det vil komme til at svare til en tabel over integraler, men blot med summer. Sådanne tabeller findes, og de letter summa­ti­­onsarbejdet betydeligt.

Det viser sig at det er hensigtsmæssigt at definere de faldende potenser også for nul og nega­tive potenser. Defineres de således:

 

x0 = 1

 

får man egenskaber frem der er helt analoge til de ordinære potenser, men dog ikke helt iden­ti­ske. Fx gælder det at

 

xn+m = xn(x-n)m

 

altså næsten den sædvanlige potensregel. Differensreglen

 

Dxn = nxn-1

 

gælder derimod både for positive og negative værdier for n, og summations­reglen

 

 

gælder også, men dog ikke hvis n = -1. Det analoge tilfælde i integralregning fø­rer som be­kendt til den naturlige logaritme, men hvad med differens­reg­ning? Det vi søger er en funktion f(x) hvorom det gælder at

 

 

Det er ikke svært at se at funktionen

 

 

opfylder betingelsen. Denne funktion, som kaldes den harmoniske ræk­ke, er altså den di­skrete matematiks modstykke til den naturlige logaritme. Man har derfor

 

 

 

Den harmoniske række optræder atter og atter når man analyserer algorit­mer. Værdien af H(n) er snævert forbundet med den naturlige logaritme, hvil­ket kan ses af at der ik­ke kan være så frygtelig stor forskel mellem en sum og et integral. Der gælder følgende tilnær­melse:

 

 

hvor g = 0.57721 56649... kaldes Eulers konstant. Udledelse og manipula­tion af sådanne asymptoti­ske formler er også en del af den konkrete matema­tik, som jeg imidlertid ikke vil kom­me yderligere ind på i dette essay.

Som bekendt er funktionen ex  uændret ved differentiation og integration. Findes der mon en tilsvarende "ufølsom" funktion indenfor differensreg­nin­gen? Ja, det gør der, for det er let at se at

 

D(2n) = 2n+1 - 2n = 2n(2-1) = 2n

 

så 2n er den diskrete matematiks eksponentialfunktion.

Lad mig som et eksempel på alt dette finde summen fra vores sorterings­pro­blem ved hjælp af differensteknikken. Det vi skulle finde var:

 

 

Nu er det let at se at k = k1, så vi kan skrive:

 

 

Med anvendelse af denne teknik bliver det et problem at omsætte "alminde­lige" udtryk til fal­dende potenser, men her er der også hjælp at hente i den kon­krete matematik. Der gælder nemlig følgende formel:

hvor koefficienterne til xk  er Stirlingtallene af  2. art. Der findes en simpel algoritme der kan om­sætte et vilkårligt polynomi­um til Sterling-form, som er en form hvor der i stedet for de almindelige potenser står faldende potenser. Hvis a'erne er det givne i nedenstående formel finder algoritmen b'erne:

 

anxn + an-1xn-1 + ... + a1x + a0 = bnxn + bn-1xn-1 + ... + b1x1 + b0

 

b'erne kan da direkte indgå i en sum og blive "integreret".

Algoritmen udformes mest effektivt sådan at den ændrer koefficient­ar­ray'et a til at indeholde b'erne. Den ser da således ud:

 

            for i := 1 to n-1 do begin

              v := a[n];

              for j := n-1 downto i do

                a[j] := v := a[j] + i*v;

            end;

 

Den er simpel, men subtil!

Det er herefter muligt at finde summen af et vilkårligt polynomieudtryk, næ­sten helt meka­nisk. Fx finder vi, da k2 = k2 + k1 ifølge ovenstående algo­ritme, at:

 

 

som stemmer med en af de mange magiske formler man finder i matematiske opslagsværker, og som lærebogsforfattere ofte blot postulerer og beviser ved hjælp af induktion.

 

Binomialkoefficienter

Størrelsen

 

 

der kaldes en binomialkoefficient, spiller en stor rolle ved analyse af algo­rit­mer. Det kom­mer af at er det antal måder på hvilke man kan ud­vælge k elementer af en mængde på n ele­menter. Hvis man fx har en mæng­de på 4 elementer {a,b,c,d}, så kan man udvælge 2 ele­men­ter på  måder, nemlig følgende:

 

{a,b} {a,c} {a,d} {b,c} {b,d} og {c,d}.

 

Regning med binomialkoefficienter er lidt af en kunst, og falder begynde­ren meget svært, men det er såmænd ikke så vanskeligt endda hvis man sæt­ter sig lidt ind i det. Sagen er at bino­mialkoefficienterne har det ligesom de tri­go­nometriske funktioner: der findes et gigantisk for­mel­apparat som man må benytte sig af hvis man vil gøre sig indenfor den ædle binomialidræt.

Jeg kan ikke yde området retfærdighed i denne korte fremstilling, men vil nøjes med at anfø­re de vigtigste formler, heriblandt de formler jeg får brug for i de følgende datalogiske eksemp­ler.

Man kan definere binomialkoefficienter for vilkårlige reelle vær­dier af r, specielt for r < 0, mens k skal være et helt tal. er pr. definition nul hvis k < 0 eller k > r;  dette medfører at uendelige summer af binomialkoefficienter i realiteten bliver til endelige summer, og man behø­ver derfor ikke at være så streng med at holde rede på grænserne for en sum når man arbejder med binomialkoefficienter.

Den første regel for binomialkoefficienter er reglen om symmetri:

 

(1)    

 

Den følger af at udvælgelse af k elementer er det samme som markering af de n-k elementer der ikke skal udvælges.

Den næste regel er reglen om at "sætte udenfor" eller "gange ind", ab­sorb­ti­ons­reglen: 

 

(2)    

 

Den kan let vises ud fra definitionen. Så kommer den fundamentale addi­ti­ons­formel:

 

(3)    

 

som følger af at hvis man fx maler et af de n elementer rødt, så må antallet af må­der hvorpå man kan udvælge k elementer være lig med antallet med det røde element ekskluderet, , plus an­tallet med det røde element inkluderet, . Hvis man itererer lidt på denne additionsformel kan man udlede følgende to summationsformler:

 

 (4)    

 

og

 

(5)    

 

Så findes der også en regel om at skifte fortegn på øverste indeks:

 

(6)   

 

Denne regel er meget vigtig i forbindelse med omformning af binomialudtryk. Der er også form­ler for summer af produkter af binomialkoefficienter, hvor det indeks der summeres over befinder sig forskellige steder i de to fak­to­rer. Jeg nævner de vigtigste; de gælder kun når alle indgående størrelser er hele tal

 

  (7)                                (Vandermondes foldning)

  (8)                          (h ³ 0)

  (9)            (h ³ 0)

 (10)      (h,m,n ³ 0)

 (11)                          (h,m ³ 0; n ³ s ³ 0)

 

Arbejdet med binomialformler består i at overføre de formler man møder, til en af de former man finder i disse regler, hvorefter reglen kan anvendes. Lad os tage et eksempel.

Vi vil se på en algoritme der genererer alle undermængder, hvor n og k er givet. Uden tab af generalitet kan vi tænke os at de n elemen­ter er de hele tal fra 1 til n incl. Man kan gene­re­re undermængderne i leksiko­grafisk orden således:

 

1. Start med undermængden {1,2,...,k}.

2. Fjern det største element der endnu kan gøres større.

3. Erstat det fjernede element med et der er én større.

5. Lad  alle efterfølgende elementer blive  en stigende talrække, begyn­dende med erstatningselementet plus én.

 

Listen af undermængder for n = 5 og k = 3 i leksikografisk orden kommer da til at se således ud:

 

1 2 3

1 2 4

1 2 5

1 3 4

1 3 5

1 4 5

2 3 4

2 3 5

2 4 5

3 4 5

 

Ved at studere denne rækkefølge nøjere kan man komme frem til følgende algoritme:

 

for i := 1 to k do a[i] := i;

! Det var første subset;

while a[1] <> n-k+1 do begin

  i := 1;

  while a[k-i+1] = n-i+1 do i := i+1;

  m := a[k-i+1];

  for j := 1 to i do a[k-i+j] := m+j;

  ! Næste subset står nu i a;

end

 

Vi spørger nu om kompleksiteten af denne algoritme. Den ydre while-løk­ke gennemløbes gange, så kompleksiteten er mindst proportio­nal med denne størrelse, hvilket jo er naturligt nok. Spørgsmålet er om man sæt­ter noget til i den indre while-løkke. I denne løkke bestemmes i som det første indeks i a taget bagfra, hvorom det gælder at a[i] endnu ikke har fået sin maksi­male værdi (som er n-i+1, se eksemplet). Hvor mange gange gen­nemløbes denne løkke pr. gen­nemløb af den ydre løkke?

Først må vi gøre os nogle kombinatoriske overvejelser. Løkken stopper når i har en værdi m, bestemt ved at a[k] = n, a[k-1] = n-1, ... , a[k-m+1] = n-m+1, men a[k-m] < n-m. På dette tidspunkt står der nogle tal i elementerne a[1] til a[k-m], som er sorteret i stigende orden, og hvori tallet n-m ikke kan forekomme (fordi det er det største af de tiloversblevne tal, og i så fald skulle have stået i a[k-m] pga. sorteringen). De tal der faktisk står i a[1] til a[k-m] må være en ud af de kombinationer af de n-m-1 øvrige "små" tal. Men det betyder at middelvær­di­en af i bliver

 

 

 

(1-tallet er der fordi løkken om man så må sige løber 1 for langt før det kon­stateres at den skal stoppe - man kan ikke vide om stopbetingelsen er opfyldt før man har afprøvet den.)

Det handler altså nu om at beregne summen

 

 

Det ser jo skræmmende ud, men lad os se på det. Den største vanskelighed er m'et der skal ganges ind i binomialkoefficienten, for der er ikke umiddel­bart nogle af reglerne ovenfor der ud­taler sig om denne situation. Kan man mon bruge absorbtionsreglen på en eller anden måde? Det nedre indeks, k-m, er i så fald uhensigtsmæssigt - der skulle have stået m. Jeg vil altså have der til at stå m i nedre indeks i stedet for k-m. Det kan jeg gøre ved at udskifte m med k-m:

 

   =

 

Den første af disse summer kan findes ved at benytte (4) med r = n-k-1, og man finder:

 

 

Den anden sum er værre. Hvis vi sætter øvre og nedre indeks uden for, ved anvendelse af (2), forsvinder det famøse m ganske vist, men der kommer blot et andet i stedet for. Men vi kan fjerne det øvre m ved at bruge (6):

 

 

Nu kan vi bruge (2), og vi får herved:

 

 

Nu bruger vi (6) igen, blot den anden vej, og får:

 

 

Det næste skridt er at erstatte m med m+1:

 

 

og ved benyttelse af (4) med r = n-k finder vi endelig

 

 

Hele summen bliver derfor:

 

=

 

hvor jeg har benyttet (3) på knækparentesen. Nu kan vi endelig finde imiddel:

 

 

(omskriv de to binomialkoefficienter til fakultetsnotation og forkort). Vi ser at imiddel < 2 hvis k < (n+1)/2, og det kan man altid opnå ved anvendelse af sym­metrireglen. Med andre ord: kom­pleksiteten er stadig proportional med   - while-løkken skader ikke!

 

Frembringerfunktioner

Vi kommer nu til den del af den konkrete matematik der måske er den allervigtigste for data­lo­giske anvendelser, nemlig frembringerfunktioner­ne. Vi har set at man ofte kommer ud for at skulle manipulere med talfølger, der i princippet er uendelige, når man fx løser rekursi­onsligninger. Sådanne uendelige talfølger er meget upraktiske at arbejde med; det ville være lettere hvis man kunne repræsentere en uendelig talfølge ved hjælp af et "endeligt" udtryk. Frem­bringerfunktioner er netop sådanne afsluttede repræsentationer af (endelige og uendelige) talfølger.

Lad os antage at vi står overfor en talfølge:

 

T = {a0,a1,a2,...,an,...}

 

Vi kan da "pakke talfølgen sammen" ved at skrive leddene op i en potens­ræk­ke:

 

F(z) = a0 + a1z + a2z2 + ... + anzn + ...

 

Det n'te led i talfølgen findes da som koefficient til zn. Størrelsen z skal ik­ke opfattes som et tal, men som en indikator der skiller og nummererer de enkelte led i talfølgen; disse led svarer faktisk til cifrene i et tal med grundtal z. Man er ikke så meget interesseret i talværdien af ud­tryk­ket, men derimod i et afsluttet udtryk for summen, sådan rent formelt. Spørgsmålet om kon­ver­gens af den uendelige række er derfor ikke væsentligt; man forestiller sig ganske simpelt at z har en passende værdi. Fidusen er nu at hvis man kan be­regne summen F(z), så har man et kompakt udtryk for hele talfølgen, og dette udtryk kan man manipulere med efter behov (fx differentiere eller integrere, lægge sammen eller gange med en anden frembringerfunktion etc.).

Ideen med frembringerfunktioner stammer vel oprindelig fra den kombina­to­riske matematik, hvor man ifølge sagens natur arbejder meget med binomi­al­koefficienter. Opgaven at finde antal­let af kombinationer af k ud af n ele­menter, hvor n er givet og k varierer, fører til talfølgen:

 

 

og frembringerfunktionen er:

 

 

men ifølge det velkendte binomialteorem:

 

 

er denne potensrække lig med:

 

 

Bemærk at hvis n er et positivt helt tal er summen i virkeligheden endelig, fordi binomial­ko­ef­feicienterne til venstre for og til højre for er lig med nul. Hvis n er negativ eller reel fås en uendelig ræk­ke, men formlen for F(z) gælder stadig. Denne formel spiller en stor rolle i he­le teorien for frembringerfunktioner.

Men lad os se på et eksempel. Jeg begyndte med at fundere over komplek­si­teten af sortering ved indsættelse, og kom frem til talfølgen:

 

 

Den hertil svarende frembringerfunktion må være:

 

 

Kan vi beregne denne sum? Ja, det kan man godt, men det er lidt uoverskue­ligt, så jeg vil hel­le­re regne baglæns. Jeg påstår at

 

 

og vil vise at koefficienten til zn er lig med . Til den ende ud­vikler jeg leddene i den krøl­lede parentes efter binomialformlen. Først det før­ste led:

 

 

hvor jeg i sidste linie har skiftet fortegn for øvre indeks i binomialkoefficien­erne, reguleret led­denes fortegn, og anvendt symmetrireglen for binomialko­ef­ficienter, (1). På samme måde fås:

 

Denne regel er værd at huske; den udtrykker summen af en uendelig kvotient­ræk­ke med kvoti­enten z:

 

 

Nu kan vi finde koefficienten til zn i udtrykket for F(z) (man ganger ind med z/2):

 

 

Fordelen ved denne metode er at den er helt rutinemæssig; man kan næsten lave et program der finder det n'te led ud fra en frembringerfunktion.

Men hvordan fandt jeg frem til frembringerfunktionen F(z)? Ja, det er en af de smarte ting ved hele teorien. Man kan nemlig finde en frembringer­funktion hvis man blot kender en rekur­sionsligning, og det gør vi jo. Jeg gen­ta­ger den her for at have den for øje:

 

 

Én fremgangsmåde er følgende:

 

1) Reducer rekursionen til én enkelt ligning, der gælder for alle n.

 

Det gælder altså om at samle de to udtryk for a i ét. Vi kan selv bestemme hvad a skal være til venstre for n = 1, og vælger arbitrært a0 = a-1 = a-2 = ...  = 0. Men så bliver a1 = 1/2 efter før­ste ligning, hvilket er 1/2 for meget. Før­ste ligning må med andre ord modificeres for at få det hele til at stemme. Hvis det havde været C++ eller Java var det let klaret:

 

a[n] = a[n-1] + n/2 - (n == 1? 1/2 : 0);

 

Vi mangler en "matematisk" notation der giver udtryk for det samme. I den konkrete mate­ma­tik har man adopteret en notation der skyldes Kenneth Iver­son, der i sin tid indførte den i programmeringssproget APL. Den går ud på at man kan skrive et logisk prædikat i knækparen­tes, fx [n lige]. Værdien af et sådant udtryk skal være 1 hvis prædikatet er true og ellers 0. Nu kan første rekursionsligning skrives:

 

 

Det var første skridt.

 

2) Multiplicer ligningen med zn og summér over alle værdier af n (dvs.  fra minus uendelig til uendelig).

 

Vi får da:

 

 

3) Find frembringerfunktionen F(z) = åanzn ud fra den fundne ligning

 

Vi ser at venstre side af ligningen er lig med F(z). Første led på højre side kan skrives:

 

 

da summen går over alle n. Det sidste led giver kun noget når n  = 1, dvs. z/2. Heraf fås:

 

 

For at finde F(z) må vi nu beregne summen ånzn. Det simpleste er at slå op i en tabel. Sådan én findes fx i Graham(1989). Her finder vi følgende:

 

talfølge

frembringerfunktion

sluttet form

1, 2, 3, 4, 5, 6, ...

 

 

som er næsten det rigtige. Hvis vi ganger med et ekstra z får vi den ønskede funktion frem. Den sluttede form for leddet  bliver derfor:

 

 

Man kan selvfølgelig også give sig i kast med at finde summen "the hard way". I dette tilfæl­de kan man benytte et trick som hyppigt kommer på tale i praksis, nemlig at under meget gene­relle betingelser, som næsten altid vil væ­re opfyldt i praksis, vil  integralet af en sum være lig med summen af integra­let. Vi skriver:

 

ånzn = zånzn-1

 

Ved integration af leddene under summationstegnet fås:

 

 

Denne sum er den uendelige kvotientrække fra før med summen:

 

 

Den oprindelige sum fås herefter ved differentiation:

 

som ved multiplikation med den manglende faktor giver samme resultat som vi kunne slå op i tabellen. Endelig kan vi så opskrive frembringerligningen:

 

 

og vi finder:

 

 

i overensstemmelse med hvad jeg påstod ovenfor.

Relevansen af alt dette her er, at vi kan se en generel metode til løsning af rekursionsligninger tone frem. Hvis jeg var begyndt med rekursionsligningen og havde fundet frembringerfunk­ti­onen, og derefter havde udviklet den i en potensrække, ville jeg være kommet frem til løs­ningen. Det eneste ikke-ruti­ne­prægede skridt i hele processen består i at finde summen ånzn, og den kunne man endda slå op i en tabel.

 

Sandsynligheder

Sandsynlighedsovervejelser spiller en stor rolle når man analyserer algorit­mer. Vi har allere­de set et eksempel i analysen af sortering ved indsættelse, hvor leddet n/2, der repræsenterer arbejdsmængden ved at indsætte et nyt ele­ment i en sorteret følge af n elementer, er resultatet af en sandsynligheds­overvejelse.

Den sandsynlighedsregning der er aktuel i forbindelse med algoritmeanaly­se er altid af den slags, hvor det drejer sig om at bestemme sandsynligheden for visse diskrete hændelser. Her er det sådan at man har en vis mængde af elementære hændelser der udgør et sandsynligheds­rum, det univers man arbejder indenfor, og til hver hændelse er der knyttet en vis sandsyn­lig­hed, som er et reelt tal mellem 0 og 1 incl. Summen af sandsynlighederne i et givet sandsyn­lig­hedsrum er altid 1.

Kast med en terning er et godt eksempel. Der er 6 elementære hændelser, og hvis terningen ikke er en snydeterning har alle hændelser den samme sand­synlighed 1/6. Rigtige terninger vil altid være mere eller mindre skæve, så denne sandsynlighedsfordeling er en abstraktion, men det ser man ofte bort fra i sandsynlighedsberegninger.

Sandsynlighedsberegninger drejer sig nu bl.a. om at finde sandsynlighe­der for sammensatte hændelser, fx sandsynligheden for at få 12 øjne ved to kast med én terning, eller ved ét kast med to terninger. Det er ikke på forhånd givet at disse to sammensatte hændelser har den samme sandsynlighed, men det har de tilfældigvis fordi de to elementære hændelser ved to kast med én terning er indbyrdes uaf­hængige. Så kan man nemlig bruge den første grundregel for sandsynlig­heds­reg­ning: sandsyn­ligheden for "både og" er lig med produktet af sand­synlighederne, og vi finder:

 

S(tolv) = S(seks)*S(seks) = 1/6*1/6 = 1/36

 

Hvis man kaster med to terninger er der 6 muligheder for den ene terning og for hver af dem 6 muligheder for den anden terning, altså i alt 6*6 = 36 udfald; kun ét udfald giver 12 øjne, og sandsynligheden bliver igen 1/36.

Denne sandsynlighed fortæller også noget andet, nemlig at hvis man kaster to terninger man­ge gange efter hinanden, så vil de vise to seksere ca. hver 36. gang. Der vil være en vis afvigel­se i denne regelmæssighed, og det kunne væ­re af interesse at vide hvor stor denne afvigelse er. Den slags spørgsmål gi­ver sandsynlighedsregningen svar på.

Nu skal dette essay ikke være et kursus i sandsynlighedsregning, og jeg vil derfor indskræn­ke mig til en kort omtale af de vigtigste regneregler og definitioner. Lad a og b være to hændel­ser med sandsynligheder p(a) og p(b). Da er:

 

p(a and b) = p(a)p(b)

 

hvis hændelserne er indbyrdes uafhængige. Endvidere gælder generelt:

 

p(a or b) = p(a) + p(b) - p(a and b)

 

som reduceres til

 

p(a or b) = p(a) + p(b)

 

hvis hændelserne udelukker hinanden.

Et vigtigt begreb i hele teorien er en stokastisk variabel. Herved forstås en størrelse der ved forskellige iagttagelser af samme kategori af hændelser kan antage forskellige værdier. Øje­antallet ved kast med en terning er en sto­kastisk variabel, og antallet af sammenligninger for en sorteringsalgoritme er en stokastisk variabel. Ved middelværdien af en stokastisk variabel X, der kan antage værdierne i mængden M = {x1,x2,x3,...,xn} forstås summen:

 

 

E(X) står for "expected value of X", og er den værdi af X man vil få frem "i snit" ved mange eksperimenter. Der gælder følgende simple regler for mid­delværdier:

 

E(X + Y) = E(X) + E(Y)

 

hvor X og Y er stokastiske variable for samme sandsynlighedsrum. Hvis k er en konstant gæl­der endvidere:

 

E(kX) = kE(X)

 

Multiplikationsreglen er mere indviklet, men hvis X og Y er indbyrdes uaf­hæn­gige gælder den simple regel:

 

E(XY) = E(X)E(Y)

 

Afvigelser fra middelværdien måles ved hjælp af variansen:

 

V(X) = E((X - E(X))2)

 

På grund af potensopløftningen bliver varianserne ofte store tal, så det er almindeligt at man i stedet regner med spredningen:

 

 

Hvis X er en stokastisk variabel der kun antager positive værdier, kan man bekvemt sam­menfatte en hel sandsynlighedsfordeling ved hjælp af en frem­bringerfunktion:

 

 

En sådan frembringerfunktion indeholder al information om X. Alle koeffici­enter er positive sandsynligheder, og deres sum er lig med 1. Middelværdien af X kan beregnes ud fra frembrin­gerfunktionen ved at differentiere mht. z og derefter sætte z = 1:

 

 

På tilsvarende måde kan man vise at variansen er

 

 

Lad os afprøve disse ideer på et simpelt eksempel. Hvor længe skal man i gen­nemsnit slå med en terning for at få en sekser?

Lad sandsynligheden for at få 6 være p, og sandsynligheden for at få no­get andet være q = 1-p. Frembringerfunktionen for den stokastiske variable X = "antal kast indtil første sekser" bli­ver da:

 

Heraf finder vi:

 

og:

 

 

Ved indsættelse af p = 1/6 fås:

 

E(X) = 6

V(X) = 30

 

At middelværdien var 6 ville man jo nok have gættet på, men vi ser at den­ne middelværdi har en stor spredning, nemlig = ca. 5.5, hvilket ikke er særligt indlysende.

 

Med de redskaber til rådighed som jeg har gennemgået i dette essay, er det muligt at løse gan­ske vanskelige kompleksitetsproblemer. Jeg vil vise et ek­sem­pel der handler om en særlig da­tastruktur der er velegnet til repræsenta­tion af en prioritetskø.

Datastrukturen hedder et p-træ. Et p-træ er et binært træ der kan være tomt. Venstregrene­ne i forhold til en rod danner en ordnet følge af knuder i stigende prioritetsorden, den såkaldte venstresti. Til hver knude i venstre­stien (med undtagelse af den sidste) er der knyttet et p-træ, det såkaldte høj­retræ. Alle knuderne i det højretræ der er knyttet til knuden x i venstrestien, har prioriteter der ligger mellem prioriteten af x og prioriteten af x's efter­følger i venstrestien, se fig. 1.

 

 

 

Fig. 1. Et p-træ med 7 knuder, der er indsat i ræk­ke­følgen 6, 5, 7, 1, 2, 4, 3.

 

 

Indsættelse af en ny knude i et p-træ kan ske som vist i nedenstående ob­jekt­orienterede al­go­ritme, programmeret i C++. Der er to slags objekter: selve træet og knuderne. Træet peger på en "spøgelsesknude" ved hjælp af attributten root; denne u­egent­lige første knude er der for at undgå at roden i det egentlige p-træ kom­mer til at indtage en særstilling der ville komplicere algoritmerne. Hver knu­de har en nøgle (key), der er prioriteten for knuden, samt to referencer, left og right, der peger på hhv. efterfølgeren i venstrestien, og højretræet. Af hensyn til algoritmen har hver knude og­så en attribut der peger på dens far (father). Det er let at se ud fra algoritmen at det element, der har den laveste prioritet, vil stå forrest i venstestien, mens det element, der har den højeste prioritet, vil stå sidst i venstrestien.

Problemet er nu at beregne det gennemsnitlige antal sammenligninger for at indsætte et nyt element i et p-træ. En sådan undersøgelse kan deles i to dele, nemlig svarende til indsættelse i ven­strestien og svarende til indsættelse i højretræet. Jeg vil her nøjes med at se på indsættelse i venstrestien.

 

 struct Pnode

 {

   Pnode *left,*right,*father;

   int key;

 

   Pnode(int k)

   {

     left = right = father = 0;

     key = k;

   }

   void insert(Pnode* x);

   void traverse(int n);

 };

      

 struct Ptree

 {

   Pnode *root;

 

   //Roden har en nøgle der er mindre end alle andre nøgler

   Ptree()

   { root = new Pnode(-1); }

   void insert(int k);

   void traverse();

 };

 

 void Ptree::insert(int k)

 {

   Pnode *node = new Pnode(k);

   //Hvis der allerede er et træ, indsætter vi i det

   if (root->left) (root->left)->insert(node);

   //og ellers danner vi et.

   else root->insert(node);

 }

 

 void Pnode::insert(Pnode* x)

 {

   if (key > x->key) //Vi har nået indsætningspunktet

   {

     //Det er enten en venstre- eller en højregren

     //x indsættes foran this

     if (father->left == this) father->left = x;

     else father->right = x;

     x->left = this; x->father = father; father = x;

     return;

   }

   if (!left) //Har vi nået enden af en venstregren?

   {

     left = x; x->father = this;

     return;

   }

   //Her kommer skanning af en venstregren

   if (left->key < x->key) { left->insert(x); return; }

   //Her går vi ind i et højretræ

   if (right) { right->insert(x); return; }

   //Højretræet var tomt

   right = x; x->father = this;

   return;

 }

 

 

Som det kan ses af algoritmen er indsættelse i venstrestien faktisk en (rekursiv) line­ær søgning ned gen­nem left-pointerne, og det kan derfor være interessant at finde ud af hvor lang venstre­stien bliver i gennemsnit. Det er klart at hvis n knuder indsættes i nummerorden, så vil venstre­stien have længden n, og ind­sæt­telse af hver knude sker bagest i venstrestien; det er ikke særlig hensigts­mæs­sigt. Strukturen er altså kun egnet hvis nøglerne ankommer i mere eller mindre til­fældig orden.

Med baggrund i ovenstående betragtninger opstiller jeg nu en sandsynlig­heds­model, der skal repræsentere den tilfældige ankomst af knuder til træet. For det første tænker jeg mig, at det er de lige tal fra 2 til 2n der er prioriteter, og at der ikke er gengan­gere blandt tallene. Endvidere tænker jeg mig at alle permutationer af disse tal er lige sandsyn­li­ge; en bestemt permutation har altså sandsynligheden.

Hvis man prøver at opbygge p-træer svarende til alle n! mulige rækkeføl­ger af indsættelser får man en mængde af p-træer, T, men der vil være færre end n! p-træer i T, da der er mange ræk­kefølger der fører til samme p-træ.

Den gennemsnitlige effektivitet af algoritmen kan nu måles ved at beregne antallet af sam­men­ligninger der bliver udført for at indsætte et tilfældigt ud­valgt element fra mængden

 

M = {1,3,5,...,2n+1}

 

Altså de ulige tal, i et tilfældigt udvalgt træ fra mænden T.  Bemærk at der ikke er nogle sam­men­fald af værdier mellem M og elementerne i T.

Elementerne i M vælges helt tilfældigt, så hvert element har samme sand­synlighed, , for at blive valgt. Lad nu pn,k være sandsynligheden i T for et p-træ med n knuder og en ven­stresti af længde k. Når man ind­sætter det valgte element fra M i sådan et p-træ, bliver venstrestien 1 større hvis det valgte element var enten 1 eller 2n+1 (sandsynlighed ), mens den er uændret i alle andre tilfælde (sandsynlighed). Træet vil dog under alle omstændig­heder indeholde n+1 elementer ef­ter indsættelsen. Man kan derfor opstille følgende rekursion for pn,k:

 

 

Startbetingelsen  følger af, at et p-træ med 2 knuder altid har en venstresti på 2, og ingen høj­retræer. P-træer med færre end 2 knuder ser jeg bort fra - de er uinteressante og forplumrer blot beregningerne. Nu indfører jeg frembrin­ger­funktionen:

 

 

og ved summation og omordning af leddene i rekursionsligningen fås:

 

 

Nu er

 

 

Her er  sandsynligheden for at et p-træ med n elementer har en venstresti på 1; men den er nul ifølge forudsætningerne. pn,n er sandsynligheden for at venstrestien er n lang i et p-træ med n knuder, dvs. at pri­oriteterne er indsat i stigende eller faldende rækkefølge. Denne sandsynlighed bliver meget lille, , når n vokser, så vi kan uden de store fejl se bort fra leddet pn,nzn, så meget mere som det skal trækkes fra og derfor bidrager til at forbedre det endelige resultat. Ved at se bort fra led­det får vi en løsning der er lidt dårligere end den virkelige, men hvis denne til­nærmede løsning er god nok er alt jo i orden. Vi finder derfor:

 

(n+1)Gn+1(z) = (n-1)Gn(z) + 2zGn(z)

G2(z) = z2

 

Dette er en rekursion udfra hvilken Gn(z) kan findes, men det er ikke nød­vendigt i vores til­fælde. Vi ved at Gn'(1) er middelværdien af k, dvs. middel­værdien af venstrestiens længde, så vi kan blot differentiere rekursions­lig­ningen og sætte z = 1. Dette fører til:

 

(n+1)Gn+1'(z) = (n-1)Gn'(z) + 2Gn(z) + 2zGn'(z)

 

Nu er

 

 

da summen er over samtlige muligheder for k (pn,1 er nul). Derfor får vi:

 

(n+1)Ln+1 = (n-1)Ln + 2 + 2Ln

 

hvor Ln er middellængden af venstrestien i et p-træ med n knuder. Ved om­ordning af leddene fås:

 

L2 = 2

 

hvor startbetingelsen stammer fra differentiation af G2(z). Denne rekursion er af den simple ty­pe vi tidligere har stiftet bekendtskab med. Den kan skrives på formen:

 

L2 = 2

 

og løsningen er:

 

 

Vi ser at venstrestien (og dermed søgningen i venstrestien) kun vokser loga­rit­misk med n.

Nu kan man gå videre og se på søgningen i højretræerne. Det er noget van­skeligere, men kan gennemføres med de teknikker jeg har omtalt. Jeg hen­viser den interesserede læser til Jonassen(1975).

 

 

Multipel indsættelse

Sortering ved indsættelse, som var det første eksempel i dette essay om kon­kret matamatik, kan generaliseres som først beskrevet af Isaac & Sing­leton, se Isaac(1956). Ideen er at arbejde med m grupper der hver for sig sor­teres ved indsættelse. De n elementer, x(1..n), der skal sor­teres, fordeles på de m grupper ved hjælp af en fordelingsfunktion, f(x), der beregner et gruppe­num­mer ud fra argumentet x. Funktionen f(x) skal være monotont vok­sende, således at den skiller elementerne i disjunkte klasser. Idet insertion(x) er den algoritme der er angivet i begyndelsen af dette essay, og tænkes at være en me­tode i klassen gruppe, kan algoritmen be­skri­ves således i et Pascal-lignende pseudosprog:

 

for k := 1 to m do L[k] := new gruppe;

for i := 1 to n do begin

  k := f(x(i)); L[k].insertion(x(i))

end;

// Nu står de sorterede elementer i L[1] til L[m]

  sat i forlængelse af hinanden;

 

Det er klart at kompleksiteten af denne algoritme er mindst O(n) på grund af i-løkken. Spørgs­målet er hvor galt det går når insertion kaldes inden i den­ne løkke. Som vi fandt frem til i starten er kompleksiteten af insertion O(l2) hvis der er l elementer at sortere. Lad os derfor finde middelværdien af l2, hvor l er middellængden af en gruppe.

Vi ser derfor nu på én af de m grupper, med det formål at finde frembrin­gerfunktionen G(z) for længden l af denne gruppe. Hvis alle permutationer af elementer i x-array'et er lige sandsyn­lige, har ethvert element sandsynlig­heden  for at ende i den betragtede gruppe og sandsynligheden  for at havne i en af de andre grupper. Frembringer­funk­tionen for ét x kan derfor udtrykkes som . Da der er n indbyrdes uaf­hængige x'er i alt bliver frembringerfunktionen for hele processen:

 

Opgaven består nu i at beregne E(l2) ud fra G(z). Vi har tidligere set at E(l) = G'(1) og V(l) = G''(1) + G'(1) - G'(1)2. Nu gælder det imidlertid at va­riansen

 

V(l) = E((l - E(l))2) = E(l2 - 2lE(l) + E(l)2)

= E(l2) - 2E(l)E(E(l)) + E(E(l)2)= E(l2) - 2E(l)E(l) + E(l)2

= E(l2) - E(l)2

 

hvor jeg har benyttet at E(l) er en konstant, og at det derfor må gælde at E(E(l)) = E(l), og ana­logt for E(l)2. Men dette viser at:

 

E(l2) = V(l) + E(l)2 = G''(1) + G'(1) - G'(1)2 + G'(1)2

            = G''(1) + G'(1)

 

Differentiation af frembringerfunktionen giver nu:

 

 

og vi finder sluttelig:

 

 

Denne formel er et udtryk for middelværdien af tiden for sortering af en gruppe. Størrelsen  er det gennemsnitlige antal elementer pr. gruppe. Vi kender ikke spredningen på denne størrelse, den afhænger af datamaterialet, men forudsætningen for metoden var, at vi kendte vores data så godt, at vi kunne vælge en funktion f(x), der skiller dataene i m grupper, der er omtrent lige store. Hvis denne forudsætning holder stik, er  tilnærmelsesvis konstant, og vi kan skrive:

 

E(l2) = ca. c(1+c) = O(1)

 

Vi har hermed vist at man med denne metode kan sortere n elementer i en tid der er pro­por­tional med n, mod sædvanligvis nlog(n). Hvis man ikke har en god funktion, der adskiller dataene effektivt, er metoden farlig. Hvis alle elementer falder in­denfor samme gruppe bliver sorteringstiden O(n2), så metoden er meget følsom overfor skævheder. Man ser også at c skal være så lille som muligt, dvs. så stort m som muligt. Den hurtigste proces fås for c = 1, dvs. m = n, hvilket stemmer med, hvad man ville forvente (der bliver ét element i hver gruppe, og sortering af ét element tager ingen tid).

 

 

Quicksort

En af de mest berømte algoritmer i nyere datalogi er den sorteringsalgorit­me der går under navnet quicksort (Hoare(1962)). De fleste lærebøger i data­lo­gi omtaler denne metode, og der er også mange der gør et forsøg på at analysere den. Det er imidlertid min erfaring, at disse analy­ser ofte ikke er helt nøjagtige, selv om hovedresultatet - nemlig at quicksort har kompleksiteten O(nlog(n)) - jo uvægerligt nås. Lad mig slutte dette essay med en analyse af quicksort hvor jeg tager udgangspunkt i en konkret realisering af algoritmen, og hvor jeg forsøger på ikke at gøre nogle tilnærmelser undervejs i analysen. Da quicksort bygger på ombytning af elementer, og ombytning er en kostbar proces i en sekventiel maskine, vil jeg både finde gennemsnit for sam­men­ligninger og ombytninger.

Algoritmen, der ses nedenfor, sorterer de elementer der befinder sig i array a mellem indeks­værdier low og high incl. i stigende orden. Først findes dele­værdien v som det midterste tal i den mængde, der skal sorteres. Man kan og­så blot tage det første tal eller et tilfældigt af tallene. Hvad man skal gøre er ble­vet diskuteret meget i tidens løb, men det synes som om Sedgewick har fundet de vi­ses sten når han i Sedgewick(1978) foreslår at vælge medianen af de tre første tal. Men jeg væl­ger altså det midterste tal som deleværdi. Valget spiller ingen rolle for den over­ordnede kom­plek­sitet af algoritmen, kun for finjustering til maksimal ydelse.

 

procedure quicksort(var a: array; low,high: integer);

  var i,j,v,t: integer;

begin

  i := low; j := high;

 v := a[(i+j) div 2];

  while i < j do

  begin

    while a[i] < v do i := i+1;

    while v < a[j] do j := j-1;

    if i <= j then begin

      t := a[i]; a[i] := a[j]; a[j] := t;

      i := i+1; j := j-1

    end

  end;

  if low < j then quicksort(a,low,j);

  if i < high then quicksort(a,i,high)

end ;

 

De to pegepinde, i og j, bevæges nu ind imod hinanden indtil man finder to elementer, a[i] og a[j], der står "forkert" i forhold til deleværdien; a[i] er større end eller lig med deleværdien og a[j] er mindre end eller lig med dele­værdien. Da selve deleværdien jo befinder sig midt i den mængde der skal sor­teres, vil denne betingelse altid blive opfyldt på et eller andet tidspunkt, og det er bl.a. denne egenskab der er med til at gøre algoritmen hurtig (man behøver ingen ekstra test på om i og j krydser hinanden i de inderste while-løkker, de er så "minimale" som vel tæn­kes kan).

Nu kommer så undersøgelsen af om de to fundne elementer skal ombyttes; det skal de hvis i ligger til venstre for j, altså hvis der ikke er krydsning af pe­gepindene. I algoritmen testes der på om i <= j, men hvis i = j sker der en om­bytning af et element med sig selv - en ubehagelig in­effektivitet! Grunden er at jeg skal have ændret værdierne af i og j også i dette tilfælde, sådan at der konstateres en krydsning i den ydre while-løkke. Det kan naturligvis effekti­viseres, men det spiller heller ingen rolle for selve analysen.

Når en krydsning af i og j er konstateret vil hele datamængden være opdelt på en sådan måde, at alle de elementer der ligger til venstre for j er mindre end v, og alle de elementer der ligger til højre for i er større end v. Disse del­mængder kan nu sorteres hver for sig ved hjælp af quick­sort.

Nu til selve analysen. Først antallet af sammenligninger. Først opstiller jeg en sandsynlighedsmodel; den er den samme som jeg brug­te i forbindelse med p-træerne: det er tallene fra 1 til n der skal sorteres, og alle startpermutationer er lige sand­synlige.

Antallet af sammenligninger for at sortere n elementer kalder jeg an. Det kræ­ver n-1 sammen­ligninger at få i og j til at krydse hinanden, og når det sker er datamængden delt i to. Lad k være indeks for delepunktet; der er da k-1 elementer i den ene del og n-k elementer i den anden (k-1+n-k = n-1, og selve deleværdien går fra). Sortering af disse delmængder kræver hhv. ak-1 og an-k sammenligninger. Da enhver værdi af k har samme sandsynlighed, nemlig 1/n, kan mid­del­værdien af antallet af sammenligninger beregnes ved at finde gennemsnittet taget over k-vær­dierne, således:

 

 

Dette er den første ligning i en rekursion for kompleksiteten af quicksort. Det ser jo frygt­ind­gydende ud, men det viser sig at man let kan reducere ligningen til noget kendt - eller i hvert fald omtrent til noget vi har set før. Jeg tager først fat på det sidste led og bruger (bl.a.) den kom­mutative lov for sum­mation:

 

 

Herved er sidste led i rekursionsligningen omdannet til at være af samme form som næstsidste led, og vi kan skrive:

 

 

Det skabte en smule luft, men stadig er der et n i nævneren på en brøk, samt - værst af alt - den alarmerende sum. n'et er hurtigt klaret:

 

 

Summen kan vi slippe af med ved hjælp af et trick. Vi opskriver rekursi­onen for et n der er 1 mindre:

 

 

og så trækker vi de to ligninger fra hinanden:

 

nan - (n-1)an-1 = n(n-1) - (n-1)(n-2) + 2an-1

 

idet alle led i summerne med undtagelse af det sidste i den øverste ligning op­hæver hinanden. Ved ordning af leddene i denne nye ligning fås:

 

nan = (n+1)an-1 + 2(n-1)

 

Pyha! Nu kan vi ånde lettet op; det begynder at ligne noget vi kender. Det er bare ærgerligt at koefficienterne til an og an-1 er forskellige. Kan vi mon ik­ke få dem gjort ens? Det vi gerne vil er at kunne skrive noget i retning af an = an-1 + f(n), for det er den type ligninger vi kender. Vi prøver derfor at multi­plicere på begge sider af lighedstegnet med en eller anden funktion af n, sn:

 

nsnan = (n+1)snan-1 + 2sn(n-1)

 

Det gælder nu om at finde sn således at leddet (n+1)sn kan omdannes til (n-1)sn-1, for så kan man, ved at foretage substitutionen bn = nsnan, få omdannet ligningen til den simple form vi kender. Derfor kræver vi at (n+1)sn = (n-1)sn-1, dvs.

 

 

Denne rekursion kan man nu iterere, og man finder:

 

 

Med denne værdi af sn omdannes ligningen til:

eller:

hvor jeg har tilføjet en passende startbetingelse, der passer med min algoritme (low = high i y­der­ste inkarnation).

Denne metode til at transformere en rekursion over i noget simplere er helt generel. Faktoren sn kaldes en summationsfaktor, og brugen af summati­ons­faktorer kan ofte hjælpe én med at løse stygge rekursioner. I mange artik­ler og lærebøger bliver summationsfaktorerne nærmest trukket op af en høj hat, som en slags tryllekunst, men som man ser er der ikke tale om magi; sum­ma­tionsfaktorer kan findes helt mekanisk.

Variabelsubstitutionen  fører rekursionen over i:

der har løsningen:

Summen kan deles i to ved at dividere op i hvert led i k-1:

Lad os tage disse summer én ad gangen.

Løsningen til b-rekursionen er derfor:

og da an = (n+1)bn fås endelig:

 

an = 2(n+1)H(n+1) - 2(2n+1)

Det var en ret kompliceret beregning, og kan man nu være sikker på at der ikke er begået nog­le regnefejl undervejs? Det kan let undersøges ved at iterere sig frem i den oprindelige re­kur­sion for små værdier af n, og sammenligne med den værdi man får ved at sætte ind i den slut­tede form. Jeg har gjort det for de første fire værdier af n, og fandt: a1 = 0, a2 = 1, a3 = og a4 = ud fra begge formler. Prøv selv; det er fascinerende at se hvordan helt for­skel­lige mellemregninger i de to formler på magisk vis fører til samme slut­resultat.

Da H(n) har kompleksiteten O(log(n)) ser vi at quicksort har kompleksiteten O(nlog(n)), som det hævdes af al­le autoritative kilder. Man kunne måske nok komme frem til et omtrentligt resultat på en nem­mere måde, men nu er vi i hvert fald sikre på vores bedømmelse.

Men nu til antallet af ombytninger, bn. Hvis sandsynligheden for, at dele­punktet kommer til at ligge ved indeks k, og der da er udført netop t ombyt­ninger, kaldes pk,t, så må man i analogi med sammenligningstilfældet kunne skrive:

Ligningen er blot en dannelse af middelværdien af den stokastiske variable "antal ombyt­nin­ger", efter den sædvanlige formel: sandsynlighed gange antal. Vanskeligheden er at der nu er et t der varierer, mens vi før blot havde et konstant antal sammenligninger, nemlig n-1.

Der er nok ingen vej uden om: vi må finde pk,t, og det er det svære i denne opgave, mens resten er mere eller mindre rutinearbejde for en konkret mate­ma­tiker. Lad os derfor gå i krig med pk,t.

Da der er n!  lige sandsynlige talmængder som inddata til algoritmen, må pk,t være lig med det samlede antal muligheder for ombytningsmønstre, divi­deret med n!. Størrelserne k og t skal opfattes som kendte og faste størrelser, og vi skal finde det samlede antal mulige ombyt­nin­ger for netop disse værdier af k og t.

Jeg minder om sandsynlighedsmodellen, som arbejder med den forudsæt­ning at det er en per­mutation af tallene fra 1 til n der skal sorteres. Hvis der op­deles sådan at position k bliver delepositionen, så betyder det altså at tallet i position k netop er lig med k, og tallene til venstre er en permutation af tallene fra 1 til k-1, mens tallene til højre er en permutation af tallene fra k+1 til n. Før opdelingen var der præcis t af tallene i den venstre gruppe, der skulle ha­ve stået i den højre - og omvendt. Disse overvejelser giver nøglen til bereg­ning af pk,t.

De to delmængder kan permuteres på hhv. (k-1)! og (n-k)! måder, så det samlede antal per­mutationer for de to dele taget som et hele er (k-1)!(n-k)!. For hver af disse permutationer skal der udtages t ombytningspositioner i hver delmængde. Det kan gøres på  måder. Man har derfor at

 

Så kom vi så langt, men nu venter der os sandelig nogle barske binomial­sum­mationer, når vi indsætter i rekursionen. Lad os først dele højresiden af rekursionen i to summer:

 

Summerne er i virkeligheden dobbeltsummer fordi der skal summeres over både k og t. Sum­men over k går fra 1 til n og summen over t går fra 0 til k-1. Det er derfor det fornuftigste at summere over t "inden i" k. Lad os tage hver sum for sig og begynde med den sidste sum; den ser lettest ud fordi der ikke er noget t uden for binomialkoefficienterne. Vi finder:

 

Summen over t kan nu beregnes ved at benytte sumformlen (8) hvori man substituerer h = k-1, s = n-k, k = t og m = n = 0. Dette giver:

 

Hele den imposante sum blev altså reduceret til, hvilket man måske kunne have gættet ud fra erfaringerne med rekursionen for antal sammenlig­ninger. Tilbage er den anden sum i ombyt­ningsrekursionen:

Her benytter jeg binomialregel (2) på og får:

Faktoren (k-1) afhænger ikke af t og kan sættes udenfor t-summen, og så kan man benytte sum­formel (8) igen, men nu med substitutionerne h = k-2, s = n-k, m = -1 og n = 0. Dette fører til:

 

Hele rekursionen kan herefter skrives:

Denne rekursion kan løses på præcis samme måde som rekursionen for antal sammenlig­ninger; løsningen fandt jeg til:

Sammenligner vi nu dette resultat med resultatet for antal sammenligninger ser vi, at der ud­føres ca. 6 gange så mange sammenligninger som ombytnin­ger i quicksort. Det lave antal om­byt­ninger er også en af grundene til at quicksort lever op til sit navn. Da en sammenligning kræ­ver 2 lagertilgange, og en ombytning på en sekventiel maskine kræver 6 lagertilgange (se proce­dure quicksort) kan vi slutte at der i gennemsnit bruges halvt så megen tid på ombytninger som på sammenligninger i quicksort.

 

Opsummering

I dette essay førte jeg læseren hastigt igennem de mest fundamentale sider af den konkrete matematik. I sin fulde almindelighed er den konkrete matema­tik mere omfattende end det frem­går af dette essay, der er tale om matematik  og ikke blot dens anvendelse på datalogiske proble­mer. De vigtigste emner jeg har udeladt er hele talteorien, samt spørgsmålet om hvordan man ud­vikler og regner med asymptotiske udtryk (O-notation). Det er ikke fordi disse emner er uden interesse for datalogen, men fordi dette blot er et essay, dvs. en kort afhandling af populært til­snit.

Jeg begyndte med at omtale rekursionsligninger og deres løsning, og dette emne blev grad­vist uddybet hen igennem essayet. Der er meget mere at sige om løsning af rekursioner, og jeg kan henvise den interesserede læser til Lueker(1980). Opstilling af en rekursion er ofte det før­ste skridt i arbejdet med at beregne kompleksiteten af en given algoritme, og jeg håber at have gi­vet læseren en fornemmelse af at hvis det først er lykkedes at opstille en re­kur­sion, så er re­sten blot mere eller mindre rutine. Desværre kender jeg ingen opskrift på denne første fase, at opstille selve rekursionen, det er her den kre­ative indsats ligger.

Løsning af rekursioner involverer næsten altid en eller anden form for sum­mation, og derfor handlede næste afsnit om summation. Jeg nævnte de fundamentale regler for summation og vi­ste nogle eksempler på sumdannelse ved anvendelse af disse regler. Også dette emne blev ud­dybet hen igennem essayet.

Den første uddybning handlede om analogien mellem differentiation og differensdannelse, altså en analogi imellem den kontinuerte og den diskrete ma­tematik. Her viste jeg hvordan man kan indføre et sumbegreb der svarer til den kontinuerte matematiks integrationsbegreb. Jeg ind­skrænkede mig til at se på potenser fordi det har størst anvendelse indenfor algoritmeanalyse, men differensregning har, som matematisk disciplin betragtet, langt større omfang; fx er diffe­rensregning en vigtig del af hele den numeriske matematik, regning med datalogiens repræsen­tationer af reelle tal.

Så fulgte et afsnit om binomialkoefficienter, måske det matematiske objekt der er mest frem­medartet for den der er opdraget i den kontinuerte matematiks verden. Jeg viste et eksempel på en rigtig "ond" summation med binomial­ko­ef­ficienter, og håber at have givet læseren en for­nem­melse af at vi her står overfor et område der minder lidt om trigonometri i den forstand at det hand­ler om at kende sit formelapparat når man skal manipulere med binomial­koef­ficienter.

Tanken om at konstruere frembringerfunktioner bør ikke være fremmed for en datalog. En frembringerfunktion er en repræsentation af en talfølge, der gør det muligt at behandle hele tal­følger, endelige og uendelig, under ét i en afsluttet form. Jeg viste ved et eksempel hvordan man kan benytte dette vig­tige redskab til løsning af rekursioner.

Efter en kort omtale af det diskrete sandsynlighedsbegreb introducerede jeg frembringer­funk­tioner for sandsynligheder, og viste hvordan man kan bru­ge sådanne til analyse af algorit­mer. Eksemplerne var dels en datastruktur til administration af prioritetskøer, og dels en sorte­ringsmetode der under vis­se omstændigheder er i stand til at sprænge "sorteringsmuren" nlog(n).

Mit essay sluttede med en ret indgående analyse af en af de mest omtalte algoritmer i moder­ne datalogi, nemlig quicksort, hvor jeg både behandlede an­tallet af sammenligninger og antallet af ombytninger. Her fik jeg brug for det meste af det stof der var blevet omtalt tidlige i essayet.

 

 

Referencer

 

[Graham (1989)]

Ronald L. Graham, Donald E. Knuth & Oren Patashnik: Concrete Mathematics - A Foundation for Computer Science. Addison-Wesley 1989.

 

[Hoare (1962)]

C. A. R. Hoare: Quicksort. Computer Journal 5(1962),10-15.

 

[Isaac (1956)]

E. J. Isaac & R. C. Singleton: Sorting by Address Calculation. Journal of the ACM 3(1956),169-174.

 

[Jonassen (1975)]

Arne Jonassen & Ole-Johan Dahl: Analysis of an Algorithm for Priority Queue Administration. BIT 15(1975),409-422.

 

[Knuth (1968)]

Donald E. Knuth: The Art of Computer Programming, vol I, Fundamental Algorithms  Addison-Wesley 1973.

 

[Lueker (1980)]

George S. Lueker: Some Techniques for Solving Recurrences. Computing Surveys 12(1980), 419-436.

 

[Sedgewick (1978)]

Robert Sedgewick: Quicksort. Garland 1978.

 

[Sloane (1973)]

N. J. A. Sloane: A Handbook of Integer Sequences. Academic Press 1973.