4.4 Heap og prioritetskø
Min-heapen som array (indeks fra 0), Insert (sift-up) og RemoveMin (down-heap) med håndkjøring, og de faste heap-faktaene.
6 av de 7 settene i arkivet (86 %), og den testes på to måter:
- Sjanger E — håndkjøring av en datastruktur, altså at du utfører
operasjonen steg for steg på papir og oppgir kun sluttilstanden. Du får et
array og en operasjonsrekke, og skal levere arrayet etterpå.
- Sjanger C — kjøretids- og teori-fakta, altså sant/usant-påstander som
rettes automatisk. Heap-faktaene er blant de mest brukte, og flere av dem er
laget for å fange den som «nesten» husker.
Prioritet: høyeste. Dette kapitlet og drillen i
kap. 4.5 er de to viktigste i Del 4.
To ting må sitte helt presist, og de er begge lette å bomme på:
1. Indeks fra 0. Rota ligger på indeks 0, forelderen til er
, og barna er og . Mange lærebøker og
andre emner indekserer fra 1 og skriver , og
. Det er ikke konvensjonen her. Bruker du 1-formlene på et
0-indeksert array, blir hver eneste indeks feil.
2. BuildHeap er , ikke . Dette er en fast distraktor på
Del 1, og du får se tellingen som beviser det i løkke 4.
Slik er kapitlet lagt opp (55 min):
| Løkke | Innhold | Tid |
|---|---|---|
| 1 | Prioritetskøen, heap-egenskapen og arrayet | ca. 13 min |
| 2 | Insert og sift-up | ca. 13 min |
| 3 | RemoveMin og down-heap | ca. 14 min |
| 4 | BuildHeap — og hvorfor den er | ca. 10 min |
| 5 | De faste heap-faktaene | ca. 5 min |
Forkunnskaper
- kap. 2.2 — heapsort ble nevnt der, med løftet om at selve
strukturen kom i Del 4. Dette er den delen. Du trenger derfra bare at heapsort
er og at det å bygge heapen er .
- kap. 4.1 — BST-egenskapen. Den er heapens motstykke, og
de to forveksles systematisk. Har du BST-egenskapen klart for deg, er
heap-egenskapen lett å holde adskilt.
- kap. 1.1 — hva betyr. Heapens høyde er
logaritmisk, og det er derfor alle operasjonene er det.
- kap. 1.4 — der sto heap-faktaene som løsrevne påstander.
Her får de begrunnelsene sine.
Er array-tenkning ferskt: Lister — indeksering fra 0 er
alt heapens smarteste triks bygger på.
Løkke 1 — alltid-minste-først-køen (ca. 13 min)
Et kommunalt vaktlag tar imot meldinger om hull i veien. Hver melding får et
hastetall: 1 er «gjennomgående hovedvei, må fikses i dag», 9 er «grusvei til en
hytteklynge». Meldingene kommer inn hulter til bulter gjennom dagen, og laget
skal alltid ta den mest hastende først.
En vanlig kø hjelper ikke — den gir deg den eldste meldingen, ikke den mest
hastende. En sortert liste hjelper heller ikke helt: hver nye melding må settes
inn på riktig plass, og det koster når lista er lang.
Det du trenger, er en struktur der du kan gjøre to ting billig: legge inn en
ny melding, og plukke ut den med lavest hastetall. Det er en prioritetskø, og
den vanligste måten å bygge den på er en heap.
Trikset i heapen er at den ikke holder alt sortert. Den holder bare akkurat nok
orden til at det minste elementet alltid ligger øverst — og «akkurat nok» er
billig å vedlikeholde.
En samling der du kan legge inn elementer og alltid hente ut det med høyest
prioritet — i en min-prioritetskø betyr det det minste elementet.
Kontrakten er tre operasjoner: Insert (legg inn), RemoveMin (fjern og
returner det minste) og FindMin (les det minste). Med en min-heap koster de
, og . Prioritetskøen er ikke en datastruktur i seg
selv, men en kontrakt som kan oppfylles på flere måter — se
kap. 1.4 om at et balansert søketre gir samme orden.
I en min-heap er hver forelder mindre enn eller lik begge barna sine.
Ordningen går opp og ned, aldri sideveis: det står ingenting om forholdet
mellom to søsken, og ingenting om forholdet mellom to noder på samme nivå.
Venstre barn kan godt være større enn høyre barn.
Konsekvensen er at det minste elementet garantert ligger på rota — og bare
det. Resten av strukturen er så godt som usortert.
En heap er alltid et komplett binært tre: alle nivåer er helt fylt, bortsett
fra det nederste, som fylles fra venstre.
Dette er ikke pynt. Det er strukturkravet som gjør at treet kan lagres som ett
array uten hull, og som holder høyden på uansett
hvilke verdier som legges inn. En heap kan aldri bli skjev slik et vanlig
søketre kan.
Dette er den vanligste forvekslingen i hele Del 4, og den har eget nummer i bokas
feilregister: felle #9.
| BST (kap. 4.1) | Min-heap | |
|---|---|---|
| Retning på ordningen | venstre–høyre | opp–ned |
| Regelen | alt i venstre subtre er mindre enn noden, alt i høyre er større | forelderen er mindre enn eller lik begge barna |
| Hvor ligger det minste? | lengst til venstre | på rota, indeks 0 |
| Hva gir in-order? | den sorterte rekkefølgen | ingenting nyttig |
| Form | kan bli en skjev kjede | alltid komplett |
Kontrollspørsmålet som skiller dem på to sekunder: sier regelen noe om
forholdet mellom søsken? I et BST gjør den det indirekte — alt til venstre
for en node er mindre enn alt til høyre. I en heap gjør den det ikke. Er du
usikker på om
9 kan stå til venstre for 4 i en min-heap: ja, hvis de har hversin forelder som begge er små nok.
Og den direkte følgen: påstanden «i en min-heap er venstre barn alltid mindre
enn høyre barn» er usann, og den er en fast distraktor på Del 1.
nivåene legges etter hverandre fra venstre mot høyre.
Fordi treet er komplett, er det ingen hull i arrayet, og hver indeks kan regnes
om til forelder og barn med ren aritmetikk. IN2010 indekserer fra 0, og
formlene over er de eneste som gjelder i denne boka. Framstillinger som lar rota
ligge på indeks 1, bruker andre formler, og de gir feil node her.
Elementet på indeks har venstre barn på og høyre barn på — men
bare hvis indeksene finnes.
Med elementer i heapen gjelder:
- : noden er et blad og har ingen barn.
- men : noden har bare venstre barn.
- ellers: noden har begge barna.
Disse tre tilfellene er hele innholdet i felle #3 — down-heap som ikke
sjekker at barnet finnes.
Min-heapen i figuren over er arrayet 2, 5, 8, 10, 25, 20, 30 med indeks fra 0.
Sett opp forelderen og de to barna til hver indeks, og si hvilke noder som er
blader.
Regn mekanisk: forelder er , barna er og ,
og en indeks som er finnes ikke, siden .
| Indeks | H[i] | Forelder | Verdi | Venstre barn | Høyre barn |
|---|---|---|---|---|---|
| 0 | 2 | - | - | 1 (verdi 5) | 2 (verdi 8) |
| 1 | 5 | 0 | 2 | 3 (verdi 10) | 4 (verdi 25) |
| 2 | 8 | 0 | 2 | 5 (verdi 20) | 6 (verdi 30) |
| 3 | 10 | 1 | 5 | 7 finnes ikke | 8 finnes ikke |
| 4 | 25 | 1 | 5 | 9 finnes ikke | 10 finnes ikke |
| 5 | 20 | 2 | 8 | 11 finnes ikke | 12 finnes ikke |
| 6 | 30 | 2 | 8 | 13 finnes ikke | 14 finnes ikke |
Bladene er indeks 3, 4, 5 og 6 — fire av sju noder. De indre nodene er 0, 1
og 2.
Kontroller heap-egenskapen med tabellen: og ;
og ; og . Alle seks
forelder–barn-parene holder, så dette er en gyldig min-heap.
Legg merke til indeks 1 og 2. Verdien 5 står til venstre for 8, men det er
ren tilfeldighet — heap-egenskapen sier ingenting om søsken. Hadde arrayet vært
2, 8, 5, 10, 25, 20, 30, ville det fortsatt vært en gyldig min-heap, selv omdet da hadde vært et brudd på BST-egenskapen (felle #9).
Regn med 0-formlene, ikke 1-formlene. Med ville
forelderen til indeks 6 blitt 3 i stedet for 2 — altså feil node, feil verdi og
feil svar på resten av oppgaven.
(Innstegsoppgave.) En min-heap ligger som array med indeks fra 0 og har
elementer.
a) Hvilken indeks har forelderen til indeks 6?
b) Hvilke indekser har barna til indeks 4?
c) Har indeks 5 to barn, ett barn eller ingen barn?
Løkke 2 — Insert og sift-up (ca. 13 min)
Et nytt element skal inn. Hvor?
Strukturkravet svarer med én gang: heapen er et komplett tre, så det er nøyaktig
én plass det nye elementet kan ligge — den første ledige, altså indeks
bakerst i arrayet. Der legger vi det, og så er heap-egenskapen antakelig brutt,
fordi elementet kan være mindre enn forelderen sin.
Reparasjonen heter sift-up: sammenlign med forelderen, bytt hvis elementet er
mindre, og gjenta oppover. Du stopper når forelderen er mindre enn eller lik
elementet, eller når du har nådd rota.
Legg merke til hvor lite som skjer. Du berører én sti fra bunnen opp til
rota, og den er ledd lang. Resten av heapen røres
ikke.
H er et array med indeks fra 0 som alleredeoppfyller heap-egenskapen.
|H| er antall elementer. Divisjonen (i-1)/2 erheltallsdivisjon, altså med nedrunding.
Prebetingelse: H er en gyldig min-heap. Postbetingelse: H inneholderx i tillegg til alt den hadde før, og er fortsatt en gyldig min-heap.
Procedure Insert(H, x)
Input: min-heap H som array (indeks fra 0), element x
Output: H med x satt inn, heap-egenskapen gjenopprettet
i = |H|
H[i] = x
while i > 0 and H[(i-1)/2] > H[i]:
bytt H[i] og H[(i-1)/2]
i = (i-1)/2
Kjoeretid: O(log n)Invarianten i én setning: etter hvert bytte er alt under i en gyldig
min-heap, og det eneste mulige bruddet ligger mellom i og forelderen — så når
løkka stopper, finnes det ingen brudd.
Merk i > 0 som første ledd i betingelsen. Uten den ville løkka regnet ut
forelderen til rota, og indeks 0 har ingen forelder. Rekkefølgen på de to leddene
er ikke likegyldig: i > 0 må stå først, slik at det andre leddet aldri
evalueres når i er 0.
Kjøretid: , der er antall elementer i heapen. Elementet
flyttes høyst ett nivå per bytte, og treet har
nivåer over bunnen — én sti, ingen forgrening.
Sett inn 7, 3, 9, 1 og 4 i denne rekkefølgen i en tom min-heap (array,
indeks fra 0). Oppgi arrayet til slutt.
Innsetting i TOM min-heap, indeks fra 0: 7, 3, 9, 1, 4
| Steg | Sett inn | Array før sift-up | Bytter (indekser) | Array etter steget |
|---|---|---|---|---|
| 1 | 7 | 7 | ingen | 7 |
| 2 | 3 | 7, 3 | 1<->0 | 3, 7 |
| 3 | 9 | 3, 7, 9 | ingen | 3, 7, 9 |
| 4 | 1 | 3, 7, 9, 1 | 3<->1 ; 1<->0 | 1, 3, 9, 7 |
| 5 | 4 | 1, 3, 9, 7, 4 | ingen | 1, 3, 9, 7, 4 |
Sluttilstand (arrayet, indeks fra 0):
1, 3, 9, 7, 4 1
(0)
3 9
(1) (2)
7 4
(3) (4)- gyldig min-heap: JA | antall elementer: 5Steg 4 er det som er verdt å studere. Etter at 1 er lagt på indeks 3, er
forelderen indeks med verdien 7. Siden ,
byttes de. Nå står 1 på indeks 1, og forelderen er indeks
med verdien 3. Siden , byttes de igjen, og
1 er på rota. Det er nøyaktig de to byttene figuren over viser.
Steg 5 er det som er lett å overse. 4 legges på indeks 4, forelderen er
indeks med verdien 3, og . Ingen bytte. Det
er helt normalt at en innsetting ikke flytter noe — mange håndkjøringer går galt
fordi kandidaten «føler» at det burde skje noe.
Kontroll: heapen har fem elementer, og de fem tallene du fikk utdelt er alle
med. Sjekk deretter de tre forelder–barn-parene: , ,
, . Alt holder.
Fellenote. Fella her er felle #9 — å prøve å holde arrayet sortert, som
om det var et søketre. 1, 3, 9, 7, 4 er ikke sortert, og skal ikke være det. En
heap er sortert oppover, ikke bortover.
Sett inn 12, 9, 14, 6, 11 og 3 i denne rekkefølgen i en tom
min-heap (array, indeks fra 0). Oppgi arrayet til slutt.
Sett inn 5, 8, 6, 9, 12, 7 og 10 i denne rekkefølgen i en tom
min-heap (array, indeks fra 0).
a) Oppgi arrayet til slutt.
b) Hvor mange bytter ble utført totalt, og hva forteller det om
innsettingsrekkefølgen?
Min-heapen 4, 6, 5, 9, 8, 7, 10 er gitt (array, indeks fra 0). Sett inn 2, og
oppgi arrayet etterpå.
Startheap: 4, 6, 5, 9, 8, 7, 10. Sett inn 2.
Legg 2 på første ledige indeks, 7: 4, 6, 5, 9, 8, 7, 10, 2
| Delsteg | Handling | Array etter delsteget |
|---|---|---|
| 1 | H[7] = 2 < forelder H[3] = 9, bytt | 4, 6, 5, 2, 8, 7, 10, 9 |
| 2 | H[3] = 2 < forelder H[1] = 6, bytt | 4, 2, 5, 6, 8, 7, 10, 9 |
| 3 | H[1] = 2 < forelder H[0] = 4, bytt | 2, 4, 5, 6, 8, 7, 10, 9 |
| 4 | elementet er på rota, stopp | 2, 4, 5, 6, 8, 7, 10, 9 |
Sluttilstand:
2, 4, 5, 6, 8, 7, 10, 9Dette er verste tilfelle for én innsetting. Elementet 2 er mindre enn alt
annet i heapen, så det må helt fra bunnen til rota: tre bytter i en heap med åtte
elementer. Antall nivåer over bunnen er
, som er nøyaktig det vi teller. Det er hele
begrunnelsen for at Insert er — du kan aldri gjøre flere bytter enn
det er nivåer.
Følg indeksene, ikke bildet. Fra indeks 7 er forelderen
; fra indeks 3 er den ; fra
indeks 1 er den . Kjeden er
stien opp, og den er den eneste delen av arrayet som endres.
Delsteg 4 er ikke overflødig. Løkka må stoppe fordi i = 0, ikke fordi noen
sammenligning slo feil. Det er i > 0-leddet i pseudokoden som gjør jobben.
Fellenote. Fella her er å legge det nye elementet på «riktig» plass med én
gang, som i en sortert liste. Nye elementer legges alltid på første ledige
indeks, og flyttes deretter. Gjør du noe annet, brytes strukturkravet, og alle
indeksformlene slutter å gjelde.
Min-heapen 3, 5, 4, 9, 8, 6, 7 er gitt (array, indeks fra 0). Sett
inn 11, og oppgi arrayet etterpå. Hvor mange bytter kreves?
Løkke 3 — RemoveMin og down-heap (ca. 14 min)
— naturlig pausepunkt —
Nå til den operasjonen prioritetskøen egentlig finnes for: å plukke ut det minste
elementet.
Selve uthentingen er gratis — det minste ligger på indeks 0. Problemet er hullet
som blir igjen. Løsningen er å fylle det med det siste elementet i arrayet
(det eneste som kan fjernes uten å ødelegge strukturkravet), krympe arrayet, og
deretter la det innflyttede elementet synke til det er på plass.
Nedstigningen heter down-heap, og den har to detaljer som er hele forskjellen
mellom full og halv uttelling:
1. Du synker alltid mot det MINSTE av barna. Bytter du med det største, får
du et nytt brudd på veien ned, og heapen blir ugyldig.
2. Du må sjekke at barnet finnes. Nederst i treet har noder ingen barn, og
noen har bare venstre barn. Å lese H[2i+2] uten å sjekke er felle #3 i
bokas feilregister, og den har eksplisitt takpoeng i sensorveiledningene: uten
sjekken får du maks delvis uttelling, uansett hvor riktig resten er.
H er et array med indeks fra 0 som oppfyllerheap-egenskapen.
|H| er antall elementer. Alle divisjoner erheltallsdivisjoner.
Prebetingelse: H er en gyldig, ikke-tom min-heap. Postbetingelse: det
minste elementet er returnert og fjernet, og H er fortsatt en gyldig min-heap
med ett element færre.
Arbeidet deles i to: RemoveMin gjør selve uthentingen, og overlater
reparasjonen til DownHeap.
Procedure RemoveMin(H)
Input: ikke-tom min-heap H som array (indeks fra 0)
Output: det minste elementet; H er krympet med ett og reparert
minste = H[0]
H[0] = H[|H| - 1]
fjern siste element fra H
DownHeap(H, 0, |H|)
return minste
Kjoeretid: O(log n)DownHeap(H, i, n) lar elementet paa indeks i synke saa lenge det er stoerre
enn det minste barnet sitt:
Procedure DownHeap(H, i, n)
Input: array H, startindeks i, antall elementer n
Output: H der subtreet med rot i oppfyller heap-egenskapen
while 2*i + 1 < n:
minste = 2*i + 1
if 2*i + 2 < n and H[2*i + 2] < H[minste]:
minste = 2*i + 2
if H[i] <= H[minste]:
bryt ut av loekka
bytt H[i] og H[minste]
i = minste
Kjoeretid: O(log n)De to sjekkene som utgjør felle #3, står begge i DownHeap:
1. while 2*i + 1 < n — finnes venstre barn i det hele tatt? Er svaret nei,
er noden et blad, og det er ingenting å synke ned til.
2. if 2*i + 2 < n and ... — finnes høyre barn? Er svaret nei, er venstre
barn det eneste kandidaten, og sammenligningen med H[2*i + 2] må ikke
utføres.
Rekkefølgen i det andre leddet er avgjørende: 2*i + 2 < n må stå førH[2*i + 2] < H[minste], ellers leses en indeks som ikke finnes.
Invarianten i én setning: etter hvert bytte er alt over i en gyldig
min-heap, og det eneste mulige bruddet ligger mellom i og barna — så når løkka
stopper, finnes det ingen brudd.
Kjøretid: , der er antall elementer. Elementet synker høyst
ett nivå per runde, og treet har nivåer. Selve
uthentingen av H[0] er — det er reparasjonen som koster.
Utfør RemoveMin én gang på min-heapen 2, 5, 3, 9, 8, 4 (array, indeks fra 0).
Oppgi arrayet etterpå.
Startheap (indeks fra 0): 2, 5, 3, 9, 8, 4 — gyldig min-heap: JA
RemoveMin nr. 1 — returnerer 2.
| Delsteg | Handling | Array etter delsteget |
|---|---|---|
| 1 | ta vare på H[0] = 2; flytt siste element 4 til rot og krymp arrayet | 4, 5, 3, 9, 8 |
| 2 | barna er H[1] = 5 og H[2] = 3, minste er H[2] = 3; 4 > 3, bytt | 3, 5, 4, 9, 8 |
| 3 | indeks 2 har ingen barn (2*2+1 = 5 er utenfor n = 5), stopp | 3, 5, 4, 9, 8 |
Array etter RemoveMin nr. 1:
3, 5, 4, 9, 8 3
(0)
5 4
(1) (2)
9 8
(3) (4)- gyldig min-heap: JADelsteg 2 er hele poenget. Elementet 4 har barna 5 og 3. Hadde vi byttet med
venstre barn (5) fordi det står først, ville arrayet blitt 5, 4, 3, 9, 8 —
og der er , altså et brudd på heap-egenskapen i rota. Down-heap går
alltid mot det minste barnet, og det er derfor.
Delsteg 3 er den sjekken oppgaven egentlig tester. Etter byttet står 4 på
indeks 2. Venstre barn ville vært indeks , men heapen har nå
bare fem elementer, altså indeksene 0 til 4. Indeks 5 finnes ikke, løkka stopper,
og vi er ferdige. Uten den sjekken ville algoritmen lest utenfor arrayet.
Sluttilstanden du leverer, er hele arrayet: 3, 5, 4, 9, 8. Ikke treet, ikke
bare det som ble flyttet — hele arrayet, kommaseparert, med indeks fra 0.
Fellenote. Fella her er felle #3 — down-heap uten å sjekke at barnet
finnes. Den har eksplisitt takpoeng i sensorveiledningene: leser du H[5] i en
heap med fem elementer, er algoritmen din feil, uansett hvor riktig resten ser
ut.
De to første er de dyreste, og begge sitter i down-heap.
- Down-heap uten å sjekke at barnet finnes. Dette er felle #3 i bokas
feilregister, og den har eksplisitt takpoeng i sensorveiledningene. Du trenger
to sjekker: 2*i + 1 < n for om venstre barn finnes i det hele tatt, og
2*i + 2 < n før du i det hele tatt sammenligner med høyre barn.
Kontrollen: har du noen gang lest en indeks som er i sporingen din?
- Å synke mot feil barn. Down-heap går mot det minste barnet, ikke mot
venstre barn og ikke mot det største. Bytter du med det største, lager du et
nytt brudd rett under deg.
- Å blande 0- og 1-indeksering. IN2010 bruker
, og . Formlene
, og hører til 1-indekserte framstillinger og
gir feil node her. Kontroll: forelderen til indeks 0 skal ikke finnes.
- Å forveksle heap-egenskapen med BST-egenskapen. Felle #9. Heapen ordner
opp–ned; søketreet ordner venstre–høyre. I en heap kan venstre barn godt
være større enn høyre barn, og arrayet er ikke sortert.
- Å påstå at BuildHeap er . Den er , og du får se
tellingen i neste løkke. Det er selve heapsorteringen som er ,
fordi de uthentingene koster hver — se
kap. 2.2.
- Å levere treet i stedet for arrayet. Svarformatet for en heap-håndkjøring
er hele arrayet, kommaseparert, med indeks fra 0. Et pent tegnet tre uten
arrayet under gir ikke full uttelling.
Og den gjennomgående: å bruke uten å definere det — felle #10. Si i
samme setning at er antall elementer i heapen.
Utfør RemoveMin én gang på min-heapen 3, 7, 5, 12, 9, 6 (array,
indeks fra 0).
a) Oppgi arrayet etterpå.
b) Hvilket element ble returnert, og hvor mange bytter kostet reparasjonen?
Løkke 4 — BuildHeap, og hvorfor den er (ca. 10 min)
Du har et vilkårlig array og vil gjøre det om til en heap. Den opplagte måten er
å sette inn ett element om gangen: innsettinger à , altså
.
Det finnes en bedre måte, og forskjellen er ikke bare konstantfaktorer — den er
en hel -klasse.
Ideen: i stedet for å bygge ovenfra og ned, går du nedenfra og opp. Den
nederste halvparten av arrayet er blader, og et blad er allerede en gyldig heap i
seg selv. Start derfor på den siste indre noden, indeks
, og kjør DownHeap derfra og bakover til indeks 0.
Procedure BuildHeap(A)
Input: vilkaarlig array A med n elementer (indeks fra 0)
Output: A omgjort til en gyldig min-heap
n = |A|
for i = n/2 - 1 ned til 0:
DownHeap(A, i, n)
Kjoeretid: O(n)Hvorfor blir dette billigere? Fordi de fleste nodene ligger nær bunnen og har
kort vei å synke. Den ene noden som kan synke helt gjennom treet, er rota — og
det er én node. Teoremet under teller det opp.
En -påstand uten telling er verre enn ingen påstand, så her er tellingen.
Et komplett tre med høyde har høyst noder på
dybde , og DownHeap fra dybde gjør høyst bytter — den kan ikke
synke lenger enn til bunnen. Ganger vi de to sammen og summerer over alle dybder,
får vi det totale arbeidet.
(høyde ):
| Dybde | Antall noder | Maks bytter per node () | Produkt |
|---|---|---|---|
| 0 | 1 | 3 | 3 |
| 1 | 2 | 2 | 4 |
| 2 | 4 | 1 | 4 |
| 3 | 8 | 0 | 0 |
| Sum | 15 | 11 |
Sum av arbeidet er 11, og . Arbeidet er altså under , ikke
.
(høyde ):
| Dybde | Antall noder | Maks bytter per node () | Produkt |
|---|---|---|---|
| 0 | 1 | 4 | 4 |
| 1 | 2 | 3 | 6 |
| 2 | 4 | 2 | 8 |
| 3 | 8 | 1 | 8 |
| 4 | 16 | 0 | 0 |
| Sum | 31 | 26 |
Sum av arbeidet er 26, og . Arbeidet er altså under , ikke
.
(høyde ):
| Dybde | Antall noder | Maks bytter per node () | Produkt |
|---|---|---|---|
| 0 | 1 | 5 | 5 |
| 1 | 2 | 4 | 8 |
| 2 | 4 | 3 | 12 |
| 3 | 8 | 2 | 16 |
| 4 | 16 | 1 | 16 |
| 5 | 32 | 0 | 0 |
| Sum | 63 | 57 |
Sum av arbeidet er 57, og . Arbeidet er altså under , ikke
.
Les mønsteret i de tre tabellene. Halvparten av nodene ligger på nederste
nivå og gjør null arbeid. En firedel ligger ett nivå over og gjør høyst ett
bytte. En åttedel gjør høyst to. Antall noder halveres for hvert nivå oppover,
mens arbeidet per node bare vokser med én — og en halvering slår en økning på én.
Totalen holder seg under for alle tre -verdiene, og forholdet mellom sum
og synker ikke ut av kontroll når vokser: , , —
alle under 1.
Konklusjonen: BuildHeap er , der er antall elementer i arrayet.
Og avgrensningen, som er like viktig: heapsort er likevel
— ikke fordi byggingen koster det, men fordi de uthentingene
etterpå koster hver. Det er blandingen av de to fasene som skaper
feilen «BuildHeap er ». Se
kap. 2.2.
Gjør arrayet 9, 4, 7, 1, 8, 3, 6, 2, 5 om til en min-heap med BuildHeap.
Oppgi arrayet til slutt og antall bytter.
Array før: 9, 4, 7, 1, 8, 3, 6, 2, 5 (n = 9)
Down-heap kjøres fra indeks 3 (= n/2 - 1) og nedover til 0. Indeksene 4 til 8 er blader og har ingen barn.
| Steg | Indeks i | Verdi | Bytter | Array etter steget |
|---|---|---|---|---|
| 1 | 3 | 1 | ingen | 9, 4, 7, 1, 8, 3, 6, 2, 5 |
| 2 | 2 | 7 | 2<->5 | 9, 4, 3, 1, 8, 7, 6, 2, 5 |
| 3 | 1 | 4 | 1<->3 ; 3<->7 | 9, 1, 3, 2, 8, 7, 6, 4, 5 |
| 4 | 0 | 9 | 0<->1 ; 1<->3 ; 3<->7 | 1, 2, 3, 4, 8, 7, 6, 9, 5 |
Sluttilstand:
1, 2, 3, 4, 8, 7, 6, 9, 5 1
(0)
2 3
(1) (2)
4 8 7 6
(3) (4) (5) (6)
9 5
(7) (8)- gyldig min-heap: JA- totalt antall bytter: 6 (mot n = 9 elementer)
Merk hvor løkka starter. Med er .
Indeksene 4 til 8 hoppes helt over, fordi de er blader — fem av ni noder gjør
ingenting overhodet. Det er halve forklaringen på i praksis.
Merk arbeidsfordelingen. Steg 1 gir null bytter, steg 2 gir ett, steg 3 gir
to, og steg 4 — rota — gir tre. Nøyaktig det mønsteret teoremet over teller:
mange noder med lite arbeid, én node med mest.
Totalen: 6 bytter for 9 elementer. Til sammenligning ville her
vært omtrent . Forskjellen er ikke en konstantfaktor du kan se
bort fra, den er en -klasse.
Fellenote. Fella her er å kjøre DownHeap ovenfra og ned i stedet for
nedenfra og opp. Går du fra indeks 0 og oppover, kan et element du «reparerte»
tidlig bli feilplassert av en senere reparasjon — og resultatet er ikke en gyldig
heap. Nedenfra og opp virker fordi hvert subtre allerede er en gyldig heap når du
kommer til rota i det.
Gjør arrayet 10, 20, 15, 30, 40, 5, 25 om til en min-heap medBuildHeap.
a) På hvilken indeks starter løkka, og hvorfor?
b) Oppgi arrayet til slutt og antall bytter.
Løkke 5 — de faste heap-faktaene (ca. 5 min)
Fire påstander om heaper går igjen på Del 1, år etter år. Tre av dem er laget for
å fange den som «nesten» husker. Her er de, med begrunnelsene.
Reparasjonen etter en innsetting: det nye elementet sammenlignes med forelderen
sin og bytter oppover så lenge det er mindre.
Én sti fra bunnen mot rota, høyst bytter, altså
. Ingen andre deler av heapen berøres. Stopper når forelderen er
mindre enn eller lik elementet, eller når rota er nådd.
Reparasjonen nedover: elementet sammenlignes med det minste av barna sine og
synker så lenge det er størst.
Én sti fra rota mot bunnen, høyst bytter, altså
. To sjekker er obligatoriske: at venstre barn finnes
(), og at høyre barn finnes () før det sammenlignes.
Å utelate dem er felle #3.
Legger elementet på første ledige indeks, altså bakerst i arrayet, og lar det
stige med sift-up.
i verste tilfelle, der er antall elementer. Beste tilfelle er
— når elementet allerede er større enn forelderen sin der det landet.
Strukturkravet gjør at det ikke finnes noen annen plass å legge det.
Returnerer H[0], flytter det siste elementet til rota, krymper arrayet og
kjører down-heap fra indeks 0.
, der er antall elementer. Selve uthentingen er ; det er
reparasjonen som koster. Merk at det er det siste elementet som flyttes opp,
ikke et av barna — alt annet ville brutt strukturkravet.
Leser det minste elementet uten å fjerne det. Det ligger alltid på indeks 0.
. Dette er heapens billigste operasjon og hele grunnen til at strukturen
brukes som prioritetskø. Kontrast: å finne det største i en min-heap er
.
Gjør et vilkårlig array om til en heap ved å kjøre down-heap fra indeks
og bakover til 0.
— ikke . Grunnen er arbeidsfordelingen: halvparten av
nodene er blader og gjør ingenting, en firedel synker høyst ett nivå, en åttedel
høyst to. Summen holder seg under . Påstanden «å bygge en heap fra et
vilkårlig array tar » er en fast distraktor, og den er usann.
Avgjør for hvert av arrayene om det er en gyldig min-heap (indeks fra 0).
a) 1, 5, 3, 8, 6, 4, 2
b) 2, 4, 3, 7, 5, 9, 6
Framgangsmåten er mekanisk: gå gjennom hver indre node og sammenlign med begge
barna. Med sju elementer er de indre nodene 0, 1 og 2, altså seks par å sjekke.
a) Array: 1, 5, 3, 8, 6, 4, 2
1
(0)
5 3
(1) (2)
8 6 4 2
(3) (4) (5) (6)- gyldig min-heap: NEI — H[2] = 3 har barnet H[6] = 2, og 2 < 3.
- kontroll av alle foreldre-barn-par:
- H[0] = 1 mot H[1] = 5: OK
- H[0] = 1 mot H[2] = 3: OK
- H[1] = 5 mot H[3] = 8: OK
- H[1] = 5 mot H[4] = 6: OK
- H[2] = 3 mot H[5] = 4: OK
- H[2] = 3 mot H[6] = 2: BRUDD
b) Array: 2, 4, 3, 7, 5, 9, 6
2
(0)
4 3
(1) (2)
7 5 9 6
(3) (4) (5) (6)- gyldig min-heap: JA — hver forelder er mindre enn eller lik begge barna sine.
- kontroll av alle foreldre-barn-par:
- H[0] = 2 mot H[1] = 4: OK
- H[0] = 2 mot H[2] = 3: OK
- H[1] = 4 mot H[3] = 7: OK
- H[1] = 4 mot H[4] = 5: OK
- H[2] = 3 mot H[5] = 9: OK
- H[2] = 3 mot H[6] = 6: OK
Sammenlign de to. I (b) står 9 til venstre for 6, altså er venstre barn
større enn høyre barn — og det er ikke noe brudd. I (a) er det heller ikke
søskenforholdet som feiler, men at 2 ligger under 3. Ordningen går opp og ned.
Ett par er nok. Så snart du finner ett brudd, er svaret nei, og du kan
stoppe. Sensor ber som regel om hvilket par som bryter — oppgi indeksene, ikke
bare verdiene.
Fellenote. Fella her er felle #9 — å lete etter BST-egenskapen i stedet
for heap-egenskapen. Er du i tvil: sjekk bare forelder mot barn, aldri søsken
mot søsken.
Avgjør sant eller usant, og begrunn hver med én setning.
a) Du kan finne det største elementet i en min-heap i .
b) Å gjøre et vilkårlig array om til en heap koster .
c) I et komplett binært tre ligger over halvparten av nodene på de to
nederste nivåene.
d) En min-heap blir en gyldig max-heap hvis du reverserer arrayet.
En kollega har skrevet denne prosedyren og påstår at den
reparerer en min-heap nedover:
Procedure DownHeap(H, i, n)
Input: array H, startindeks i, antall elementer n
Output: H reparert nedover fra i
while i < n:
venstre = 2*i + 1
hoeyre = 2*i + 2
if H[venstre] < H[hoeyre]:
minste = venstre
else:
minste = hoeyre
bytt H[i] og H[minste]
i = minstea) Finn alle feilene.
b) Vis konkret hva som går galt når prosedyren kjøres med i = 0 på arrayet4, 5, 3, 9, 8 med .
c) Skriv en korrekt versjon.
Begrepsbank
Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har
lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder
kjernestoffet.
De to sjekkene som må stå i enhver down-heap: 2*i + 1 < n for om venstre barn
finnes i det hele tatt, og 2*i + 2 < n før høyre barn brukes i en
sammenligning.
Uten dem leser algoritmen utenfor arrayet. Dette er felle #3 i bokas
feilregister, og den eneste fellen i Del 4 med eksplisitt takpoeng: mangler
sjekken, er full uttelling utelukket uansett hvor riktig resten er.
Når et element skal synke, byttes det med det minste av de to barna — ikke
med venstre barn, og ikke med det største.
Grunnen: bytter du med det største, blir det du flyttet opp større enn søskenet
sitt, og du har laget et nytt brudd rett under deg. Med det minste blir den nye
forelderen mindre enn begge barna, og bruddet forsvinner.
Det største elementet i en min-heap ligger i et blad, men heap-egenskapen
sier ikke hvilket. Bladene er omtrent halvparten av nodene, og alle må sjekkes.
Kjøretiden er derfor , ikke . Påstanden «du kan finne det
største elementet i en min-heap i » er usann og er en fast
distraktor på Del 1. Trenger du både min og max billig, trenger du to strukturer
— eller en annen struktur.
Å snu arrayet til en min-heap gir ikke en gyldig max-heap.
Reverseringen snur rekkefølgen i arrayet, men den snur ikke forelder–barn-
forholdene: forelderen til indeks blir ikke barnet til den reverserte
indeksen. Konkret gir min-heapen 5, 8, 6, 9, 12, 7, 10 reversert10, 7, 12, 9, 6, 8, 5, der rota 10 har barnet 12 — brudd med én gang. Vil du ha
en max-heap, må du bygge den med motsatt sammenligning.
I et komplett binært tre ligger over halvparten av nodene på de to nederste
nivåene.
Antall noder dobles for hvert nivå nedover, så det nederste nivået alene rommer
omtrent halvparten. Konkret, for : nivåene med indeks 3 til 6 (4 noder) og
7 til 14 (8 noder) utgjør 12 av 15 noder, altså 80 %. Denne fordelingen er hele
grunnen til at BuildHeap er .
kanter fra rota til det dypeste bladet.
Fordi treet alltid er komplett, er dette den lavest mulige høyden for noder —
en heap kan aldri bli skjev slik et vanlig søketre kan. Det er derfor Insert ogRemoveMin er også i verste tilfelle. Merk skillet: en heap med 8
elementer har høyde 3 og fire nivåer.
Speilbildet av min-heapen: hver forelder er større enn eller lik begge barna,
og det største elementet ligger på rota.
Samme struktur, samme indeksformler, samme kjøretider — bare motsatt
sammenligning i sift-up og down-heap. En max-heap fås ved å bygge på nytt med
snudd sammenligning, ikke ved å reversere arrayet til en min-heap.
Bygger en heap av arrayet () og henter deretter ut elementene ett for ett
( ganger ).
Totalt i alle tilfeller, in-place, men ikke stabil —
uthentingen flytter elementer over lang avstand. Merk hvor de to tallene kommer
fra: byggingen er lineær, uthentingen er logaritmisk per element. Se
kap. 2.2.
elementene som flyttet seg.
Delvis riktig array gir delvis uttelling, så lever alltid noe. Tegner du treet i
tillegg, er det greit — men arrayet må stå der, for det er det sensor ber om.
Begge gir på innsetting og uthenting av det minste elementet, så i
-klasse er de like — påstanden om at et AVL-tre kan brukes som prioritetskø
med samme orden som en heap, er sann.
Heapen vinner likevel i praksis: den er ett array uten pekere, den har lave
konstantfaktorer, og den bygges på . Søketreet vinner når du i tillegg
trenger sortert traversering eller søk på vilkårlige nøkler. Se
kap. 4.3.
Heapen ordner opp–ned: forelderen er mindre enn eller lik begge barna. Et
binært søketre ordner venstre–høyre: alt til venstre er mindre, alt til høyre
er større.
Å blande dem er felle #9 i bokas feilregister. To følger å merke seg: i en
min-heap kan venstre barn godt være større enn høyre barn, og arrayet til en heap
er ikke sortert. Se kap. 4.1.
Del 1-sjangeren der du får en struktur og en operasjonsrekke og skal oppgi kun
sluttilstanden.
Minst én per sett, ofte to, og heapen er den hyppigste kandidaten (86 %, 6 av 7
sett). Formatet er strengt: hele arrayet, indeks fra 0. Delvis riktig tilstand
gir delvis uttelling, så la aldri svaret stå tomt.
Del 1-sjangeren med sant/usant-påstander som rettes automatisk, med
antigjettings-skalering: summen skaleres slik at ren gjetting i snitt gir
null poeng.
Heap-faktaene er blant de mest brukte: BuildHeap er , max i en min-heap
er , reversering gir ikke en max-heap, venstre barn er ikke alltid minst.
Merk den andre halvdelen av regelen: på vanlige korte svar, som ikke er
strafferammet, teller ubesvart som feil, så der skal du alltid svare.
Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.
Skolesaga er en uavhengig læringsressurs og er ikke tilknyttet eller godkjent av Universitetet i Oslo. Dette er ikke offisielt studiemateriell. Les mer.