CMD + K
CMD + K
Splitt og hersk
Splitt og hersk løser et problem ved å dele det i mindre delinstanser, løse hver rekursivt, og sette delsvarene sammen. Merge-Sort og Quicksort er arketypene. Kjøretiden fanges av en rekurrens, som vi løser med substitusjon, rekursjonstrær eller masterteoremet.
- 01Beskrive de tre stegene i splitt og hersk og forklare hva basistilfellet gjør i en rekursiv algoritme
- 02Sette opp rekurrensen for Merge-Sort og bruke et rekursjonstre til å vise at den løser seg til Θ(n lg n)
- 03Anvende masterteoremet på en rekurrens T(n)=aT(n/b)+f(n) ved å sammenligne f(n) med n^{log_b a} og velge riktig tilfelle
- 04Forklare hvorfor Quicksort er Θ(n²) i verste fall men forventet Θ(n lg n), og hvordan tilfeldig pivot unngår verste tilfelle
Del, løs, sett sammen
Tenk deg at du skal telle alle bøkene i et stort bibliotek alene. Det tar evigheter. Men har du femten venner, deler dere lokalet i seksten soner, hver teller sin sone, og til slutt legger dere sammen de seksten tallene. Den siste sammenleggingen er rask; selve tellingen skjedde parallelt og på en brøkdel av tiden. splitt og hersk bygger på akkurat denne ideen, bare at «vennene» er rekursive kall til samme algoritme.
Mønsteret har tre faste steg. Først deler vi problemet i mindre delinstanser av samme type. Så løser vi hver delinstans rekursivt — og fordi de er av samme type, kan vi bruke akkurat den samme algoritmen på dem. Til slutt kombinerer vi delsvarene til et svar på det opprinnelige problemet. Rekursjonen kan ikke fortsette i det uendelige; et basistilfelle er det minste tilfellet vi løser direkte, uten videre splitting. For sortering er en liste med ett element allerede sortert, og det stopper rekursjonen.
Merge-Sort steg for steg
Den reneste illustrasjonen er merge-sort. Den deler arrayet i to like halvdeler, sorterer hver halvdel rekursivt, og fletter de to sorterte halvdelene sammen til ett sortert array. Hele kunsten ligger i flettesteget: når begge halvdeler er sorterte, kan vi flette dem ved å gjentatte ganger plukke det minste av de to fremste elementene.
MERGE-SORT(A, lo, hi):
if hi - lo <= 1: # 0 eller 1 element: ferdig sortert
return
mid = (lo + hi) // 2
MERGE-SORT(A, lo, mid) # venstre halvdel
MERGE-SORT(A, mid, hi) # høyre halvdel
MERGE(A, lo, mid, hi) # flett de to sorterte halvdeleneFlettingen er lineær: vi går gjennom begge halvdelene én gang, sammenligner de to fremste elementene, og kopierer det minste over i et resultatarray. For to sorterte rekker [2, 5, 8] og [1, 3, 6] plukker vi 1, så 2, så 3, så 5, så 6, så 8 — seks sammenligninger, ett gjennomløp. Med elementer totalt på et nivå koster flettingen arbeid.
Det at Merge-Sort alltid deler nøyaktig i to, uansett hvordan dataene ser ut, er grunnen til at den er like rask i beste som i verste tilfelle. Den bryr seg ikke om at inndataene tilfeldigvis er nesten sorterte eller helt baklengs.
Kjøretid som en rekurrens
For å regne ut kjøretiden trenger vi et språk for «tiden for uttrykt ved tiden for mindre tilfeller». Det språket er en rekurrens: en ligning der defineres ved av mindre argumenter. For Merge-Sort er den enkel å lese rett ut av algoritmen — to rekursive kall på halve størrelsen, pluss lineært flettearbeid:
ƒmerge-sort-rekurrensen
Leddet er de to rekursive kallene, og er flettingen. Spørsmålet er hva denne rekurrensen løser seg til. Det enkleste er å tegne det opp.
Rekursjonstreet
Et rekursjonstre gjør rekurrensen synlig. Rota er det opprinnelige kallet med kostnad for flettingen. Den har to barn, hvert med størrelse og flettekostnad . To barn à gir til sammen — like mye som rota. Slik fortsetter det: på hvert nivå dobles antall noder, men hver node er halvparten så stor, så summen per nivå holder seg konstant lik .
Nå er svaret nesten gitt. Hvert nivå koster . Hvor mange nivåer er det? Vi halverer størrelsen helt ned til 1, og antall halveringer fra til er . Med rota teller vi nivåer, hver til :
$$
Dette er kjernen i hvorfor Merge-Sort er : konstant arbeid per nivå, logaritmisk mange nivåer.
Substitusjonsmetoden: gjett og bevis
Rekursjonstreet gir intuisjon, men ikke et stramt bevis. Til det bruker vi substitusjonsmetoden: vi gjetter en grense og beviser den med induksjon. Anta vi tror for en passende konstant . Induksjonssteget setter den antatte grensen inn for de mindre tilfellene på høyresiden:
$$
Så lenge er , og hele uttrykket er . Induksjonen går opp, og gjettet er bekreftet. Substitusjon krever altså at du har en kandidat på forhånd — derfor er rekursjonstreet en så nyttig forløper: det produserer gjettet du senere beviser.
Masterteoremet: en kokebok
De fleste splitt-og-hersk-rekurrenser har samme form: delinstanser av størrelse pluss kombinasjonskostnad . masterteoremet er en oppskrift som leser av svaret direkte fra disse tre tallene, uten at du tegner tre eller kjører induksjon:
ƒmasterteoremet
Hele avgjørelsen står og faller på en sammenligning mellom kombinasjonsarbeidet og uttrykket . Det siste tallet teller hvor mye arbeid løvene i rekursjonstreet utgjør til sammen. Tre utfall er mulige.
I tilfelle 1 vokser polynomielt saktere enn løvene. Da dominerer bladene, og svaret styres av dem:
ƒmasterteoremet, tilfelle 1
I tilfelle 2 er kombinasjon og blader i balanse — like mye arbeid på hvert nivå, akkurat som i Merge-Sort. Hvert av de nivåene bidrar likt, og vi får en ekstra logaritmisk faktor:
ƒmasterteoremet, tilfelle 2
For Merge-Sort er , og . Da er , som matcher — tilfelle 2 treffer, og svaret er . Samme svar som rekursjonstreet ga, på sekunder.
I tilfelle 3 vokser kombinasjonsarbeidet polynomielt raskere enn løvene, og (med en mild regularitetsbetingelse) dominerer rota:
ƒmasterteoremet, tilfelle 3
Masterteoremet dekker ikke alle rekurrenser — det krever fast forhold mellom og , og noen rekurrenser faller i sprekkene mellom tilfellene. Da må du tilbake til rekursjonstre eller substitusjon. Men for det store flertallet av splitt-og-hersk-algoritmer er det den raskeste veien til svaret.
Quicksort: rask i snitt, treg i verste fall
quicksort er den andre store splitt-og-hersk-sorteringen, og den jobber annerledes enn Merge-Sort. Den velger et pivot-element og partisjonerer arrayet: alt mindre enn pivoten havner til venstre, alt større til høyre. Så sorterer den de to sidene rekursivt. Det fine er at all jobbingen skjer i partisjoneringen, før de rekursive kallene — det finnes ikke noe eget flettesteg, og sorteringen skjer på stedet.
QUICKSORT(A, lo, hi):
if hi - lo <= 1:
return
p = PARTITION(A, lo, hi) # plasser ett element riktig, del rundt det
QUICKSORT(A, lo, p) # venstre del (mindre enn pivot)
QUICKSORT(A, p + 1, hi) # høyre del (større enn pivot)Kjøretiden avhenger helt av hvor balanserte partisjonene blir. Velger vi alltid et pivot som lander midt i, deler vi i to like halvdeler hver gang, og rekurrensen blir den samme som Merge-Sort — . Men velger vi et uheldig pivot som lar den ene siden være tom, går det galt:
ƒquicksort, verste tilfelle
Da blir rekursjonsdybden i stedet for , og kostnadene danner en aritmetisk rekke , som summerer til . Et eksempel: sorterer du et allerede sortert array og velger alltid det første elementet som pivot, treffer du nettopp dette verste tilfellet hver gang.
Løsningen er enkel og elegant: velg pivot tilfeldig. Da blir det praktisk talt umulig for inndataene å fremtvinge skjeve splitter gjentatte ganger, fordi rekkefølgen i arrayet ikke lenger styrer pivotvalget. Forventet kjøretid blir like god som Merge-Sort:
ƒquicksort, forventet kjøretid
I praksis er Quicksort ofte raskere enn Merge-Sort til tross for samme asymptotiske kjøretid, fordi den sorterer på stedet og har små konstantfaktorer. Det er en god påminnelse om at ikke er ett enkelt tall — to algoritmer med samme asymptotiske klasse kan likevel ha merkbart ulik fart på ekte data.
Mine notater
Skriv egne notater for dette kapittelet. De lagres automatisk og dukker opp i «Mine notater»-oversikten paa fag-siden.