8. Lajittelu, joukot ja valinta Yksi tietojenkäsittelyn klassisista tehtävistä on lajittelu (järjestäminen) (sorting) jo mekaanisten tietojenkäsittelylaitteiden ajalta. Lajiteltua tietoa tarvitaan lukemattomissa yhteyksissä, esim. puhelinluetteloista binäärihakualgoritmiin (luku 6.2.), joka toimii oikein ainoastaan järjestetylle sekvenssille. Lajitteluongelma määritellään seuraavasti. Olkoon S n alkion sekvenssi, jossa alkiot ovat vertailukelpoisia keskenään täydellisen järjestyksen mielessä, ts. aina on mahdollista verrata kahta alkiota sen suhteen, kumpi niistä on pienempi ja kumpi suurempi. Lajitellaan ne kasvavaan järjestykseen tai ei vähenevään, jos sekvenssissä on yhtäsuuria alkioita. 8. luku 394
Edellä on esitetty yksinkertaiset ( naivit ) lajittelualgoritmit, kupla, lisäys ja valintalajittelu, jotka toimivat ajassa O(n 2 ) alkioiden määrään n nähden. Luvussa 5 käsiteltiin prioriteettijonoon perustuvaa lajittelua, joka toimi sekvenssitoteutuksella ajassa O(n 2 ), mutta kekolajitteluna ajassa O(n log n). Tässä luvussa esitetään tehokkaat lomituslajittelu ja pikalajittelu sekä tarkastellaan lyhyesti lokerolajittelua. Lisäksi esitetään tietorakenne joukko ja lajittelusta tavallaan johdettu valinta. Oletetaan, että kahden alkion välinen vertailu on aina tehtävissä ajassa O(1). 8.1. Lomituslajittelu Esitetään lomituslajittelu (merge sort), joka on luontevaa toteuttaa rekursiivisena. 8. luku 395
Lomituslajittelu perustuu yleiseen algoritmien suunnittelumenetelmään nimeltä hajota ja hallitse (divide and conquer). Tämä käsittää kolme vaihetta: 1. Hajota: Jos syötteen koko on pienempi kuin ennalta määrätty kynnysarvo, kuten yksi tai kaksi alkiota, ongelma ratkaistaan (triviaalina) suoraviivaisella menetelmällä ja palautetaan saatu ratkaisu. Muuten jaetaan syöte kahteen tai useampaan erilliseen osajoukkoon. 2. Rekursio: Ratkaistaan rekursiivisesti osaongelmat osajoukkoihin liitttyen. 3. Hallitse: Osaongelmien ratkaisut lomitetaan alkuperäisen ongelman ratkaisuun. 8. luku 396
Edeltävä menettely oli esitetty yleisessä muodossa. Esitetään se nyt erityisesti lajittelua varten, missä on sekvenssi S ja n alkiota: 1. Hajota: Jos sekvenssissä S on ainakin kaksi alkiota (mitään ei tarvitse tehdä, jos on yksi tai ei yhtään), poistetaan kaikki alkiot sekvenssistä S ja sijoitetaan ne sekvensseihin S 1 ja S 2, joista kumpikin sisältää noin puolet S:n alkioista, ts. S 1 käsittää ensimmäiset n/2 alkiota ja S 2 loput n/2 alkiota. 2. Rekursio: Lajitellaan rekursiivisesti sekvenssit S 1 ja S 2. 3. Hallitse: Sijoitetaan alkiot takaisin sekvenssiin S lomittamalla lajitellut sekvenssit S 1 ja S 2 yhdeksi lajitelluksi sekvenssiksi. 8. luku 397
Lomituslajittelun suoritusta on hyvä havainnollistaa lomituslajittelupuun (merge sort tree) avulla (kuva 8.1.). Siinä jokainen solmu edustaa lomituslajittelun rekursiivista kutsua. Solmuun v liittyy sekvenssi S ja solmun lapsiin (rekursiiviset kutsut) osasekvenssit S 1 ja S 2. Puun lehdet liittyvät yksittäisiin alkioihin vastaten algoritmin tilanteita, joissa ei enää tehdä rekursiivisia kutsuja. Visualisointi auttaa algoritmin analyysin ymmärtämisessä. Kun syöte jaetaan suurin piirtein kahtia jokaisen rekursiivisen kutsun yhteydessä, puun korkeus on luokkaa log n (2 kantainen logaritmi). Lause 8.1. Lomituslajittelun suoritusta kuvaavan puun korkeus on syötejoukon kooltaan n tilanteessa log n. Perustelu voisi olla harjoituksena. Lausetta hyödynnetään lomituslajittelun suoritusajan laskemisessa. 8. luku 398
85 24 63 45 17 31 96 50 17 31 96 50 85 24 63 45 (a) (b) Kuva 8.1. (alku) Lomituslajittelun havainnollistaminen puuna, jossa solmut vastaavat rekursiivista kutsua. Pisteviivalla merkityt solmut tarkoittavat kutsuja, joita ei ole vielä tehty. 8. luku 399
17 31 96 50 17 31 96 50 63 45 63 45 85 24 24 85 (c) (d) Kuva 8.1. (jatkoa) Tyhjät solmut ohuella viivalla reunustettuna tarkoittavat tehtyjä kutsuja. Paksulla yhtenäisellä viivalla reunustettu solmu on nykyinen kutsu. 8. luku 400
17 31 96 50 17 31 96 50 63 45 63 45 85 24 85 24 (e) (f) Kuva 8.1. (jatkoa) Loput solmut eli ne, jotka on reunustettu ohuella yhtenäisellä viivalla ja eivät ole tyhjiä, edustavat kutsuja odottamassa lapsisolmun rekursion palautusta. 8. luku 401
17 31 96 50 17 31 96 50 63 45 63 45 85 24 24 85 (g) (h) Kuva 8.1. (jatkoa) Tilanne (h) on hallitse vaihe. 8. luku 402
17 31 96 50 17 31 96 50 24 85 63 45 24 85 63 45 (i) (j) Kuva 8.1. (jatkoa) 8. luku 403
17 31 96 50 17 31 96 50 24 85 24 85 45 63 45 63 (k) (l) Kuva 8.1. (jatkoa) 8. luku 404
17 31 96 50 17 31 96 50 24 85 24 85 63 63 45 45 (m) (n) Kuva 8.1. (jatkoa) 8. luku 405
17 31 96 50 17 31 96 50 24 85 24 85 45 63 45 63 (o) (p) Kuva 8.1. (jatkoa) Tilanne (o) on hallitse vaihe. 8. luku 406
17 31 96 50 24 45 63 85 17 31 96 50 24 45 63 85 (q) (r) Kuva 8.1. (jatkoa) Tilanne (q) on hallitse vaihe. 8. luku 407
24 45 63 85 24 45 63 85 17 31 96 50 17 31 50 96 (s) (t) Kuva 8.1. (jatkoa) Tilanteiden (s) ja (t) välistä on jätetty useita muita esittämättä. Tilanne (t) on hallitse vaihe. 8. luku 408
24 45 63 85 17 31 50 96 17 24 31 45 50 63 85 96 (u) (v) Kuva 8.1. (loppu) Tilanne (v) on hallitse vaihe. 8. luku 409
Mietitään nyt tämän hajota ja hallitse algoritmin vaiheita yksityiskohtaisesti. Lomituslajittelun hajota ja rekursiovaiheet ovat yksinkertaisia. Sekvenssin kooltaan n hajottaminen jakaa sen asteesta n/2 1 lukien kahdeksi osasekvenssiksi. Rekursiivinen kutsu käsittää vain näiden sekvenssien välityksen parametreina. Laskennallisesti eniten vaativa on hallitse vaihe, joka lomittaa lajitellut osasekvenssit yhdeksi lajitelluksi. Koodina 8.1. esitetty algoritmi merge (lomittaa) poistaa iteratiivisesti pienemmän alkion osasekvensseistä ja lisää sen tulossekvenssin loppuun, kunnes osasekvenssit tyhjenevät. Kuvassa 8.2. on esimerkki lomituksesta. 8. luku 410
Algorithm merge(s 1, S 2, S): Input: Sekvenssit S 1 ja S 2 (alkioiden välillä määritelty täydellinen järjestys) lajiteltuna ei vähenevässä järjestyksessä ja tyhjä sekvenssi S. Output: Sekvenssi S, joka sisältää sekvenssien S 1 ja S 2 alkioiden unionin lajiteltuna ei vähenevään järjestykseen. Sekvenssit S 1 ja S 2 tyhjenevät suorituksen aikana. while S 1 ei ole tyhjä and S 2 ei ole tyhjä do if S 1.first().element() S 2.first().element() then {siirrä S 1 :n ensimmäinen alkio sekvenssin S loppuun} S.insertLast(S 1.remove(S 1.first())) else {siirrä S 2 :n ensimmäinen alkio sekvenssin S loppuun} S.insertLast(S 2.remove(S 2.first())) Koodi 8.1. (alku) Lomitusalgoritmi lomittaa kaksi lajiteltua sekvenssiä yhdeksi lajitelluksi sekvenssiksi. 8. luku 411
{siirrä S 1 :n loput alkiot sekvenssin S loppuun} while S 1 ei ole tyhjä do S.insertLast(S 1.remove(S 1.first())) {siirrä S 2 :n loput alkiot sekvenssin S loppuun} while S 2 ei ole tyhjä do S.insertLast(S 2.remove(S 2.first())) Koodi 8.1. (loppu) Lomitusalgoritmi lomittaa kaksi lajiteltua sekvenssiä yhdeksi lajitelluksi sekvenssiksi. 8. luku 412
S 1 24 45 63 85 S 1 24 45 63 85 S 2 17 31 50 96 S 2 31 50 96 (a) S 17 (b) S 1 45 63 85 S 1 45 63 85 S 2 31 50 96 S 2 50 96 S 17 24 S 17 24 31 (c) (d) Kuva 8.2. (alku) Esimerkki koodin 8.1. lomitusalgoritmin suorituksesta. 8. luku 413
S 1 63 85 S 1 63 85 S 2 50 96 S 2 96 S 17 24 31 45 S 17 24 31 45 50 (e) (f) S 1 85 S 1 S 2 96 S 2 96 S 17 24 31 45 50 63 S 17 24 31 45 50 63 85 (g) (h) S 1 S 2 Kuva 8.2. (loppu) Esimerkki lomitusalgoritmin suorituksesta. S 17 24 31 45 50 63 85 96 (i) 8. luku 414
Tehdyt sekvenssien lisäykset ja poistot toimivat ajassa O(1) (kaksoislinkitetty lista tai rengasrakenne luvusta 4). Kun osasekvenssien alkioiden määrät ovat n 1 ja n 2, niin lomitusalgoritmin kolmen while silmukan iteraatioiden määrä on yhteensä n 1 + n 2, mistä tulee suoritusajaksi O(n 1 +n 2 ). Ilman yksityiskohtaista lomituslajittelun analyysia lopputulos on ymmärrettävissä seuraavasti. Hajota vaiheen suoritus on lineaarisessa suhteessa sekvenssin kokoon. Yllä osoitettiin samoin olevan hallitsevaiheen lomituksen. Kun i viittaa solmun v syvyyteen, solmussa käytetty aika on O(n/2 i ), koska solmuun v liittyvän rekursiivisen kutsun sekvenssin koko on n/2 i. Lomituslajittelupuuta edeltä tarkasteltaessa kokonaisuutena havaitaan siinä syvyydellä i olevan 2 i solmua ko. tasolla. Tästä saadaan kaikissa solmuissa tasolla i käytettävä kokonaisaika O(2 i n/2 i ), joka on O(n). Lauseen 8.1. mukaan puun korkeus on log n. Kun puun jokaisella log n + 1 tasolla käytetty aika on O(n), saadaan seuraava lause. 8. luku 415
Lause 8.2. Lomituslajittelualgoritmi lajittelee sekvenssin kooltaan n alkiota ajassa O(n log n). Lomituslajittelu toimii asymptoottisessa mielessä samassa ajassa kuin kekolajittelu. 8.2. Joukot Joukko (set) tarkoittaa vastaavaa matemaattista käsitettä, jolle määritellään myös vastaavat operaatiot, kuten leikkaus ja unioni. Joukko operaatiot eivät edellytä täydellisen järjestyksen relaatiota alkioiden välillä, mutta järjestysinformaatiota voidaan silti hyödyntää joukon toteutuksessa. 8. luku 416
Joukon abstrakti tietotyyppi ja yksinkertainen toteutus Joukko operaatiot ovat osittain samanlaisia kuin sanakirjan (luku 7). Perusjoukko operaatiot ovat unioni (union), leikkaus (intersection) ja erotus (subtraction), jotka määritellään oheisina joukkojen A ja B avulla: A B = {x: x A tai x B}, A B = {x: x A ja x B} ja A B = {x: x A ja x B}. Joukon A metodit ovat seuraavat: size(): Palauttaa joukon A alkioiden määrän. Tulos: kokonaisluku 8. luku 417
isempty(): Tulos: totuusarvo Palauttaa totuusarvon sen mukaan, onko A tyhjä. insertelement(e): Lisää alkion e joukkoon A, jollei se ollut siellä ennestään. Syöte: alkio elements(): Palauttaa alkioiden luettelon joukosta A. Tulos: alkioiden luettelo ismember(e): Määrää, onko e joukossa A. Syöte: alkio Tulos: totuusarvo union(b): Palauttaa unionin A B. Syöte: joukko Tulos: joukko 8. luku 418
intersect(b): Palauttaa leikkauksen A B. Syöte: joukko Tulos: joukko isequal(b): Palauttaa arvon tosi, jos ja vain jos on A = B. Syöte: joukko Tulos: totuusarvo Operaatiot unioni, leikkaus ja erotus määriteltiin niin, että ne ovat eituhoavia (nondestructive), ts. ne eivät muuta mainittujen joukkojen sisältöä (tulos talletetaan johonkin kolmanteen joukkoon). Tuhoavina ne voidaan yhtä hyvin määritellä, jolloin tulos asetetaan toiseen niistä ja toinen merkitään tyhjäksi. Eräs yksinkertaisimmista keinoista toteuttaa joukot on tallettaa niiden alkiot järjestettyyn sekvenssiin. Vaikka alkioiden välillä ei olisi täydellistä järjestystä, usein voidaan jonkinlainen järjestysrelaatio määritellä (jos ei muuta ole, niin voidaan käyttää ainakin alkioiden talletusosoitteita muistissa). Metodit size(), isempty() ja elements() saadaan suoraan sekvenssin avulla. 8. luku 419
Metodi insertelement(e) voidaan toteuttaa myös melko suoraviivaisesti sanakirjan avulla. (Poisto on esitettävissä sekvenssin avulla niin ikään, vaikka sitä ei tässä ole eritelty.) Joukko operaatiot ja geneerinen lomitus Joukko operaatiot unioni, leikkaus ja erotus toteutetaan geneerisen lomituksen avulla, joka saa syötteenään kaksi lajiteltua sekvenssiä (syötejouko) ja rakentaa niistä tulosjoukkoa edustavan sekvenssin. Sekvenssien soveltama järjestys voi olla mikä tahansa yhtenäinen järjestys (siis täydellinen järjestys). Geneerinen lomitusalgoritmi on esitetty koodina 8.2. Koodin 8.2. algoritmi tutkii iteratiivisesti ja vertaa sekvenssien A ja B nykyisiä alkioita a ja b, onko a < b, a = b vai a > b. Vertailun perusteella se kopioi toisen tai ei kumpaakaan tulossekvenssiin C. 8. luku 420
Algorithm genericmerge(a,b): Input: Lajitellut sekvenssit A ja B. Output: Lajiteltu sekvenssi C. {Sekvenssejä A ja B ei hävitetä.} Olkoon A sekvenssin A kopio. Olkoon B sekvenssin B kopio. while A ja B eivät ole tyhjiä do a A.first() b B.first() if a < b then firstisless(a,c) A.removeFirst() Koodi 8.2. (alku) Geneerinen lomitusalgoritmi parametrisoituna metodeilla firstisless, bothareequal ja firstisgreater. 8. luku 421
else if a = b then bothareequal(a,b,c) A.removeFirst() B.removeFirst() else firstisgreater(b,c) B.removeFirst() while A ei ole tyhjä do a A.first() firstisless(a,c) A.removeFirst() while B ei ole tyhjä do b B.first() firstisless(b,c) B.removeFirst() Koodi 8.2. (loppu) Geneerinen lomitusalgoritmi. 8. luku 422
Vertailun perusteella tehtävä toiminta riippuu suoritettavasta operaatiosta, unioni, leikkaus tai erotus. Jos on kysymyksessä esimerkiksi unioni, kopioidaan pienempi alkioista a ja b ja jos ne ovat yhtä suuria, kopioidaan toinen (kumpi vain tässä tapauksessa). Täten kopioidaan jokainen eri alkio, mutta ei luoda kaksoiskappaleita (duplikaatteja). Koodissa 8.2. mainitut metodit firstisless, bothareequal ja firstisgreater määriteltäisiin (ei esitetä tässä) sopivasti riippuen kulloinkin suoritettavasta operaatiosta. Leikkauksessa kopioidaan luonnollisesti ne alkiot, jotka esiintyvät molemmissa joukoissa A ja B. Erotuksessa kopioidaaan puolestaan ne, jotka ovat toisessa, mutta eivät toisessa joukossa. Alkioiden vertailut vievät aikaa O(1). Geneerisen lomituksen kokonaissuoritusaika on näin ollen lineaarinen O(n A + n B ), missä n A ja n B viittavaat vastaavien joukkojen kokoihin. Taulukossa 8.1. ovat lueteltuina eri operaatioiden suoritusajat. 8. luku 423
Taulukko 8.1. Järjestettyinä sekvensseinä toteutettujen joukkojen operaatioiden suoritusajat. Lukumäärä n viittaa käsittelyyn liittyvän joukon tai joukkojen (yhteis)kokoon. metodi suoritusaika size, isempty O(1) insertelement O(n) elements, ismember O(n) union, intersect, subtract O(n) isequal O(n) 8. luku 424
8.3. Pikalajittelu Esiteltävä lajittelumenetelmä on nimeltään pikalajittelu (quick sort), joka lomituslajittelun tapaan perustuu hajota ja hallitse menettelyyn. Se on jonkin verran erilainen, sillä laskenta suoritetaan pääosin ennen rekursiivisia kutsuja. Pikalajittelun korkean tason kuvaus Pikalajittelualgoritmi lajittelee sekvenssin S rekursiivisesti. S jaetaan osiin, jotka ovat toisiinsa nähden erillisiä, ja nämä lajitellut osasekvenssit yhdistetään. Algoritmi pitää sisällään kolme vaihetta: 8. luku 425
1. Hajota: Jos sekvenssissä S on vähintään kaksi alkiota (mitään ei tarvitse tehdä, jos on tätä vähemmän), valitaan erityisalkio x sekvenssistä S. Se on pivotalkio. Olkoon x esim. viimeinen alkio. Poistetaan kaikki alkiot sekvenssistä S ja asetetaan ne kolmeen sekvenssiin: L: ne alkiot, jotka ovat pienempiä kuin x E: ne alkiot, jotka ovat yhtä suuria kuin x G: ne alkiot, jotka ovat suurempia kuin x Jos sekvenssin S alkiot ovat erisuuria, E sisältää luonnollisesti ainoastaan pivotalkion itsensä. 2. Rekursio: Lajitellaan rekursiivisesti sekvenssit L ja G. 3. Hallitse: Asetetaan alkiot takaisin sekvenssiin S järjestyksessä ensin sekvenssin L alkiot, sitten sekvenssin E alkiot ja lopuksi sekvenssin G alkiot. 8. luku 426
Kuten lomituslajittelussa, pikalajittelussakin suoritusta voidaan kuvata binäärisellä rekursiopuulla, kuvan 8.3. pikalajittelupuuna. Lomituslajittelusta eroten pikalajittelupuun korkeus on pahimmassa tapauksessa lineaarinen toki keskimääräisessä (käytännössä merkittävässä) logaritminen. Pahin tapaus esiintyy mm. n eri alkion ollessa valmiiksi järjestyksessä. Tällöin pivot on suurin alkio, jolloin sekvenssin L koko on n 1, kun taas sekvenssin E koko on yksi ja G nolla. Tämä tilanne toistuu joka kutsulla sekvenssin koon vähetessä yhdellä. Niinpä tämän erityisen tapauksen puun korkeus on n 1. 8. luku 427
85 24 63 45 17 31 96 50 24 45 17 31 50 85 63 96 (a) Kuva 8.3. (alku) Pikalajittelun esimerkki, missä solmut edustavat rekursiivisia kutsuja. Pisteviivalla piirretyt solmut tarkoittavat kutsuja, joita ei vielä ole tehty. Vaihe (b) on jako. (b) 8. luku 428
50 85 63 96 50 85 63 96 24 45 17 31 24 17 31 45 (c) Kuva 8.3. (jatkoa) Paksulla reunaviivalla esitetty solmu edustaa suoritettavaa kutsua. Ohuella yhtenäisellä reunaviivalla esitetyt tyhjät solmut (ei vielä tässä) edustavat päätettyjä kutsuja. Muunlaiset solmut, kuin mainitut tyypit, edustavat odottavia. Vaihe (d) on jako. (d) 8. luku 429
50 85 63 96 50 85 63 96 31 45 31 45 24 17 17 24 (e) (f) Kuva 8.3. (jatkoa) Vaihe (f) on jako. 8. luku 430
50 85 63 96 50 85 63 96 31 45 31 45 17 24 17 24 (g) (h) Kuva 8.3. (jatkoa) 8. luku 431
50 85 63 96 50 85 63 96 31 45 31 45 17 17 24 24 (i) (j) Kuva 8.3. (jatkoa) 8. luku 432
50 85 63 96 50 85 63 96 31 45 17 24 31 45 17 24 (k) (l) Kuva 8.3. (jatkoa) Osa (k) on hallitse vaihe. 8. luku 433
50 85 63 96 50 85 63 96 17 24 31 17 24 31 45 45 (m) (n) Kuva 8.3. (jatkoa) 8. luku 434
50 85 63 96 17 24 31 45 50 85 63 96 17 24 31 45 (o) (p) Kuva 8.3. (jatkoa) Osa (o) on hallitse vaihe. Huomaa, että vaiheen (p) jälkeen useita on jätetty esityksestä pois ennen vaihetta (q). 8. luku 435
17 24 31 45 50 63 85 96 17 24 31 45 50 63 85 96 (q) (r) Kuva 8.3. (jatkoa) Osa (r) on hallitse vaihe. 8. luku 436
Pikalajittelu paikallaan Toistaiseksi ei ole tarkasteltu lajittelumenetelmän tarvitsemaa muistitilaa itse lajiteltavan aineiston lisäksi. Lajittelualgoritmi toimii paikallaan (in place), jos se käyttää pelkästään vakiomäärän muistitilaa lajiteltavan aineiston lisäksi. Tällainen lajittelumenetelmä on varsin tehokas tilankäytön kannalta. Kuvattu lomituslajittelu ei ole po. tyyppiä, ja sen muuttaminen paikallaan toimivaksi on monimutkainen tehtävä. Yleisesti ottaen tämän ei tarvitse olla mutkikasta. Esim. luvun 5 kekolajittelun tapauksessa muunnos käy helposti, kun sovelletaan taulukkona toteutettua sekvenssiä kekoa varten. Myös pikalajittelu on toteutettavissa toimivaksi paikallaan. 8. luku 437
Käytetään itse sekvenssiä sisältämään osasekvenssit. Syötesekvenssin sisältämiä alkioita vaihdetaan keskenään, joten erillisiä osasekvenssejä ei tarvitse luoda. Toimitaan kahden indeksin, l (vasen) ja r (oikea) avulla. Jakovaihe tehdään selaamalla näiden rajoittamaa osasekvenssiä samanaikaisesti sekä indeksistä l eteenpäin että indeksistä r taaksepäin. Alkioita vaihdetaan pareittain, jos ne ovat väärässä järjestyksessä (kuva 8.4.) Indeksien kohdatessa osasekvenssit L ja G ovat vastakkain tämän kohtauspaikan eri puolilla. Algoritmi käsittelee sitten rekursiivisesti nämä osasekvenssit. Algoritmin paikallaan käsittely vähentää sekä suoritusaikaa (aikayksiköissä mitattuna) että muistitilan tarvetta. (Teknisesti ajatellen tämä ei ole puhtaasti paikallaan toimiva, koska rekursion hallinta käyttää ylimääräistä tilaa, mutta algoritmi on toteutettavissa iteratiivisesti niin, että tätäkään ei tapahdu, kun pinoa ei käytetä. Tätä erityiskysymystä ei pohdita tässä.) 8. luku 438
85 24 63 45 17 31 96 50 l (a) r 85 24 63 45 17 31 96 50 l (b) r 31 24 63 45 17 85 96 50 l (c) r Kuva 8.4. (alku) Jakovaihe pikalajittelussa paikallaan. Indeksi l selaa sekvenssiä vasemmalta oikealle ja indeksi r oikealta vasemmalle. Vaihto tehdään, kun l on suuremmassa alkiossa kuin pivot ja r on pienemmässä kuin pivot. Lopullinen vaihto pivotin kanssa päättää jakovaiheen. 8. luku 439
31 24 63 45 17 85 96 50 l (d) r 31 24 17 45 63 85 96 50 l (e) r Kuva 8.4. (jatkoa) 8. luku 440
31 24 17 45 63 85 96 50 r (f) l 31 24 17 45 50 85 96 63 r (g) l Kuva 8.4. (loppu) 8. luku 441
Kuten tämän luvun 8.3 alkupuolella mainittiin, pikalajittelun suoritusaika on pahimmassa tapauksessa O(n 2 ), missä n on lajiteltavien lukumäärä, ja se esiintyy mm. aineiston ollessa jo alkutilanteessa kasvavassa järjestyksessä. Paradoksaalista kyllä, pikalajittelu toimii täten huonosti tilanteessa, jossa lajittelun pitäisi olla helppoa. Samasta syystä johtuen myös aineiston ollessa melkein lajiteltua (sisältää järjestyksessä olevia osajonoja ja periaatteessa vain muutamilla vaihdoilla saataisiin järjestykseen) pikalajittelu toimii heikosti. Tilastollisesti mieltäen kaikista mahdollisista satunnaisista lajiteltavien alkutilanteista em. tapaukset ovat harvinaisia. Näin ollen keskimääräinen suoritusaika on pikalajittelun kannalta huomattavasti tärkeämpi seikka. Keskimääräisen tapauksen (odotusarvon) analyysiin tarkemmin kajoamatta todetaan sen muistuttavan lomituslajittelun analyysia ja algoritmin olevan tehokas, jälleen O(n log n). Tämä saadaan aikaan, kun jakovaiheissa osasekvenssit L ja G ovat kutakuinkin yhtä suuria. Pivotalkio voidaan valita satunnaisesti (tasainen jakauma), joka tekee tästä satunnaistetusta pikalajittelusta hyvin keskimääräisessä suoritusajassa pysyvän. 8. luku 442
8.4. Vertailuperusteisen lajittelun alaraja Edellä nähtiin, miten tehokkaimmat esitetyistä lajittelumenetelmistä tuottivat suoritusajaksi pahimman tapauksen tai odotusarvon mukaisesti O(n log n). Tällöin herää luonnollinen kysymys, onko näille olemassa ylipäänsä (jollekin menetelmälle) mainittua parempaa tulosta. Seuraavaksi esitetään yhteenvedonomaisesti tulema, että mainittu kompleksisuus liittää tähän myös alarajan, ts. parempaa tulosta ei voida saavuttaa millään vertailuihin perustuvalla menetelmällä pahimman tapauksen tilanteessa. Tarkastelun perustaminen vain vertailujen laskentaan riittää, koska suoritettavien kahden alkion välisten vertailujen määrä vaikuttaa olennaisesti koko suoritusaikaan (esim. vertailujen tuloksena seuraavat vaihdot ovat suoraan vertailun tuloksesta riippuva, vakioajassa tehtävä toiminta). Näin voidaan pohtia hypoteettista menetelmää. 8. luku 443
Sivuutetaaan analyysin yksityiskohdat ja mielletään perättäisten vertailujen suorittaminen kulkemiseksi puussa T, jonka juuresta lähdetään liikkeelle, tehdään vertailuja ja päädytään lopulta lehteen. Jokainen lehti edustaa lajiteltavan aineiston S yhtä permutaatiota (permutation), kun oletetaan alkioiden olevan erillisiä (ei siis duplikaatteja). Puu T voidaan ymmärtää päätöspuuna, jossa vertailut tehdään. Saadaan seuraava lause. Lause 8.3. Minkä tahansa vertailuperusteisen lajittelualgoritmin pahimman tapauksen suoritusajan alaraja on Ω(n log n). Perustelu: Vertailuperusteisen algoritmin suoritusajan täytyy olla suhteessa päätöspuun T korkeuteen. Jokainen lajiteltavien alkioiden aineiston S permutaatio liittyy vain yhteen lehteen. Puussa T on ainakin n! = n(n 1)(n 2) 2 1 lehteä (yhtä kuin erilaisten permutaatioiden määrä), josta saadaan puun korkeudeksi vähintään log (n!). 8. luku 444
Kertomassa on ainakin n/2 tekijää, jotka ovat suurempia tai yhtä suuria kuin n/2. Tästä saadaan alaraja arvio: log (n!) log (n/2) n/2 = (n/2) log n/2, joka on Ω(n log n). Johtopäätöksenä on, että vertailuperusteista lajittelua ei voi tehdä pahimmalle tapaukselle yo. alarajaa nopeammin. Taas pulpahtaa mieleen kysymys, onko mahdollista tehdä muunlaisia lajittelualgoritmeja, jotka voivat asymptoottisessa mielessä kuitenkin toimia nopeammin kuin O(n log n). 8. luku 445
8.5. Lokerolajittelu On todella mahdollista saada asymptoottisesti nopeampia suoritusaikoja kuin O(n log n) rajoittamalla lajiteltavan sekvenssin alkioiden tyyppiä. Tällainen lähtökohta on täysin relevantti, koska vastaavia esiintyy monesti käytännön lajittelutilanteissa. Rajoitetaan lajiteltavien tietoyksiköiden (avain alkio pareja) avainten arvot kokonaisluvuiksi välille [0, N 1], missä N 2. Rajoitusta hyväksi käyttäen vertailujen tai niitä korvaavien toimintojen määrää avainten välillä voidaan merkittävästi supistaa. Perusmenetelmä on tällöin lokerolajittelu (bucket sort), joka ei perustu vertailuihin, vaan avainten soveltamiseen lokerotaulukon B indekseinä. Taulukon alkiot ovat indekseiltään välillä [0,N 1]. Tietoyksikkö avaimeltaan k sijoitetaan lokeroon B[k], joka on tämän avainarvon sekvenssi. 8. luku 446
Kun kaikki avaimet on asetettu lokeroidensa sekvensseihin, ne siirretään takaisin sekvenssiin S lajiteltuun järjestyksen lokeroitten sisältöjen mukaan B[0], B[1],, B[N 1]. Algoritmi esitetään koodina 8.3. Algorithm bucketsort(s): Input: Sekvenssi S, jossa on tietoyksiköt kokonaislukuavaimineen väliltä [0,N 1]. Output: Sekvenssi S lajiteltuna avainten ei vähenevään järjestykseen. Olkoon BNsekvenssin taulukko, jonka lokerot ovat aluksi tyhjiä. for jokaiselle sekvenssin S tietoyksikölle x do Olkoon k tietoyksikön x avain. Poistetaan x sekvenssistä S ja lisätään se sekvenssin B[k] loppuun. for i 0 to N 1 do for jokaiselle sekvenssin B[i] tietoyksikölle x do Poistetaan x lokerosta B[i] ja lisätään se sekvenssin S loppuun. Koodi 8.3. Lokerolajittelu. 8. luku 447
Suoraviivaisesti on todettavissa lokerolajitelun toimivan ajassa O(n + N) ja samoin tilassa O(n + N), missä n on lajiteltavien ja N lokeroiden määrä. Kun avainarvojen väli N on pieni verrattuna lajiteltavien määrään n, kuten N = O(n) tai N = O(n log n), niin lokerolajittelu on hyvin tehokas. Tehokkuus heikkenee koon N kasvaessa suhteessa lukumäärään n. Lajiteltaessa avain alkio pareja tärkeä kysymys esiintyy siinä, miten käsitellään sama avaimisia tietoyksiköitä, koska näiden alkiot kuitenkin tyypillisesti eroavat toisistaan. Jos mitkä tahansa tietoyksiköt (k i, e i ) ja (k j, e j ), jotka ovat tässä järjestyksessä ennen lajittelua, ovat aina lajittelun jälkeen samassa järjestyksessä, sanotaan algoritmin olevan stabiili (stable). Tätä ominaisuutta hyödyntäen lokerolajittelua voidaan muokata kokonaislukuja yleisempään kontekstiin, jolloin on kysymyksessä kantalukulajittelu (radix sort); tätä ei kuitenkaan tarkastella tässä kurssissa. 8. luku 448
8.6. Valinta On olemassa monenlaisia sovelluksia, joissa pitää hakea yksittäinen alkio alkiojoukosta. Tällaisia tilanteita ovat minimin ja maksimin haku, mutta myös muiden, erityisesti mediaanin (median) hakua tarvitaan usein (mediaani on alkio, jota pienempiä ovat puolet muista alkioista ja loput suurempia). Yleisesti esitetään kysymys järjestyksessä k:nnen alkion valinnasta (selection). Yksioikoinen lähestymistapa olisi lajitella aineisto ja sitten valita k:nneksi suurin. Tämä olisi tarkemmin ajatellen hyvin tehotonta, sillä nähdäänhän välittömästi, että esim. alkiot suuruusjärjestyksessä k=1, k=2 tai k=n 1 ovat saatavissa ajassa O(n). Täten herää mielenkiintoinen kysymys, onko mahdollista suorittaa valinta ajassa O(n) yleisestikin mille tahansa k:lle ja mediaanille, k= n/2, erityisesti. 8. luku 449
Valintaongelma on suoritettavissa ajassa O(n) mille tahansa k:lle. Tämä saadaan aikaan satunnaistetulla pikavalinnalla (randomized quickselect), joka toimii järjestämättömälle n alkion sekvenssille, kun täydellisen järjestyksen ominaisuus on voimassa kaikkien alkioparien välillä. Tämän menetelmän suoritusajan odotusarvo (keskimääräinen) on lineaarinen. Pahin tapaus on kaikesta huolimatta jälleen O(n 2 ). Aluksi on järjestämätön sekvenssi S, jossa on n alkiota, ja haettava avain k väliltä [1,n]. Pikavalinta algoritmi on ylätasolla nimensä mukaisesti pikalajittelun kanssa samankaltainen. Otetaan satunnaisesti alkio x sekvenssistä ja käytetään sitä pivotalkiona sekvenssin jakamiseksi osiin L, G ja E, joissa alkiot ovat pienempiä, suurempia tai yhtä suuria kuin pivotalkio. Avaimesta k riippuen päätetään, mistä noista kolmesta jatketaan rekursiota (koodi 8.4.). 8. luku 450
Algorithm quickselect(s,k): Input: Sekvenssi S, jossa on n alkiota, ja avain k väliltä [1,n]. Output: k:nneksi pienin alkio sekvenssistä S. if n = 1 then return sekvenssin S (ensimmäinen) alkio Otetaan satunnainen kokonaisluku r väliltä [0,n 1] Olkoon x sekvenssin S alkio astetta r. Poistetaan kaikki sekvenssin S alkiot ja asetetaan ne kolmeen osasekvenssiin: u L: ne, jotka ovat pienempiä kuin x u E: ne, jotka ovat yhtä suuria kuin x u G: ne, jotka ovat suurempia kuin x Koodi 8.4. (alku) Satunnaistettu pikavalinta algoritmi. 8. luku 451
if k L then quickselect(l,k) else if k L + E then return x {jokainen alkio E:ssä on yhtä suuri kuin x} else quickselect(g,k L E ) {uusi valintaparametri!} Koodi 8.4. (loppu) Satunnaistettu pikavalinta algoritmi. 8. luku 452