Symbolske data SICP 2.3 Vi har så langt alt vesentlig sett på tall enkeltstående tall, talluttrykk, og lister og trær med tall. Et av målene for John McCarthy (opphavsmannen til Lisp) var å lage et språk for manipulering av symboler slik man f.eks. gjør i matematikken (algebra) ved behandling av ligninger med én eller flere ukjente. Ett program kan være data til et annet (f.eks. er kildekoden data til kompilatoren). Med symboler kan vi anskueliggjøre dette innenfor ett og samme program. Men har vi et språk for symbolmanipulering, trenger vi selvsagt ikke begrense oss til matematikken. Vi kan også regne på begrepsmessige relasjoner mellom mer eller mindre kompliserte objekter personer, naturlige språk, etc.. 257
For å kunne operere på symboler, trenger vi en symbolsyntaks. En ting er de navngitte objekter vi har i og med programmets prosedyrer og variabler, men for skikkelig symbolbehandling ønsker vi også variabel-variabler, dvs. Scheme-variabler med variabelsymboler som verdier Så å si alle høynivåspråk, herunder Scheme, har typer for enkelttegn og tegnstrenger, men disse er ikke særlig godt egnet for å etablere det metabegrepet vi ønsker oss. Scheme har derfor, i tillegg til talltypene, typene tegn og boolean og de sammensatte typene par, liste, vektor og streng, en egen type symbol. Selv om Scheme er løst (dynamisk) typet er typekontrollen strikt der det er relevant. F.eks. kan sammenligningsprosedyrene <, <=, =, >=, og > bare brukes på tall. Men, som vi skal se, finnes det ekvivalensprosedyrer som tillater argumenter av ulike typer, selv om en sammenligning på tvers av typer alltid vil gi resultatet false. 258
(For leselighetens skyld skriver jeg true og false i stedet for #t og #f.) (symbol? 'foo) => true (symbol? (car '(a b))) => true (symbol? "bar") => false (symbol? #t) => false (symbol? '()) => false (define nil '()) (symbol? nil) => false (symbol? 'nil) => true (define nil 'nil) (symbol? nil) => true ; her har vi en tegnstreng ; her har vi en boolesk verdi ikke et symbol ; den tomme listen er ikke et symbol ; nil er en Scheme-variabel, bundet til den tomme listen. ; men dette er symbolet ikke den tomme listen ; nil er nå en Scheme-variabel bundet til symbolet nil. Symboler kan opptre helt og holdent på linje med tall, strenger og objekter av andre typer, men de kan også opptre som eller representere variabler. Dvs. på det syntaktiske nivået er de stadig objekter, men på det symbolbehandlende metanivået er de variabler. 259
Symboler er kjennetegnet ved at to symboler er identiske dersom de staves likt. Et symbol opptrer i sitert quote'et form. quote er en spesialform som bl.a. brukes for å innføre symboler. 'sym, og dermed (quote sym), evaluerer til sym. 'sym er syntaktisk sukker for (quote sym) REPL skriver ut den forenklede varianten, slik at f.eks.(quote (quote sym)) skrives 'sym. Bruken av én enkelt prefisket apostrof er entydig, fordi etterfølgende whitespace eller sluttparentes skiller det aktuelle symbolet fra etterfølgende språklige elementer. quote kan også brukes for lister. '(1 2 3)) (list 1 2 3)) '(a b c)) (list 'a 'b 'c)) Alle slags Scheme-uttrykk prosedyrekall, bruk av spesialformer, definisjoner, etc. kan quotes, og alle quotede uttrykk kan evalueres vha. prosedyren eval, slik at for eksempel (eval '(+ 2 3)) og ((eval '+) 2 3)begge evaluerer til 5, men hvis resultatet av eval ikke gir mening, går det galt, f.eks. slikt at (eval 'a) ville gi kjøreavbrudd, dersom ikke a alt var definert. Noen få tegn kan ikke quotes direkte: #., ' " ` \ ( ) [ ] { } hash, punktum, komma, enkel og dobbel apostrof, backquote, backslash, pipe og parentesene (og muligens noen til), men de kan quotes sammen med escapetegnet \. I Racket skrives slike symboler ut omgitt av pipes. Eks '\# skrives #. 260
Sammenligningsprosedyrer for ulike typer objekter tall - tall, strenger - strenger, symboler - symboler, etc. I forbindelsen med tallbehandling har vi brukt sammenligningsprosedyrene for likhet og størrelsesrelasjoner: =, <, >, <= og >=. I Scheme virker disse, som nevnt, bare for tall, og vi har andre sammenligningsoperatorer for objekter av andre typer; og vi kan også innlemme egne sammenligningsoperator i abstraksjonsabrrieren for en egendefinert type. I mange språk er disse overlesset slik at de også virker for strenger og C++ tillater ytterligere skreddersydd (custom) overlessing for egendefinerte typer. Det finnes imidlertid også generelle ekvivalenspredikater, bl.a eq? gjelder objekters identitet (referanse / adresse / lokasjon (plass i memory)) equal? gjelder numerisk, boolesk, tegnmessige eller symbolmessige likhet, eller sammensatte objekter, mht. deres innhold, element for element. Det finnes også et ekvivalenspredikatet eqv? som gjelder gjelder objekters identitet og eventuell numeriske, booleske, tegnmessige eller symbolmessige likhet. Vi klarer oss imidlertid lenge med eq? og equal? 261
To distinkte objekter kan ha samme innhold. (define symbolpar-1 '(sym bol)) (define symbolpar-2 '(sym bol)) (eq? symbolpar-1 symbolpar-2) false (samme innhold ulike objekter) (equal? symbolpar-1 symbolpar-2) true (samme innhold) (define symbol-1 'sym) (define symbol-2 'sym) (eq? symbol-1 symbol-2) true (symboler som staves likt, er identiske) (equal? symbol-1 symbol-2) true " (eq? symbol-1 (car symbolpar-2)) true " (= symbol-1 symbol-2) feilmelding: = expects type <number>... (eq? 2 2) true (fra Racket ellers, i hht R 5 RS, uspesifisert) (equal? 2 2) true Idéelt skal det, for av såvel tall som symboler, være slik at hvis ulike forekomster har samme verdi så er de også samme objekt(cfr. Platon), og slik er det også i Racket. 262
Symbolsk differensiering (derivasjon) SICP 2.3. Differensiering i matematikken går ut på å finne funksjoners endringsrater eller deriverte (avledede). F.eks. har funksjonen f(x) = 3x endringsraten 3, dvs. for hver endring av x endres funskjonsverdien med 3. For f(x) = 3x er dette en grei beskrivelse ettersom f er linær. For ikke-lineære funksjoner som f.eks. x 2, trenger vi en mer raffinert beskrivelse. Vi sier at den deriverte av en funksjon f(x) er grenseverdien for uttrykket (i) f(x + h) f(x) h når h (på en eller annen måte) går mot 0. Eller sagt på en annen måte Merk at f(x + 0) f(x), altså 0, ikke gir 0 0 mening (ii) f ' (x) = lim f(x + h) f(x) h 0 h Eller på nok en måte, når vi sier at det til en endring x av x svarer en endring y av y. dy y (iii) f ' (x) = = lim dx x 0 x NB! Uttrykket dx/dy angir ikke en brøk, men kun et symbol for den deriverte som altså er en grenseverdi. Her er det viktig å holde ting fra hverandre. 263
Bl.a. følgende regler gjelder for derivasjon: (1) c' = 0 den deriverte av en konstant = 0 (2) x' = 1 den deriverte av identitetsfunksjonen = 1 (3) (f(x) + g(x))' = f '(x) + g'(x) den deriverte av summen av to funksjoner = summen av de deriverte av de to funksjonene (4) (f(x) g(x))' = f '(x) g(x) + g'(x) f(x) den deriverte av produktet av to funksjoner = summen av den deriverte av den første ganger den andre og den deriverte av den andre ganger den første f(x) (f '(x) g(x) g'(x) f(x)) den deriverte av brøken av to funksjoner = differansen mellom (5) ( )' = den deriverte av telleren ganger nevneren og g(x) (g(x)) 2 den deriverte av nevneren ganger telleren, delt på kvadratet av nevneren (6) (f(g(x)))' = f '(g(x)) g'(x) den deriverte av en sammensatt funksjon = den deriverte av den ytterte mht. den innerste ganger den deriverte av den innerste Ved gjentatt anvendelse av (4) kan vi avlede (7) (x n )' = n x n-1 f.eks. den deriverte av x 3 = 3x 2 fordi (x x x)' = ((x x) x)' = (x x)' x + (x x) x' = (x' x + x x')x + x x 1 = (1 x + x 1) x + x x 1 = x x + x x + x x = 3 x x. Her har jeg brukt f ' for den dervierte av f. For å unngå forvekslingen med formen quote, bruker jeg heretter formen dy/dx den deriverte av y mht. x. 264
Differensiering i Scheme Vi tar for oss et utvalg av disse reglene, når vi nå går løs på symbolsk differensiering i Scheme, i første omgang (1) (4) som i SICP er notert slik * : dc (1) = 0 dx dx (2) = 1 dx d(u + v) du dv (3) = + dx dx dx (4) d(u v) dv du = u + v dx dx dx NB! Selv om dette ser ut som (og forsåvidt er) matematikk, er det for oss primært symbolmanipulering etter bestemt regler. Finner vi et uttrykk med formen til venstresiden i for eksempel (3), skal vi lage et uttrykk som det som står på høyresiden i (3). * Det finnes flere notasjoner for den deriverte med ulike opphavsmenn (ingen kvinner) først og fremst Euler, Newton, Leibniz og Lagrange. 265
Her er noen eksempler på det vi ønsker å få til, når vi deriverer med hensyn på x: d(x + 3) d(3x) d(x y) d((xy)(3x)) 1, 3, y, 6xy dx dx dx dx Vi velger imidlertid i første omgang prefiks- fremfor infiks-notasjon, siden prefiks er lettere å arbeide med i Scheme. Og i tillegg til det uttrykket som skal deriveres, lar vi symbolet for den variabelen det skal deriveres med hensyn på, være argument til deriveringsprosedyren Vi vil altså ha en funksjon som virker slik: (deriv '(+ x 3) 'x) 1 ; den deriverte av (+ x 3) mht. x = 1 (deriv '(* x 3) 'x) 3 (deriv '(* x y) 'x) y 3x xy (deriv '(* (* x y) (* x 3)) 'x) xy + 3x = 6xy dx dx Det skal imidlertid vise seg at vi må arbeide en del, før vi kan få til resultater som disse. 266
Første utgaven av deriveringsprosedyren ser slik ut. (define (deriv exp var) ; derivér uttrykket exp med hensyn til variabelen var. (cond ((number? exp) 0) ; regel (1): konstant ((variable? exp) ; regel (1) el. (2): konstant eller hensynsvariabel (if (same-variable? exp var) 1 ; regel (2): hensynsvariabel 0)) ; regel (1): konstant ((sum? exp) ; regel (3): sum (make-sum (deriv (addend exp) var) ; rekursér for å derivere addenden (deriv (augend exp) var))) ; rekursér for å derivere augenden ((product? exp) ; regel (4): produkt (make-sum (make-product (multiplier exp) (deriv (multiplicand exp) var)); rekursér for å deriv. multiplikand (make-product (deriv (multiplier exp) var) ; rekursér for å deriv. multiplikator (multiplicand exp)))) (else (error "unknown expression type DERIV" exp)))) 267
Over har vi brukt predikater og selektorer vi ennå ikke har definert. Hvordan disse skal defineres, beror på hvordan vi velger å representere de (algebraiske) uttrykkene som skal deriveres. Ved hjelp av lister, og med prefiks-notasjon, får vi følgende implementasjon: Tall er tall (define (number? x) (number? x)) ; number? er en Scheme-primitive Variabler er symboler (define (variable? x) (symbol? x)) ; symbol? er en Scheme-primitive Vi trenger en sammenligningsprosedyre for å kunne kjenne igjen den variabelen vi deriverer med hensyn på, (define (same-variable? v1 v2) (and (variable? v1) (variable? v2) (eq? v1 v2))) ; Her hadde det holdt med ett kall på variable? fordi ; hvis den ene av v1 og v2 er en variabel ; vil ikke eq? returnere true med mindre også den andre er en variabel. 268
Summer og produkter er lister med Scheme's prefixform der første element er en operand-tag (define (make-sum a1 a2) (list '+ a1 a2)) (define (sum? exp) ; Et uttrykk er en sum (and (pair? exp) ; hvis det er et par, og (eq? (car exp) '+))) ; dets første element er symbolet +, (define (addend sum) (cadr sum)) ; Andre element er det som får noe lagt til seg. (define (augend sum) (caddr sum)) ; Tredje element er det som legges til. (define (make-product a1 a2) (list '* a1 a2)) (define (product? exp) ; Et uttrykk er et produkt (and (pair? exp) ; hvis det er et par, og (eq? (car exp) '*))) ; dets første element er symbolet *. (define (multiplier sum) (cadr sum)) ; Andre element er det antall ganger noe skal ganges. (define (multiplicand sum) (caddr sum)) ; Tredje element er det som skal ganges. 269
Med denne implementasjonen får vi følgende output med de inndata som er vist over. (deriv '(+ x 3) 'x) (+ 1 0) ; regel (3, 2, 1) (deriv '(* x 3) 'x) (+ (* x 0) (* 1 3)) ; regel (4, 2, 1) (deriv '(* x y) 'x) (+ (* x 0) (* 1 y)) ; regel (4, 2, 1) (deriv '(* (* x y) (* x 3)) 'x) (+ (* (* x y) ; regel (4) (+ (* x 0) (* 1 3))) ; regel (3, 2, 1) (* (+ (* x 0) (* 1 y)) ; regel (3, 2, 1) (* x 3))) Til sammenligning viser vi om igjen det output vi kunne ønske oss. (deriv '(+ x 3) 'x) 1 (deriv '(* x 3) 'x) 3 (deriv '(* x y) 'x) y (deriv '(* (* x y) (* x 3)) 'x) 6xy 270
Vi kan komme et stykke på vei ved å modifisere konstruktorene, og dette kan vi gjøre uten å endre derivasjonsprosedyren. (define (make-sum a1 a2) (cond ((=number? a1 0) a2) ; addend = 0, så summen = augend ((=number? a2 0) a1) ; augend = 0, så summen = addend ((and (number? a1) (number? a2)) ; begge er tall, så vi returnmerer (+ a1 a2)) ; den numeriske summen (else (list '+ a1 a2)))) (define (make-product m1 m2) (cond ((or (=number? m1 0) (=number? m2 0) 0)) ; minst én faktor = 0, så produktet blir også 0 ((=number? m1 1) m2) ; multiplikand = 1, så produktet = multiplikator ((=number? m2 1) m1) ; multiplikator = 1, så produktet = multiplikand ((and (number? m1) (number? m2)) ; begge er tall, så vi returnmerer (* m1 m2)) ; det numeriske produktet (else (list '* m1 m2)))) 271
Sammenligningsfunksjonen =number? x n tar en variabel eller et tall som første argument og et tall (forutsetningsvis) som andre argument, og returnerer true hvis første argument er samme tall som andre. (define (=number? x n) (and (number? x) (= x n))) Dette gir følgende forbedrede output: (deriv '(+ x 3) 'x) 1 (deriv '(* x 3) 'x) 3 (deriv '(* x y) 'x) y (deriv '(* (* x y) (* x 3)) 'x) (+ (* (* x y) 3) (* y (* x 3))) klart bedre enn resultatet av den opprinnelige implementasjonen, (deriv '(* (* x y) (* x 3)) 'x) (+ (* (* x y) (+ (* x 0) (* 1 3))) (* (+ (* x 0) (* 1 y)) (* x 3))) men vi har fremdeles har et stykke igjen. Dette er et tema for ukeoppgavene. 272
Representasjon av mengder SICP 2.3.3 Schemes liste-begrep gir en mulig representasjon av mengder, forutsatt visse modifikasjoner og presiseringner. Bl.a. må vi ta vare på at - kardinaliteten til en mengde er gitt ved antall distinkte elementer i mengden, mens - lengden til en liste er gitt ved antall elementer overhodet. En bag eller et multiset er en samling av elementer med multiplisitet flere forekomster av samme element / verdi. Mengde (1, 2, 3) = (2, 1, 3) = (1, 1, 2, 3) Multiset (1, 2, 3) = (2, 1, 3) (1, 1, 2, 3) Liste (1, 2, 3) (2, 1, 3) (1, 1, 2, 3) (se under om ordnede mengder) Dette gjør vi (som for rasjonelle tall og algebraiske uttrykk) ved å definere datatypen mengde (set) ved noen grunnoperasjoner i første omgang snitt og union (det siste som øvelse) samt en konstruktor for å legge et element til en mengde, og et predikat for å avgjøre om noe er et medlem i en mengde. 273
For å ta det siste først: Scheme har bl.a. semipredikatene memq og member for å sjekke om noe er et element i en liste. I implementasjonen av disse brukes henholdsvis eq? og equal?. SICP bruker fortrinnsvis og vi bruker utelukkende eq? og equal?. Som nevnt over er forskjellen mellom disse generelt den at (eq? x y) returnerer true hvis x og y er identiske altså samme objekt (lokasjon) mens (equal? x y) returnerer true hvis x og y er like, element for element. (define x '(a b c)) (define y (map (lambda (e) e) x)) ; y er nå en kopi av, men ikke identisk med, x. (eq? x y) #f (equal? x y) #t Hva symboler angår, er hele poenget at (eq? sym1 sym2) (equal? sym1 sym2). Som sagt: to symboler som staves likt er identiske (også rent fysisk i maskinen). Gitt to symboler a og b, så gjelder at: (string=? (symbol string a) (symbol string b)) (eq? a b). Se R 5 RS 6.3.3 og 6.5.5. 274
Primitene member tar et objekt og en liste som argumenter, søker rekursivt gjennom den gitte listen etter det gitte objektet vha. equal og returnerer ved eventuelt funn den delen av listen som begynner med det funne objektet, eller false, hvis objektet ikke ble funnet. Her er vi imidlertid interessert i ekvivalenspredikater snarere enn semipredikater, og definerer ett mht. identitet og ett mht. likhet: (define (memq? elm set) (cond ((null? set) #f) ((eq? elm (car set)) #t) (else (memq? elm (cdr set))))) (define (member? elm set) (cond ((null? set) #f) ((equal? elm (car set)) #t) (else (member? elm (cdr set))))) (memq? 'b '(a b c)) #t (memq? 'd '(a b c)) #f (memq? '(b c) '(a (b c) d)) #f (member? 'b '(a b c)) #t (member? '(b c) '(a (b c) d)) #t Merk at den eksplisitte returverdien #t i andre cond-clausee her ikke er nødvendig ettersom eq? og equal? er ekte predikater og returnerer enten #t eller #f. 275
Vi vil ha med predikatet element-of-set? i abstraksjonsbarrieren for mengder. Skulle vi ha ønsket at mengder skulle kunne inneholde mengder, måtte vi ha definert element-of-set? vha. member?. Men om vi ønsker ordnede mengder, kan vi ikke tillate mengder i mengder For å kunne legge et element inn i en mengde definerer vi: (define (adjoin-set x set) (if (element-of-set? x set) set (cons x set))) Prosedyren returnerer den aktuelle mengden, etter at det gitte elementet evt. er lagt inn. Når vil velger å unngå duplisering av elementer er det av pragmatiske grunner, for å forenkle mengdesoprasjoner som snitt og union. 276
Snittoperasjonen kan vi definere slik: (define (intersection-set set1 set2) ;S (cond ((or (null? set1) (null? set2)) '()) ;én eller begge lister er tomme, så ingen flere felles elementer ((element-of-set? (car set1) set2) ;set1-elementet finnes i begge mengdene (cons (car set1) (intersection-set (cdr set1) set2))) (else (intersection-set (cdr set1) set2)))) ;set1-elementet finnes ikkje i set2. (intersection-set '(a b d g h i) '(b c d e h k)) (b d h) Ser vi på arbeidsmengden her, finner vi at element-of-set? og adjoin-set, som kaller element-of-set? én gang, har linær arbeidsmengde, mens intersection-set som kaller element-of-set? for hvert element i den ene argumentmengden, har kvadratisk arbeidmsengde. Det samme vil union-operasjonen få. 277
Merk at set2 forblir uendret gjennom alle rekursive kall, og testen (null? set2) er dermed bare relevant ved første kall på intersection. For å tydeliggjøre dette, kunne vi ha skrevet prosedyren slik: (define (intersection-set set1 set2) (define (iter set1) (cond ((null? set1) '()) ((member (car set1) set2) (cons (car set1) (iter (cdr set1)))) (else (iter (cdr set1))))) (if (null? set2) '() (iter set1))) Og dette gir dessuten, som man ser, en iterativ prosess. 278
Mengder som ordnede lister En ordnet mengde er et par (S, R) der - S er en mengde og - R er en binær ordningsrelasjon på mengdens elementer, typisk mengden av tallene og relasjonen, (Z, ). Par-relasjonen gjør lister god egnet for representasjon av ordnede mengder, der car og cdr alltid angir det første og det etterfølgende element. Vårt anliggende her er imidlertid ikke ordnede mengder, men å effektivisere mengdesoprasjonene ved hjelp av ordnede lister Ved operasjoner på ordnede lister kan vi redusere den linære søkelengden fra n til n/2 for elementer som ikke blir funnet. Men fremfor alt kan vi redusere arbeidsmengden for snitt og union fra kvadratisk til lineær. I Scheme er også parene (character, char<=?) og (string, string<=?) ordnede mengder, men her begrenser vi oss til å se på mengder av tall. Vi søker i en uordnet liste L. - Hvis det for hvert søk x er svært sannsynlig at x er i L, får vi en gjennomsnittlig søkelengde = L /2, og - hvis det for hvert søk x er svært lite sannsynlig at x er i L, får vi en gjennomsnittlig søkelengde = L, men, som det fremgår av neste side, - hvis L var ordnet, ville vi i begge tilfeller få en gjennomsnittlig søkelengde = L /2. ( L = lengden til L.) 279
Halvering av linær søkelengde i en ordnet mengde: (define (element-of-set? elem set) (cond ((null? set) #f) ((= elem (car set)) #t) ((< elem (car set)) #f) (else (element-of-set? elem (cdr set))))) Effektiviseringen oppnås ved at vi stopper når det evt. ikke er noe vits i å lete lenger (element-of-set? 3 '(1 2 4 5)) #f Vi stopper her når vi kommet halveis, og ser at det søkte elementet ikke kan ligge lenger ut. 280
Linearisering av snittoperasjon vha. parallell gjennomløping av de ordnede argumentmengdene (define (intersection-set set1 set2) (if (or (null? set1) (null? set2)) '() (let ((x1 (car set1)) (x2 (car set2))) (cond ((< x1 x2) (intersection-set (cdr set1) set2)) ((< x2 x1) (intersection-set set1 (cdr set2))) (else ; x1 = x2 (cons x1 (intersection-set (cdr set1) (cdr set2)))))))) (intersection-set '(1 2 4 7 8 9 11 14 15) (2 4 8 11 14) '(2 3 4 5 8 10 11 12 14 17)) 281
Ovenstående utforming av algoritmen avviker et ørlite grann fra lærebokens, idet vi nøyer oss med å teste for to av de tre mulige størrelsesrelasjonene mellom x1 og x2 og lar den tredje være implisert i else-grenen. La oss følge utførelsen av ovenstående kall, idet (vi later som om ) snittet fylles opp underveis. runde set1 x1 set2 x2 snitt 1 (1 2 4 7 8 9) 1 (2 3 4 5 8 10) 2 () 2 (2 4 7 8 9) 2 (2 3 4 5 8 10) 2 () 3 (4 7 8 9) 4 (3 4 5 8 10) 3 (2) 4 (4 7 8 9) 4 (4 5 8 10) 4 (2) 5 (7 8 9) 7 (5 8 10) 5 (2 4) 6 (7 8 9) 7 (8 10) 8 (2 4) 7 (8 9) 8 (8 10) 8 (2 4) 9 (9) 9 (10) 10 (2 4 8) 10 () - - - (2 4 8) Vi ser at får en arbeidsmengde n + m. Siden prosedyren gir en rekursiv prosess, vil i realiteten snittet ikke fylles opp før rekursjonen avvikles kall for utenforliggende kall. 282
Fletting Ovenstående kan sees som en variant av en mer generell flette(merge)-algoritme. Ordinær fletting gir unionen av to mengder med eller uten duplisering av elementer, avhengig av hva som skjer ved likhet (i else-grenen i algoritmen over). Ellers kan vi variere algoritmen mht. når vi cons-er inn nye elementer i resultatmengden. Variasjonene omfatter: - full fletting med multiplisitet (mulige multiple forekomster av elementer) (typisk ved Merge Sort) - union, dvs. fletting med bare unike elementer i resultatmengden - snitt - differansen : set1 set2 eller set2 set1. 283
(define (combine-sets set-1 set-2) (cond (<set-1 er gjennomløpt> ; basistilfelle-1 <basisverdi-1>) (<set-2 er gjennomløpt> ; basistilfelle-2 <basisverdi-2>) (else ; almenntilfellet (<identifiser første element fra hver mengde> (cond (<elementet fra set1 er minst> ; almenntilfelle-1 (combine-sets...)) (<elementet fra set2 er minst> ; almenntilfelle-2 (combine-sets...)) (else (combine-sets...)) ; elementene er like Oppgave: Erstatt pseudokoden for å lage hhv. snitt, union og differanse. NB! Ved ren sammenfletting (merge) kan mønsteret forenkles. Oppgave: Implementer fletting på enklest mulig måte. Oppgave: Kan differanse-operasjonen også forenkles, og i så fall hvordan? 284
Oppsumering av arbeidsmengden ved operasjoner på ordnede lister La M og N være to mengder. I utgangspunktet vil kostnadene ved en mengdesoperasjon være "kvadratisk", dvs mellom M N /2 og M N, X = størrelsen til X. idet vi sammenligner hvert element i M med hvert element i N. Ved å implementere mengdestypen slik at mengdeselementen ligger i en ordnet liste f.eks. som tall, i stigende orden, kan vi redusere kostnadene ved mengdesoperasjoner fra kvadratisk til lineær, dvs fra M N /2 til M + N, idet vi sammenligner elementene i M med elementene i N, i en parallell gjennomløping av de to mengdene Søking i lister er allerede i utgangspunktet en lineær prosess, så her blir gevinsten ved å ordne mengden i beste fall en halvering av arbeidsmengen. Er sannsynligheten for funn liten får vi en halvering, men er sannynligheten stor, får vi ingen gevinst 285
Mengder representert ved binære trær SICP 2.3.3 Med hensyn til søking kan vi organisere mengder som binære trær. Et tre er bygget opp av noder f.eks. slik at hver node har en verdi og ingen, ett eller flere subtrær. I et binært tre har ingen node mer enn to subtrær, og i noen binære trær som de vi skal bruke har hver node nøyaktig to subtrær, når et tomt tre også regnes som et subtre. For et binært tre med stigende unike verdier, gjelder følgende krav: For hver node x skal alle noder til venstre for x ha lavere og alle nodene til høyre for x ha høyere verdi enn x. 286
Som vi ser av trærne under, som alle representerer mengden {1, 3, 5, 7, 9, 11}, gir dette kravet opphav til flere mulige ulike representasjoner av en og samme mengde, men for at et binært tre skal gi en effektiv organisering mht. søk, må det være balansert (noe vi ikke har tid til å gå inn på her). Antall binære trær med 6 noder = 132 A: '(11 (9 (7 (5 (3 (1 () ()) ()) ()) ()) ()) ()) B: '(7 (3 (1 () ()) (5 () ())) (9 () (11 () ()))) C: '(3 (1 () ()) (7 (5 () ()) (9 () (11 () ())))) D: '(5 (3 (1 () ()) ()) (9 (7 () ()) (11 () ()))) E: '(1 () (3 () (5 () (7 () (9 () (11 () ())))))) 287
Ved binær søking i en ordnet rekke får vi logaritmisk arbeidsmengde, 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 2 4 5 7 8 11 12 14 15 21 22 24 25 31 32 35 36 37 40 42 45 48 24 25 31 32 35 36 37 40 42 45 48 24 25 31 32 35 32 35 som i dette eksemplet der vi finner verdien 35 blant 22 verdier ved 4 ( log 2 22) halveringer. Midtpunktet beregnes til n/2 når x = floor(x), dvs. nærmeste heltall <= x. 35 En slik søkemåte forutsetter en struktur der vi har direkte aksess til de enkelte elementene typisk en vektor. I en liste er vi henvist til å søke sekvensielt, men med en trestruktur oppheves denne begrensningen. Riktignok må vi vandre gjennom en sekvens av noder, men søkeveien går via forgreninger, tilsammen ikke mer enn log 2 n, hvis treet er balansert. La l være logaritmen til et tall n mht. en base b, dvs. l = log b n. Da er l det tallet vi må opphøye b i for å få n, dvs. n = b l. F.eks. for n = 1000 og b = 10, har vi l = log 10 1000 = 3 og 10 3 = 1000. Når vi beregner antall med binære halveringer, lar vi basen være 2. F.eks. for n = 512 og b = 2, har vi l = log 2 512 = 9 og 2 9 = 512. Hvis n ligger mellom to potenser av to, f.eks. n = 350 og dermed ligger mellom 2 8 = 256 og 2 9 = 512, må vi i verste fall foreta 9 halveringer for, om mulig, å finne det vi søker i et balansert. tre. 288
(22 (8 (4 (2 () ()) (5 () (7 () ()))) Vi finner 35 i fire trinn ved å gå (14 (11 () (12 () ())) fra 22 < 35 til venstre (15 () (21 () ())))) fra 36 > 35 til venstre (36 (31 (24 () (25 () ())) fra 31 < 35 til høyre (32 () (35 () ()))) fra 32 < 35 til høyre (42 (37 () (40 () ())) der vi finner 35 (45 () (48 () ()))))) 289
Vi definerer følgende konstruktor og selektorer: (define (make-tree entry left right) (list entry left right)) (define (entry tree) (car tree)) (define (left-branch tree) (cadr tree)) (define (right-branch tree) (caddr tree)) Også ved søking og innsetting, som ved snittoperasjonen over, avviker vi fra læreboken, i det vi lar den tredje av tre mulige tilfeller, nemlig likhet, være implisert i else-grenen. Søkealgoritmen blir da slik: (define (element-of-set? x set) (cond ((null? set) #f) ((< x (entry set)) (element-of-set? x (left-branch set))) ((> x (entry set)) (element-of-set? x (right-branch set))) (else #t))) ; x = (entry set) 290
Og innsettingsalgoritmen blir slik: (define (adjoin-set x set) (cond ((null? set) (make-tree x '() '())) ((< x (entry set)) (make-tree (entry set) ((> x (entry set)) (adjoin-set x (left-branch set)) (right-branch set))) (make-tree (entry set) (left-branch set) (adjoin-set x (right-branch set)))) (else set))) Dette kan gi ubalanse. Hvis elementene kommer i stigende orden, får vi i realiteten en ren liste der hvert nytt element havner i høyregren til nederste element. Ovenstående algoritme gjør at alle nye verdier havner nederst i treet, som blader. For å sikre at treet blir balansert, må vi ha muligheten for å plassere nye verdier høyere opp i treet. Dette krever betydelig mer tenking og betydelig mer kode. 291
La oss følge innsettingen av 9 i treet på side 279: (a) Treet er ikke tomt, og x = 9 < entry = 22. (b) Lag nytt tre med samme entry, med 9 lagt til i venstre gren og med samme høyregren (c) Treet er ikke tomt, og x = 9 > entry = 8. (d) Lag nytt tre med samme entry og samme venstregren med 9 lagt til i høyre gren (e) Treet er ikke tomt, og x = 9 < entry = 14. (f) Lag nytt tre med samme entry, med 9 lagt til i venstre gren og med samme høyregren (g) Treet er ikke tomt, og x (= 9) < (entry = 11). (h) Lag nytt tre med samme entry, med 9 lagt til i venstre gren og med samme høyregren (i) Treet er tomt (j) Lag nytt tre med 9 som entry og tom venstre- og høyregren. 292