3.1 Hauger og Heapsort
Maks-haugen som array, `Max-Heapify`, `Build-Max-Heap`, `Heap-Extract-Max` og `Heapsort` — inkludert at `Build-Max-Heap` er `Θ(n)`, ikke `Θ(n\lg n)`.
Temaet er registrert i 16 av de 17 settene i grunnlaget (94 %), og
sorteringstemaet som
Heapsort hører til, i 17 av de 17 settene (100 %).Temaet testes på to måter, og de krever helt ulike ferdigheter:
- Sjanger C — håndkjøring, altså at du utfører algoritmen steg for steg på
papir og oppgir bare sluttilstanden. Du får et array og et algoritmenavn,
og skal levere arrayet etterpå. Ikke en forklaring, ikke en tegning: arrayet.
- Sjanger E — kjøretidskunnskap, altså at du oppgir kjøretiden til en
navngitt operasjon i det strammeste uttrykket som er riktig. Her ligger
fagets mest brukte felle: at Build-Max-Heap skulle være .
Den er , og du får se tellingen som beviser det.
Høyeste prioritet — dette må sitte. Eksamen er hjelpemiddelfri, så både
indeksformlene og kjøretidene må ligge i hodet.
Tre ting er verdt å merke seg med én gang:
1. Arrayet er A[1..n], med rota på indeks 1. Forelderen til er
, og barna er og . Dette er konvensjonen
CLRS bruker, og den NTNU bruker i oppgavene og løsningsforslagene. Blander
du inn en annen indekseringsvane, blir hver eneste indeks feil.
2. Haugegenskapen er ikke søketreegenskapen. En maks-haug ordner
opp–ned; et binært søketre ordner venstre–høyre. Forvekslingen er den mest
fremhevede datastrukturfeilen i faget, og den koster hele oppgaven.
3. Du utfører algoritmen mekanisk, også når inputen er ugyldig. Ber
oppgaven deg kjøre en haugoperasjon på et array som ikke er en gyldig haug,
skal du gjøre nøyaktig det algoritmen sier — ikke reparere først.
Slik er kapitlet lagt opp (55 min):
| # | Innhold | Tid |
|---|---|---|
| 1 | Haugegenskapen og arrayet | ca. 12 min |
| 2 | Max-Heapify — å sive ett element ned | ca. 12 min |
| 3 | Build-Max-Heap, og hvorfor den er lineær | ca. 14 min |
| 4 | Heap-Extract-Max og prioritetskøen | ca. 9 min |
| 5 | Heapsort | ca. 8 min |
Forkunnskaper
- kap. 2.1 — sammenligningsbaserte sorteringer.
Heapsort er en av dem, og den måles mot de andre. Dette sto der:
Merge-Sort er men bruker ekstra plass, Insertion-Sort
er i verste tilfelle, og ingen sortering som bare sammenligner
elementer kan gjøre det bedre enn i verste tilfelle.
- kap. 1.1 — asymptotisk notasjon. Du trenger
forskjellen mellom (bare øvre grense vist) og (tett grense,
både øvre og nedre), fordi kapitlet her bruker begge bevisst.
Er logaritmen fersk: Potenser og logaritmer. I hele denne boka
betyr det samme som , altså «hvor mange ganger må du halvere
før du er nede på 1». Det er den samme konvensjonen CLRS bruker.
Vil du se sortering konkret i kode først:
Sortering: boblesortering, .sort() og gitt sort_list.
Haugegenskapen og arrayet (~12 min)
Se for deg vaktlista på en legevakt. Pasientene står ikke i den rekkefølgen de
kom; den mest alvorlige skal alltid tas først. Du trenger derfor en struktur
som gjør én ting veldig godt: gi meg den viktigste akkurat nå, og la meg
legge til nye pasienter underveis uten å sortere hele lista på nytt.
En sortert liste ville løst det, men den er dyr å holde ved like: hver
innsetting må skyve resten. En maks-haug gir deg det viktigste elementet
umiddelbart, og betaler bare en logaritmisk pris per endring. Prisen er at
resten av strukturen er nesten usortert — og det er nettopp det som gjør den
billig.
Det smarte er at haugen ikke trenger pekere i det hele tatt. Den ER et vanlig
array. Treet du tegner på papiret finnes bare i hodet ditt; på maskinen ligger
alt etter hverandre, nivå for nivå.
I en maks-haug er hver forelder større enn eller lik begge barna sine.
Ordningen går opp og ned, aldri sideveis. Det står ingenting om forholdet
mellom to søsken: venstre barn kan godt være større enn høyre barn, og to
noder på samme nivå kan stå i hvilken som helst rekkefølge.
Konsekvensen er at det største elementet garantert ligger på rota — og
bare det. Resten av arrayet er så godt som usortert, og det er meningen: en
haug er billig nettopp fordi den lover lite.
Formelt, for hver indeks som har barn: .
En maks-haug lagres som et helt vanlig array A[1..n], fylt nivå for nivå
ovenfra og ned, og venstre før høyre innenfor hvert nivå. Rota ligger påA[1].
Denne boka indekserer fra 1, slik CLRS gjør, og slik oppgavene og
løsningsforslagene i TDT4120 gjør. Det er ikke en smakssak: hele
indeksregningen under henger på det, og en formel fra en annen
indekseringsvane gir feil node hver eneste gang.
Fordi treet alltid fylles nivå for nivå uten hull, er høyden — den lavest mulige for noder. Alt som koster «én sti opp eller
ned» er derfor .
Fra en indeks finner du hele familien med tre regnestykker, uten å tegne
noe:
forelderen ligger på , venstre barn på og høyre barn
på .
Kanttilfellene er der oppgaver felles. Indeks 1 er rota og har ingen
forelder. Er , har noden ingen barn i det hele tatt — den er et blad.
Er men , har den bare venstre barn, og da skal du bare
sammenligne med det ene.
Alle tre regnestykkene er : konstant tid, uansett hvor stor haugen
er.
A.heap-size er hvor mange av arrayets elementer som teller som en del avhaugen akkurat nå. Den er ikke det samme som arrayets lengde.
Skillet finnes fordi to av operasjonene krymper haugen uten å krympe arrayet:Heap-Extract-Max flytter siste haugelement til rota og setter A.heap-size
ned med én, og Heapsort legger det største elementet bakerst i arrayet og
tar det ut av haugen ved å gjøre det samme.
Praktisk konsekvens for håndkjøring: elementene bak A.heap-size ligger
fortsatt i arrayet, men de er usynlige for Max-Heapify. Oppgaven ber
som regel om hele arrayet, og da skal de med — men de skal ikke sammenlignes.
Arrayet A = 41, 27, 33, 19, 22, 12, 30, 7, 15 er oppgitt med indeks fra 1.
a) Er det en gyldig maks-haug?
b) Sett opp forelderen og de to barna for hver indeks, og oppgi hvilke
indekser som er blader.
er større enn eller lik begge barna.
. . .
. Indeksene 5 til 9 har ingen barn, siden . Ingen brudd, altså gyldig.
b) Med , og :
A[i] | forelder | venstre | høyre | |
|---|---|---|---|---|
| 1 | 41 | ingen (rot) | A[2] = 27 | A[3] = 33 |
| 2 | 27 | A[1] = 41 | A[4] = 19 | A[5] = 22 |
| 3 | 33 | A[1] = 41 | A[6] = 12 | A[7] = 30 |
| 4 | 19 | A[2] = 27 | A[8] = 7 | A[9] = 15 |
| 5 | 22 | A[2] = 27 | ingen | ingen |
| 6 | 12 | A[3] = 33 | ingen | ingen |
| 7 | 30 | A[3] = 33 | ingen | ingen |
| 8 | 7 | A[4] = 19 | ingen | ingen |
| 9 | 15 | A[4] = 19 | ingen | ingen |
Bladene er indeksene 5, 6, 7, 8 og 9 — alle med , altså alle
.
Tegnet som tre ser den samme haugen slik ut:
41
(1)
27 33
(2) (3)
19 22 12 30
(4) (5) (6) (7)
7 15
(8) (9)Legg merke til to ting som ofte forvirrer. A[4] = 19 er mindre ennsøskenet
A[5] = 22, og A[7] = 30 er større enn A[2] = 27 som ligger etnivå høyere. Begge deler er helt greit: haugegenskapen sier bare noe om
forelder mot barn, ikke noe om søsken og ikke noe om noder i ulike grener.
Bruk haugen A = 41, 27, 33, 19, 22, 12, 30, 7, 15 fra Eksempel 1.
a) Hvilken indeks ligger forelderen til A[7] på, og hvilken verdi står
der?
b) Har A[3] to barn, ett barn eller ingen barn? Begrunn med
indeksregning.
c) Forklar med én setning hvorfor A[5] = 22 godt kan være større ennA[4] = 19 uten at haugegenskapen er brutt.
Avgjør for hvert array om det er en gyldig maks-haug med indeks fra 1. Svar
ja eller nei, og oppgi det første bruddet du finner.
a) 48, 35, 40, 12, 30, 21, 9
b) 48, 35, 40, 12, 30, 44, 9
c) 9, 12, 21, 30, 35, 40, 48
d) 50, 50, 22, 18, 50, 7
Max-Heapify — å sive ett element ned (~12 min)
Alle haugoperasjonene bygger på én eneste reparasjonsrutine. Situasjonen den
takler er alltid den samme: ett element står på feil plass, og alt under
det er i orden.
Det er en veldig spesifikk kontrakt, og den er verdt å lese to ganger.Max-Heapify(A, i) forutsetter at de to deltrærne under A[i] allerede er
gyldige maks-hauger. Bare A[i] selv kan være for liten. Rutinen lar da det
elementet synke nedover langs én sti til det finner plassen sin.
Retningen er hele poenget: elementet bytter med det største av de to
barna. Bytter du med det minste, lager du et nytt brudd rett under deg — og
da har du ikke reparert noe, bare flyttet feilen.
A[1..n], indeks fra 1.A.heap-size sier hvor mange elementer som er med i haugen. Divisjonenfloor(i/2) er heltallsdivisjon.Prebetingelse: deltrærne med rot i 2i og 2i+1 er begge gyldige
maks-hauger; bare A[i] kan bryte haugegenskapen.
Postbetingelse: deltreet med rot i i er en gyldig maks-haug, og det
inneholder nøyaktig de samme elementene som før.
Max-Heapify(A, i)
Input: array A[1..n], indeks i der begge deltrarne allerede er maks-hauger
Output: deltreet med rot i i er en gyldig maks-haug
l = 2*i
r = 2*i + 1
storst = i
if l <= A.heap-size and A[l] > A[storst]
storst = l
if r <= A.heap-size and A[r] > A[storst]
storst = r
if storst != i
bytt A[i] og A[storst]
Max-Heapify(A, storst)
Kjoeretid: O(lg n)Grunnideen i én setning: når det største av de tre elementene A[i],A[2i] og A[2i+1] er løftet opp til i, kan det eneste gjenværende
bruddet ligge i deltreet vi nettopp byttet med — og der løser det samme
kallet problemet på nytt, ett nivå lenger ned.
Legg merke til l <= A.heap-size som første ledd i begge testene. Uten
den ville rutinen lese utenfor haugen. Rekkefølgen er ikke likegyldig:
grensesjekken må stå først, slik at sammenligningen aldri utføres på en
indeks som ikke finnes.
Kjøretid: , der er antall elementer i haugen. Elementet
flytter seg høyst ett nivå per bytte, og treet har
nivåer — én sti nedover, ingen forgrening. Kontrollert numerisk: over 20 000
tilfeldige hauger gjorde ingen kjøring flere enn
bytter.
Utfør Max-Heapify(A, 1) på arrayet A = 16, 38, 29, 31, 20, 11, 9
(indeks fra 1, A.heap-size = 7). Oppgi arrayet etterpå.
Start: 16, 38, 29, 31, 20, 11, 9 med . Gyldig maks-haug: nei — men
prebetingelsen er oppfylt, for begge deltrærne under rota er i orden. BareA[1] bryter.
| Steg | Kall | Sammenligning | Array etter steget | Handling |
|---|---|---|---|---|
| 1 | Max-Heapify(A, 1) | A[1]=16 mot venstre barn A[2]=38, mot høyre barn A[3]=29 | 38, 16, 29, 31, 20, 11, 9 | bytt A[1] og A[2] |
| 2 | Max-Heapify(A, 2) | A[2]=16 mot venstre barn A[4]=31, mot høyre barn A[5]=20 | 38, 31, 29, 16, 20, 11, 9 | bytt A[2] og A[4] |
| 3 | Max-Heapify(A, 4) | indeks 4 har ingen barn (2*4 = 8 > 7) | 38, 31, 29, 16, 20, 11, 9 | stopp |
På eksamen leverer du bare linja under — tavlen er her for å vise hvordan du
kommer dit.
38, 31, 29, 16, 20, 11, 9Steg 1 er hele poenget. Elementet 16 har barna 38 og 29. Hadde vi byttet
med høyre barn 29, ville arrayet blitt 29, 38, 16, 31, 20, 11, 9, og der
er — altså et nytt brudd i rota. Max-Heapify bytter alltid med
det største barnet, nettopp for å unngå det.
Steg 3 er sjekken oppgaven egentlig tester. Etter det andre byttet står 16
på indeks 4. Venstre barn ville vært indeks , men haugen har
bare sju elementer. Indeks 8 finnes ikke, og rutinen stopper. Uten
grensesjekken ville algoritmen lest utenfor arrayet.
Kontroll som tar ti sekunder: haugen hadde sju elementer og har fortsatt
sju, alle de opprinnelige tallene er med, ingen er duplisert, og hver forelder
er nå større enn eller lik begge barna.
Utfør Max-Heapify(A, 1) på arrayet A = 10, 33, 27, 12, 25, 6, 21 (indeks
fra 1, A.heap-size = 7).
Oppgi arrayet etterpå. Oppgaven ber om output, ikke om en forklaring av
algoritmen.
De to første koster hele oppgaven, og begge sitter i sivingen.
- Å sive mot feil barn. Max-Heapify går mot det største barnet, ikke
mot venstre barn og ikke mot det minste. Bytter du med det minste, lager du
et nytt brudd rett under deg. Kontrollen: gikk du noen gang forbi et barn
som var større enn det du byttet med?
- Å forveksle haugegenskapen med søketreegenskapen. Dette er felle #2 —
å blande de to strukturene. En maks-haug ordner opp–ned: forelder
begge barn, og ingenting er sagt om venstre mot høyre. Et binært søketre
ordner venstre–høyre. I en haug kan venstre barn godt være større enn
høyre barn, og arrayet er ikke sortert.
- Å reparere en ugyldig haug før du kjører operasjonen. Dette er felle
#10 — å «hjelpe» algoritmen i gang. Ber oppgaven deg kjøre
Heap-Extract-Max på et array som ikke er en gyldig maks-haug, skal du
utføre trinnene mekanisk, akkurat slik pseudokoden sier. Du får da et annet
svar enn om du hadde reparert først, og det er det svaret oppgaven vil ha.
- Å påstå at Build-Max-Heap er . Dette er felle #9 —
å oppgi feil kjøretidsfakta. Den er , og du får se tellingen i
neste avsnitt. Det er selve Heapsort som er , fordi de
uthentingene etterpå koster hver.
- Å bruke indeksformler fra en annen indekseringsvane. Denne boka bruker
, og med rot på indeks 1. Kontroll: rota skal
ikke ha noen forelder, og A[1] skal være det største elementet i en gyldig
maks-haug.
- Å levere treet i stedet for arrayet. Svarformatet for en
haug-håndkjøring er hele arrayet, kommaseparert, med indeks fra 1. En
pent tegnet trefigur uten arrayet under gir ikke full uttelling.
Og den gjennomgående: å bruke uten å si hva det er. Skriv i samme
setning at er antall elementer i haugen.
Build-Max-Heap, og hvorfor den er lineær (~14 min)
Nå har vi en rutine som reparerer ett brudd. Spørsmålet er hvordan man gjør et
helt vilkårlig array om til en maks-haug.
Svaret er å kalle Max-Heapify nedenfra og opp. Retningen er
nødvendig: Max-Heapify(A, i) forutsetter at deltrærne under allerede er
i orden, og den forutsetningen er bare oppfylt hvis du har behandlet alle
indekser større enn først.
Og du kan hoppe over den nederste halvparten helt. Alle indekser har , altså ingen barn. En node uten barn er per
definisjon allerede en gyldig maks-haug med ett element. Derfor starter løkka
på og teller nedover til 1.
A[1..n], indeks fra 1, medvilkårlig innhold.
A.length er .Prebetingelse: ingen — arrayet kan være i hvilken som helst tilstand.
Postbetingelse: hele A[1..n] oppfyller haugegenskapen, og inneholder
nøyaktig de samme elementene som før. Omorganiseringen skjer på stedet,
altså uten et hjelpearray.
Build-Max-Heap(A)
Input: vilkaarlig array A[1..n]
Output: A omorganisert til en maks-haug, paa stedet
A.heap-size = A.length
for i = floor(A.length / 2) downto 1
Max-Heapify(A, i)
Kjoeretid: Theta(n)Invarianten i én setning: ved starten av hver runde er hver node med
indeks større enn rota i en gyldig maks-haug — så når løkka når ,
er hele arrayet en maks-haug.
Hvorfor løkka teller nedover: Max-Heapify(A, i) krever at deltrærne
under allerede er i orden. Teller du oppover, er den forutsetningen brutt
fra første kall, og resultatet blir en haug som ser riktig ut på toppen og er
ødelagt lenger nede.
Kjøretid: . Den løse tellingen « kall à » gir
, og den er riktig som øvre grense — men den er ikke stram. Den
stramme analysen står i neste avsnitt, og forskjellen er et fast eksamenspoeng.
En vaktliste registrerer ti hastegrader i den rekkefølgen pasientene meldte
seg: A = 4, 19, 7, 23, 11, 30, 6, 15, 28, 9 (indeks fra 1).
Kjør Build-Max-Heap(A) og oppgi arrayet etterpå.
Start: 4, 19, 7, 23, 11, 30, 6, 15, 28, 9 med . Gyldig maks-haug:
nei.
Løkka starter på og teller ned til 1.
Indeksene 6 til 10 er blader og trenger ingen behandling.
| Steg | Kall | Sammenligning | Array etter steget | Handling |
|---|---|---|---|---|
| 1 | Max-Heapify(A, 5) | A[5]=11 mot venstre barn A[10]=9 | 4, 19, 7, 23, 11, 30, 6, 15, 28, 9 | ingen bytte, stopp |
| 2 | Max-Heapify(A, 4) | A[4]=23 mot venstre barn A[8]=15, mot høyre barn A[9]=28 | 4, 19, 7, 28, 11, 30, 6, 15, 23, 9 | bytt A[4] og A[9] |
| 3 | Max-Heapify(A, 9) | indeks 9 har ingen barn (2*9 = 18 > 10) | 4, 19, 7, 28, 11, 30, 6, 15, 23, 9 | stopp |
| 4 | Max-Heapify(A, 3) | A[3]=7 mot venstre barn A[6]=30, mot høyre barn A[7]=6 | 4, 19, 30, 28, 11, 7, 6, 15, 23, 9 | bytt A[3] og A[6] |
| 5 | Max-Heapify(A, 6) | indeks 6 har ingen barn (2*6 = 12 > 10) | 4, 19, 30, 28, 11, 7, 6, 15, 23, 9 | stopp |
| 6 | Max-Heapify(A, 2) | A[2]=19 mot venstre barn A[4]=28, mot høyre barn A[5]=11 | 4, 28, 30, 19, 11, 7, 6, 15, 23, 9 | bytt A[2] og A[4] |
| 7 | Max-Heapify(A, 4) | A[4]=19 mot venstre barn A[8]=15, mot høyre barn A[9]=23 | 4, 28, 30, 23, 11, 7, 6, 15, 19, 9 | bytt A[4] og A[9] |
| 8 | Max-Heapify(A, 9) | indeks 9 har ingen barn (2*9 = 18 > 10) | 4, 28, 30, 23, 11, 7, 6, 15, 19, 9 | stopp |
| 9 | Max-Heapify(A, 1) | A[1]=4 mot venstre barn A[2]=28, mot høyre barn A[3]=30 | 30, 28, 4, 23, 11, 7, 6, 15, 19, 9 | bytt A[1] og A[3] |
| 10 | Max-Heapify(A, 3) | A[3]=4 mot venstre barn A[6]=7, mot høyre barn A[7]=6 | 30, 28, 7, 23, 11, 4, 6, 15, 19, 9 | bytt A[3] og A[6] |
| 11 | Max-Heapify(A, 6) | indeks 6 har ingen barn (2*6 = 12 > 10) | 30, 28, 7, 23, 11, 4, 6, 15, 19, 9 | stopp |
På eksamen leverer du bare linja under — tavlen er her for å vise hvordan du
kommer dit.
30, 28, 7, 23, 11, 4, 6, 15, 19, 9Tegnet som tre:
30
(1)
28 7
(2) (3)
23 11 4 6
(4) (5) (6) (7)
15 19 9
(8) (9) (10)
Tell byttene. Elleve rader i tavlen, men bare fem av dem er faktiskebytter: steg 2, 4, 6, 7, 9 og 10 — seks bytter. Med er . Seks er ikke i nærheten, og det er ingen tilfeldighet: se neste
avsnitt.
Legg merke til at det første kallet ikke gjorde noe.
Max-Heapify(A, 5)fant at og stoppet umiddelbart. Det er typisk: nodene nederst har
lite arbeid å gjøre, og det er dem det er flest av.
Kjør Build-Max-Heap(A) på arrayet A = 7, 3, 22, 14, 9, 30, 5, 12 (indeks
fra 1).
a) Hvilken indeks starter løkka på, og hvorfor?
b) Oppgi arrayet etter at hele Build-Max-Heap er utført.
tellingen.
Steg 1 — hvor mange noder har en gitt høyde? I et tre som fylles nivå for
nivå har høyst noder høyde , der høyden til en
node er antall kanter ned til det dypeste bladet under den. Bladene har
høyde 0, foreldrene deres høyde 1, og så videre.
Intuisjon: omtrent halvparten av alle noder er blader, en firedel ligger ett
nivå over, en åttedel to nivåer over. Antallet halveres for hvert nivå du går
oppover.
Steg 2 — hvor mye arbeid gjør Max-Heapify fra en node med høyde ?
Høyst bytter, for elementet kan ikke synke lenger enn til bunnen av sitt
eget deltre. Kostnaden er altså .
Intuisjon: arbeidet vokser med én per nivå oppover, mens antallet noder
halveres. Halvering slår en økning på én.
Steg 3 — gang sammen og summer:
Intuisjon: summen konvergerer mot 2 — den blir ikke større
uansett hvor mange ledd du tar med. Derfor er totalarbeidet under , ikke
.
Tellingen for tre konkrete størrelser (regnet ut med formelen over):
| høyde | sum av arbeidet | sum | |||
|---|---|---|---|---|---|
| 15 | 3 | 11 | 30 | 58 | 0,73 |
| 31 | 4 | 26 | 62 | 153 | 0,84 |
| 63 | 5 | 57 | 126 | 376 | 0,91 |
| 127 | 6 | 120 | 254 | 887 | 0,95 |
| 1 000 | 9 | 998 | 2 000 | 9 965 | 1,00 |
| 1 000 000 | 19 | 1 000 069 | 2 000 000 | 19 931 568 | 1,00 |
Forholdet sum kryper mot 1 og stopper der. Det gjør ikke , som vokser uten grense. Det er hele forskjellen.
Og nedre grense: hver av de elementene må i det minste leses, så
arbeidet er også . Øvre og nedre grense møtes, og derfor skriver vi
— ikke bare .
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 felle
#9.
Build-Max-Heap på et array med elementer?Oppgi det strammeste riktige uttrykket.
b) Den løse analysen sier: « kall til Max-Heapify, hver
, altså ». Forklar med to setninger hvorfor denne
grensen er riktig, men ikke stram.
c) Hva er kjøretiden til Heapsort, og hvorfor er den ikke den samme som
til Build-Max-Heap?
Heap-Extract-Max og prioritetskøen (~9 min)
— naturlig pausepunkt —
Tilbake til legevakta. Nå skal den mest alvorlige pasienten inn, og haugen
skal fortsatt være en haug etterpå.
Å lese ut det største er gratis: det ligger på A[1]. Problemet er hullet som
oppstår. Løsningen er å fylle hullet med det siste elementet i haugen,
krympe A.heap-size med én, og så la det nye rotelementet sive ned medMax-Heapify. Da er prebetingelsen oppfylt: begge deltrærne er urørt og
dermed fortsatt gyldige, og bare rota kan være for liten.
En struktur som støtter «gi meg det viktigste» og «legg til et nytt element»
kalles en prioritetskø. Haugen er standardimplementasjonen, og du møter
den igjen som motor i Dijkstra og i MST-Prim senere i boka — der er det
nettopp «hvilken node er nærmest nå?» som skal besvares millioner av ganger.
A[1..n], indeks fra 1, ogA[1..A.heap-size] er en gyldig maks-haug.Prebetingelse: A.heap-size >= 1.
Postbetingelse: det største elementet er returnert og fjernet fra haugen,A.heap-size er redusert med én, og resten er fortsatt en gyldig maks-haug.
Heap-Extract-Max(A)
Input: maks-haug A[1..A.heap-size]
Output: det storste elementet, fjernet fra haugen
if A.heap-size < 1
feil: haugen er tom
maks = A[1]
A[1] = A[A.heap-size]
A.heap-size = A.heap-size - 1
Max-Heapify(A, 1)
return maks
Kjoeretid: O(lg n)Grunnideen i én setning: ved å fylle hullet i rota med det siste
haugelementet holder treet seg fylt nivå for nivå, og siden bare rota kan ha
blitt for liten, er nøyaktig ett Max-Heapify-kall nok.
Kjøretid: . Alt bortsett fra Max-Heapify er konstant arbeid,
og Max-Heapify følger én sti fra rota og ned, altså høyst nivåer.
Merk rekkefølgen. A.heap-size reduseres før Max-Heapify kalles.
Gjør du det motsatt, sammenligner reparasjonen med et element som ikke lenger
er en del av haugen — og da får du et annet svar. Ved håndkjøring er det
denne detaljen som skiller riktig fra nesten riktig.
Utfør Heap-Extract-Max(A) én gang på arrayet A = 5, 21, 13, 4, 18
(indeks fra 1, A.heap-size = 5). Arrayet er ikke en gyldig maks-haug.
Følg algoritmen mekanisk. Oppgi hvilket element som returneres, og haugen
etterpå.
Start: 5, 21, 13, 4, 18 med . Gyldig maks-haug: nei — er
mindre enn begge barna.
Vi reparerer ikke først. Algoritmen leser A[1], uansett hva som står der.
| Steg | Kall | Sammenligning | Array etter steget | Handling |
|---|---|---|---|---|
| 1 | Heap-Extract-Max | les A[1] = 5 | 5, 21, 13, 4, 18 | ta vare på maks = 5 |
| 2 | Heap-Extract-Max | flytt siste element til rota, heap-size = 4 | 18, 21, 13, 4 | krymp haugen |
| 3 | Max-Heapify(A, 1) | A[1]=18 mot venstre barn A[2]=21, mot høyre barn A[3]=13 | 21, 18, 13, 4 | bytt A[1] og A[2] |
| 4 | Max-Heapify(A, 2) | A[2]=18 mot venstre barn A[4]=4 | 21, 18, 13, 4 | ingen bytte, stopp |
På eksamen leverer du bare de to linjene under — tavlen er her for å vise
hvordan du kommer dit.
Returnert:
5Haugen etterpå (
A.heap-size = 4): 21, 18, 13, 4Det som skjer i steg 1 er hele poenget. Algoritmen returnerer 5, selv om
21 er det største tallet i arrayet. Heap-Extract-Max er skrevet under
forutsetningen at rota ER maksimum; er den ikke det, gir algoritmen feil
element. Det er ikke en feil i håndkjøringen din — det er svaret.
Fellenoten: fellen her er #10 — å «reparere» en ugyldig haug før du
utfører operasjonen. Hadde du først kjørt Build-Max-Heap og deretter
uthentingen, ville du returnert 21 og fått en helt annen slutthaug. Det er et
annet spørsmål enn det som ble stilt.
Kontroll: fem elementer inn, ett returnert, fire igjen i haugen. Etter
uthentingen er 21, 18, 13, 4 en gyldig maks-haug — reparasjonen virket, selv
om utgangspunktet var ugyldig.
Utfør Heap-Extract-Max(A) én gang på arrayet A = 9, 40, 17, 6, 35 (indeks
fra 1, A.heap-size = 5). Arrayet er ikke en gyldig maks-haug — følg
algoritmen mekanisk.
a) Hvilket element returneres?
b) Oppgi haugen etterpå.
Heapsort (~8 min)
Har du en maks-haug, har du nesten en sortering. Det største elementet ligger
på A[1], og den siste plassen i arrayet er nøyaktig der det skal ende opp.
Bytt de to, krymp haugen med én, reparer rota — og gjenta.
Etter runder ligger arrayet stigende sortert, og du har ikke brukt en
eneste ekstra celle. Det er det som menes med på stedet (in-place): all
omorganisering skjer inne i det arrayet du fikk, med bare et par
hjelpevariabler ved siden av. Merge-Sort klarer ikke det, og det erHeapsorts viktigste fortrinn.
Prisen er at sorteringen ikke er stabil. En stabil sortering beholder den
innbyrdes rekkefølgen til elementer med lik nøkkel — sorterer du en vaktliste
etter hastegrad, kommer de som meldte seg først fortsatt først blant de like
alvorlige. Heapsort gir ingen slik garanti, fordi byttene flytter elementer
over lange avstander i arrayet.
A[1..n], indeks fra 1, medvilkårlig innhold.
Prebetingelse: ingen.
Postbetingelse: A[1..n] er stigende sortert, og inneholder de samme
elementene som før. Ingen hjelpearray er brukt.
Heapsort(A)
Input: vilkaarlig array A[1..n]
Output: A stigende sortert, paa stedet
Build-Max-Heap(A)
for i = A.length downto 2
bytt A[1] og A[i]
A.heap-size = A.heap-size - 1
Max-Heapify(A, 1)
Kjoeretid: Theta(n lg n)Invarianten i én setning: ved starten av hver runde er A[i+1..n] de
største elementene, ferdig sortert på plass, og A[1..i] er en gyldig
maks-haug med resten.
Kjøretid: . Byggingen er , og deretter gjøres
runder à . At grensen er tett og ikke bare øvre, kommer av
at sorteringen gjør like mye arbeid uansett input — det finnes ingen «heldig»
rekkefølge som gjør Heapsort raskere, slik ferdigsortert input gjørInsertion-Sort rask.
Egenskapene du skal kunne uten å tenke: , på stedet,
ustabil.
Kjør Heapsort(A) på A = 12, 26, 5, 31, 18, 8 (indeks fra 1).
a) Oppgi arrayet rett etter Build-Max-Heap.
b) Oppgi arrayet etter den første iterasjonen av for-løkka, med
haugstørrelsen.
c) Oppgi det ferdig sorterte arrayet.
Start: 12, 26, 5, 31, 18, 8 med .
Steg 1 til 7 nedenfor er byggefasen; fra steg 8 begynner sorteringsfasen.
| Steg | Kall | Sammenligning | Array etter steget | Handling |
|---|---|---|---|---|
| 1 | Max-Heapify(A, 3) | A[3]=5 mot venstre barn A[6]=8 | 12, 26, 8, 31, 18, 5 | bytt A[3] og A[6] |
| 2 | Max-Heapify(A, 6) | indeks 6 har ingen barn (2*6 = 12 > 6) | 12, 26, 8, 31, 18, 5 | stopp |
| 3 | Max-Heapify(A, 2) | A[2]=26 mot venstre barn A[4]=31, mot høyre barn A[5]=18 | 12, 31, 8, 26, 18, 5 | bytt A[2] og A[4] |
| 4 | Max-Heapify(A, 4) | indeks 4 har ingen barn (2*4 = 8 > 6) | 12, 31, 8, 26, 18, 5 | stopp |
| 5 | Max-Heapify(A, 1) | A[1]=12 mot venstre barn A[2]=31, mot høyre barn A[3]=8 | 31, 12, 8, 26, 18, 5 | bytt A[1] og A[2] |
| 6 | Max-Heapify(A, 2) | A[2]=12 mot venstre barn A[4]=26, mot høyre barn A[5]=18 | 31, 26, 8, 12, 18, 5 | bytt A[2] og A[4] |
| 7 | Max-Heapify(A, 4) | indeks 4 har ingen barn (2*4 = 8 > 6) | 31, 26, 8, 12, 18, 5 | stopp |
| 8 | Heapsort-iterasjon 1 | bytt A[1] = 31 og A[6] = 5 | 5, 26, 8, 12, 18, 31 | største element på plass bakerst |
| 9 | Max-Heapify(A, 1) | A[1]=5 mot venstre barn A[2]=26, mot høyre barn A[3]=8 | 26, 5, 8, 12, 18, 31 | bytt A[1] og A[2] |
| 10 | Max-Heapify(A, 2) | A[2]=5 mot venstre barn A[4]=12, mot høyre barn A[5]=18 | 26, 18, 8, 12, 5, 31 | bytt A[2] og A[5] |
| 11 | Max-Heapify(A, 5) | indeks 5 har ingen barn (2*5 = 10 > 5) | 26, 18, 8, 12, 5, 31 | stopp (heap-size = 5) |
| 12 | Heapsort-iterasjon 2 | bytt A[1] = 26 og A[5] = 5 | 5, 18, 8, 12, 26, 31 | største element på plass bakerst |
| 13 | Max-Heapify(A, 1) | A[1]=5 mot venstre barn A[2]=18, mot høyre barn A[3]=8 | 18, 5, 8, 12, 26, 31 | bytt A[1] og A[2] |
| 14 | Max-Heapify(A, 2) | A[2]=5 mot venstre barn A[4]=12 | 18, 12, 8, 5, 26, 31 | bytt A[2] og A[4] |
| 15 | Max-Heapify(A, 4) | indeks 4 har ingen barn (2*4 = 8 > 4) | 18, 12, 8, 5, 26, 31 | stopp (heap-size = 4) |
| 16 | Heapsort-iterasjon 3 | bytt A[1] = 18 og A[4] = 5 | 5, 12, 8, 18, 26, 31 | største element på plass bakerst |
| 17 | Max-Heapify(A, 1) | A[1]=5 mot venstre barn A[2]=12, mot høyre barn A[3]=8 | 12, 5, 8, 18, 26, 31 | bytt A[1] og A[2] |
| 18 | Max-Heapify(A, 2) | indeks 2 har ingen barn (2*2 = 4 > 3) | 12, 5, 8, 18, 26, 31 | stopp (heap-size = 3) |
| 19 | Heapsort-iterasjon 4 | bytt A[1] = 12 og A[3] = 8 | 8, 5, 12, 18, 26, 31 | største element på plass bakerst |
| 20 | Max-Heapify(A, 1) | A[1]=8 mot venstre barn A[2]=5 | 8, 5, 12, 18, 26, 31 | ingen bytte, stopp (heap-size = 2) |
| 21 | Heapsort-iterasjon 5 | bytt A[1] = 8 og A[2] = 5 | 5, 8, 12, 18, 26, 31 | største element på plass bakerst |
| 22 | Max-Heapify(A, 1) | indeks 1 har ingen barn (2*1 = 2 > 1) | 5, 8, 12, 18, 26, 31 | stopp (heap-size = 1) |
På eksamen leverer du bare de tre linjene under — tavlen er her for å vise
hvordan du kommer dit.
a) Etter
Build-Max-Heap: 31, 26, 8, 12, 18, 5b) Etter første iterasjon (
heap-size = 5): 26, 18, 8, 12, 5, 31c) Ferdig sortert:
5, 8, 12, 18, 26, 31Se hvordan halen vokser. Etter iterasjon 1 er
A[6] = 31 ferdig. Etteriterasjon 2 er
A[5..6] = 26, 31 ferdig. Den sorterte halen bakerst blir énlengre for hver runde, og haugen foran den blir én kortere. Ingen ekstra
plass er brukt — derfor «på stedet».
Merk hva delspørsmålene ber om. b) spør bare om én iterasjon. Kjører
du hele sorteringen når bare første iterasjon var spurt, har du svart på et
annet spørsmål — og brukt tid du trengte på de andre oppgavene.
Arrayet A = 45, 28, 39, 14, 21, 33, 8 (indeks fra 1) er allerede en gyldig
maks-haug med A.heap-size = 7.
Utfør én iterasjon av for-løkka i Heapsort. Oppgi hele arrayet etterpå,
og hva A.heap-size er da. Oppgaven ber om én iterasjon, ikke hele
sorteringen.
Arrayet A = 6, 17, 3, 25, 11, 20, 14, 2 (indeks fra 1) skal sorteres medHeapsort.
a) Oppgi arrayet rett etter Build-Max-Heap.
b) Oppgi arrayet og A.heap-size etter de to første iterasjonene av
for-løkka.
En medstudent påstår: «Heapsort er en stabil sortering, siden den bare
bytter elementer og aldri kopierer dem til et hjelpearray.»
a) Stemmer påstanden? Svar ja eller nei.
b) Gi et konkret motbevis eller et konkret argument, og oppgi hva som blir
resultatet.
Arrayet A = 3a, 3b, 1c har tre elementer der tallet er nøkkelen som
sorteres på, og bokstaven bare er en merkelapp som viser hvilket element det
er. 3a står altså foran 3b i inputen.
Kjøretidene samlet
Dette er kapitlets puggeflate. Eksamen er hjelpemiddelfri, så tabellen må
ligge i hodet.
| Operasjon | Beste | Verste | Krav / egenskap |
|---|---|---|---|
Parent, Left, Right | ren indeksregning, ingen leting | ||
Heap-Maximum | maksimum ligger alltid på A[1] | ||
Max-Heapify | krever at begge deltrærne allerede er maks-hauger | ||
Build-Max-Heap | på stedet; ikke | ||
Heap-Extract-Max | krever A.heap-size | ||
Max-Heap-Insert | nytt element legges bakerst og stiger | ||
Heapsort | på stedet, ustabil; ingen input gjør den raskere | ||
| Finne minimum i en maks-haug | haugen sier ingenting om hvor det minste ligger |
Den siste raden overrasker mange. En maks-haug lover bare at maksimum ligger
på toppen. Det minste elementet kan ligge hvor som helst blant bladene, og du
må lete gjennom dem alle.
Én presisering som er verdt å ta med seg:
Build-Max-Heap er asymptotiskraskere enn å bygge haugen med kall til
Max-Heap-Insert, som gir. Men på en enkelt liten input kan de to gjøre like mange
bytter, og innsettingsvarianten kan til og med gjøre færre. Det er derfor
påstanden hører hjemme som en asymptotisk påstand, ikke som «
Build-Max-Heapgjør alltid færre bytter».
Begrepsbank
Begrepsbanken er flashcard- og repetisjonsstoff — den gjentar det du nettopp
har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet
gjelder kjernestoffet.
reparerer ett brudd på haugegenskapen ved å la elementet på indeks sive
nedover mot det største barnet så lenge et barn er større.
Kjøretid — én sti fra og ned, aldri forgrening.
Krever at begge deltrærne under allerede er gyldige maks-hauger. Er
den betingelsen brutt, reparerer rutinen ikke hele treet.
gjør et vilkårlig array om til en maks-haug på stedet ved å kalleMax-Heapify for hver indeks fra og nedover til 1.
Kjøretid — ikke . Den løse tellingen «
kall à » er en gyldig øvre grense, men ikke stram: halvparten av
nodene er blader med null arbeid.
Retningen er et krav. Løkka må telle nedover, ellers er Max-Heapifys
prebetingelse brutt.
fjerner og returnerer det største elementet: leser A[1], flytter det siste
haugelementet dit, reduserer A.heap-size med én og kaller Max-Heapify(A, 1).
Kjøretid — alt bortsett fra reparasjonen er konstant arbeid.
Krever A.heap-size . Og merk: rutinen kontrollerer ikke at rota
faktisk er maksimum. Er inputen ikke en gyldig haug, returnerer den likevelA[1].
leser det største elementet uten å fjerne det.
Kjøretid , fordi maksimum per definisjon ligger på A[1] i en
gyldig maks-haug.
Det er denne operasjonen som gjør haugen til en prioritetskø: «hva er det
viktigste akkurat nå?» besvares uten å lete.
setter inn et nytt element ved å legge det bakerst i haugen og la det stige
oppover mot rota så lenge forelderen er mindre.
Kjøretid — én sti fra bladet og opp.
Retningen er motsatt av Max-Heapify: her siles det oppover, og
sammenligningen går bare mot forelderen, ikke mot søsken.
sorterer stigende ved å bygge en maks-haug og deretter bytte A[1] med
siste haugelement, krympe haugen og reparere rota — ganger.
Kjøretid i alle tilfeller. På stedet, og ustabil.
At grensen er tett og ikke bare øvre, kommer av at ingen input gjørHeapsort raskere: den bygger og tømmer haugen likt uansett.
kravet som gjør et array til en maks-haug: hver forelder er større enn eller
lik begge barna sine, altså for alle .
Ordningen er opp–ned. Det finnes ingen venstre–høyre-orden: to søsken
kan stå i hvilken som helst rekkefølge.
Konsekvensen er at bare maksimum har en garantert plass. Resten av arrayet er
tilnærmet usortert.
haugen lagres som et vanlig array, fylt nivå for nivå ovenfra og ned og
venstre før høyre. Rota ligger på A[1].
Kostnaden for å finne familien til en node er : ingen pekere, bare
regning.
Indeks fra 1 er konvensjonen i denne boka, og den som brukes i TDT4120s
oppgaver og løsningsforslag.
tre regnestykker som erstatter pekere: forelderen ligger på , venstre barn på og høyre barn på .
Alle tre er .
Kanttilfellene: indeks 1 har ingen forelder. Er , er noden et
blad. Er men , har noden bare venstre barn.
antall elementer som teller som en del av haugen akkurat nå — ikke det samme
som arrayets lengde.
Skillet koster ingenting å vedlikeholde ( per endring), men det er
avgjørende ved håndkjøring.
Heap-Extract-Max og Heapsort krymper haugen uten å krympe arrayet.
Elementene bak A.heap-size ligger fortsatt der, men er usynlige forMax-Heapify.
antall kanter fra rota ned til det dypeste bladet.
For en haug med elementer er høyden , altså
.
Grunnen er at treet alltid fylles nivå for nivå uten hull. Det er denne
høyden som gjør Max-Heapify, Heap-Extract-Max og Max-Heap-Insert til
-operasjoner.
en datastruktur som støtter «gi meg det viktigste elementet» og «legg til et
nytt element», uten å holde alt sortert.
Med en maks-haug koster oppslaget og de to endringene
hver.
Du møter den igjen som motor i Dijkstra og MST-Prim i Del 4, der
spørsmålet «hvilken node er nærmest nå?» stilles om og om igjen.
en algoritme er på stedet når den omorganiserer inputarrayet direkte og bare
bruker et konstant antall hjelpevariabler ved siden av.
Ekstra plassbruk: .
Heapsort og Build-Max-Heap er på stedet. Merge-Sort er det ikke — den
trenger et hjelpearray på celler.
en sortering er stabil når to elementer med lik nøkkel beholder den
innbyrdes rekkefølgen de hadde i inputen.
Heapsort er ustabil. Motbevis: 3a, 3b, 1c sorteres til 1c, 3b, 3a,
mens en stabil sortering ville gitt 1c, 3a, 3b.
Grunnen er at byttene flytter elementer over lange avstander i arrayet — i
første runde hopper rota helt bakerst, forbi alt som ligger imellom.
regelen som styrer hvilken vei Max-Heapify går: elementet bytter med det
største av de to barna, ikke med venstre barn og ikke med det minste.
Det koster én ekstra sammenligning per nivå, altså fortsatt .
Bytter du med det minste barnet, står du igjen med et nytt brudd rett under
deg — feilen er flyttet, ikke rettet.
alle indekser har og dermed ingen barn. En
node uten barn er allerede en gyldig maks-haug med ett element.
Å hoppe over dem sparer omtrent halvparten av kallene, men endrer ikke
kjøretiden: den er uansett.
Det er derimot en fast kilde til feil i håndkjøring å starte på eller på
1 i stedet.
nodene uten barn, altså alle indekser med . De utgjør omtrent
halvparten av haugen.
Antallet er .
De er nøkkelen til -analysen: halvparten av nodene gjør null
arbeid under Build-Max-Heap, og en firedel gjør høyst ett bytte.
to strukturer som forveksles systematisk, og som ordner i hver sin retning.
Begge har som naturlig arbeidsmengde per operasjon, men de
svarer på ulike spørsmål.
Maks-haugen ordner opp–ned (forelder begge barn) og gir deg
maksimum i . Søketreet ordner venstre–høyre og lar deg søke
etter en bestemt nøkkel. En haug kan ikke søke effektivt; et søketre kan ikke
love deg maksimum uten å gå til høyre spiss.
når en oppgave ber deg utføre en haugoperasjon, følger du pseudokoden linje
for linje — også når inputen ikke er en gyldig maks-haug.
Kostnaden er den samme som ellers; det er svaret som blir et annet.
Reparerer du først, svarer du på et annet spørsmål enn det som ble stilt.
Dette er felle #10 i bokas feilregister.
svaret på en haugoppgave er hele arrayet, kommaseparert, med indeks fra
1 — og A.heap-size når haugen er krympet.
Det tar ett sekund å skrive og gir full uttelling.
Et pent tegnet tre uten arrayet under gjør det ikke, og heller ikke «bare de
elementene som ble flyttet».
oppgavetypen der du utfører en navngitt algoritme steg for steg på papir og
oppgir sluttilstanden.
Den er den hyppigste håndkjøringssjangeren i faget, og hauger og søketrær er
de to vanligste strukturene: temaet er registrert i 16 av de 17 settene i
grunnlaget.
Svarformen er kun sluttilstanden, i det formatet oppgaven ber om. En
forklaring av hvordan algoritmen virker, gir ingen ekstra uttelling.
oppgavetypen der du oppgir kjøretiden til en navngitt operasjon.
Svarformen er ett uttrykk, i det strammeste som er riktig — og med
der grensen er tett og der bare øvre grense er vist.
Her ligger felle #9: å skrive for Build-Max-Heap, som er
.
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 Norges teknisk-naturvitenskapelige universitet. Dette er ikke offisielt studiemateriell. Les mer.