Tietorakenteet, laskuharjoitus 4,

Samankaltaiset tiedostot
Tietorakenteet, laskuharjoitus 4,

Tietorakenteet, laskuharjoitus 3, ratkaisuja

TIETORAKENTEET JA ALGORITMIT

811312A Tietorakenteet ja algoritmit II Perustietorakenteet

Tietorakenteet, laskuharjoitus 6,

A TIETORAKENTEET JA ALGORITMIT

Ohjelmointi 2 / 2008 Välikoe / Pöytätestaa seuraava ohjelma.

Algoritmit 2. Luento 7 Ti Timo Männikkö

Listarakenne (ArrayList-luokka)

Pino S on abstrakti tietotyyppi, jolla on ainakin perusmetodit:

58131 Tietorakenteet ja algoritmit (syksy 2015)

Algoritmit 1. Luento 4 Ke Timo Männikkö

Algoritmit 1. Luento 5 Ti Timo Männikkö

Kaksiloppuinen jono D on abstrakti tietotyyppi, jolla on ainakin seuraavat 4 perusmetodia... PushFront(x): lisää tietoalkion x jonon eteen

18. Abstraktit tietotyypit 18.1

private TreeMap<String, Opiskelija> nimella; private TreeMap<String, Opiskelija> numerolla;

Yleistä. Nyt käsitellään vain taulukko (array), joka on saman tyyppisten muuttujien eli alkioiden (element) kokoelma.

Algoritmit 2. Luento 2 Ke Timo Männikkö

Sisältö. 22. Taulukot. Yleistä. Yleistä

Sisällys. 18. Abstraktit tietotyypit. Johdanto. Johdanto

Informaatioteknologian laitos Olio-ohjelmoinnin perusteet / Salo

Ohjelmointi 2 / 2010 Välikoe / 26.3

Tietorakenteet, laskuharjoitus 2,

Kääreluokat (oppikirjan luku 9.4) (Wrapper-classes)

Sisältö. 2. Taulukot. Yleistä. Yleistä

Algoritmit 1. Demot Timo Männikkö

Rajapinta (interface)

58131 Tietorakenteet ja algoritmit (kevät 2016) Ensimmäinen välikoe, malliratkaisut

lähtokohta: kahden O(h) korkuisen keon yhdistäminen uudella juurella vie O(h) operaatiota vrt. RemoveMinElem() keossa

Ohjelmoinnin jatkokurssi, kurssikoe

Algoritmit 2. Luento 2 To Timo Männikkö

1. (a) Seuraava algoritmi tutkii, onko jokin luku taulukossa monta kertaa:

14. Poikkeukset 14.1

4 Tehokkuus ja algoritmien suunnittelu

2. Perustietorakenteet

(a) L on listan tunnussolmu, joten se ei voi olla null. Algoritmi lisäämiselle loppuun:

58131 Tietorakenteet ja algoritmit (kevät 2013) Kurssikoe 1, , vastauksia

Luokka Murtoluku uudelleen. Kirjoitetaan luokka Murtoluku uudelleen niin, että murtolukujen sieventäminen on mahdollista.

Algoritmit 2. Luento 3 Ti Timo Männikkö

4. Perustietorakenteet: pino, jono ja lista

Sisällys. 14. Poikkeukset. Johdanto. Johdanto

14. Poikkeukset 14.1

Tietorakenteet ja algoritmit Johdanto Lauri Malmi / Ari Korhonen

58131 Tietorakenteet ja algoritmit (kevät 2014) Uusinta- ja erilliskoe, , vastauksia

1.1 Pino (stack) Koodiluonnos. Graafinen esitys ...

Algoritmit 1. Demot Timo Männikkö

Algoritmit 2. Luento 3 Ti Timo Männikkö

ALGORITMIT 1 DEMOVASTAUKSET KEVÄT 2012

AVL-puut. eräs tapa tasapainottaa binäärihakupuu siten, että korkeus on O(log n) kun puussa on n avainta

Sisällys. 14. Poikkeukset. Johdanto. Johdanto

Tarkennamme geneeristä painamiskorotusalgoritmia

Rinnakkaisohjelmointi kurssi. Opintopiiri työskentelyn raportti

Ohjelmoinnin perusteet Y Python

Tietorakenteet ja algoritmit - syksy

Tietorakenteet, laskuharjoitus 1,

2. Seuraavassa kuvassa on verkon solmujen topologinen järjestys: x t v q z u s y w r. Kuva 1: Tehtävän 2 solmut järjestettynä topologisesti.

Algoritmit 1. Demot Timo Männikkö

JAVA-PERUSTEET. JAVA-OHJELMOINTI 3op A JAVAN PERUSTEET LYHYT KERTAUS JAVAN OMINAISUUKSISTA JAVAN OMINAISUUKSIA. Java vs. C++?

Algoritmit 2. Demot Timo Männikkö

58131 Tietorakenteet Erilliskoe , ratkaisuja (Jyrki Kivinen)

Tietorakenteet (syksy 2013)

Ohjelmassa henkilön etunimi ja sukunimi luetaan kahteen muuttujaan seuraavasti:

Algoritmit 2. Luento 4 To Timo Männikkö

1 Tehtävän kuvaus ja analysointi

Tietorakenteet. JAVA-OHJELMOINTI Osa 5: Tietorakenteita. Sisällys. Merkkijonot (String) Luokka String. Metodeja (public)

4. Joukkojen käsittely

Algoritmit 1. Luento 3 Ti Timo Männikkö

Tietorakenteet ja algoritmit

Ohjelmoinnin perusteet Y Python

Java ja grafiikka. Ville Sundberg

Luokan sisällä on lista

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

1. Mitä tehdään ensiksi?

811312A Tietorakenteet ja algoritmit , Harjoitus 2 ratkaisu

Algoritmit 2. Demot Timo Männikkö

811312A Tietorakenteet ja algoritmit, , Harjoitus 5, Ratkaisu

Tietorakenteet, laskuharjoitus 10, ratkaisuja. 1. (a) Seuraava algoritmi tutkii, onko jokin luku taulukossa monta kertaa:

815338A Ohjelmointikielten periaatteet Harjoitus 5 Vastaukset

Algoritmit 1. Luento 6 Ke Timo Männikkö

Algoritmit 1. Demot Timo Männikkö

20. Javan omat luokat 20.1

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op. Tietorakenneluokkia 2: HashMap, TreeMap

16. Javan omat luokat 16.1

Sisällys. 20. Javan omat luokat. Java API. Pakkaukset. java\lang

Olio-ohjelmointi Javalla

Tehtävän V.1 ratkaisuehdotus Tietorakenteet, syksy 2003

Tietorakenteet, laskuharjoitus 7, ratkaisuja

Hakupuut. tässä luvussa tarkastelemme puita tiedon tallennusrakenteina

List-luokan soveltamista. Listaan lisääminen Listan läpikäynti Listasta etsiminen Listan sisällön muuttaminen Listasta poistaminen Listan kopioiminen

Taulukot. Taulukon käsittely. Tämän osan sisältö. Esimerkki. Taulukon esittely ja luonti. Taulukon alustaminen. Taulukon koko

ITKP102 Ohjelmointi 1 (6 op)

Java-kielen perusteita

Java-kielen perusteet

811312A Tietorakenteet ja algoritmit, , Harjoitus 5, Ratkaisu

(p j b (i, j) + p i b (j, i)) (p j b (i, j) + p i (1 b (i, j)) p i. tähän. Palaamme sanakirjaongelmaan vielä tasoitetun analyysin yhteydessä.

useampi ns. avain (tai vertailuavain) esim. opiskelijaa kuvaavassa alkiossa vaikkapa opintopistemäärä tai opiskelijanumero

Tietorakenteet ja algoritmit

Algoritmit 2. Luento 4 Ke Timo Männikkö

Ohjelmoinnin peruskurssi Y1

TKT20001 Tietorakenteet ja algoritmit Erilliskoe , malliratkaisut (Jyrki Kivinen)

Transkriptio:

Tietorakenteet, laskuharjoitus 4, 9. 12.2 1. Tehtävässä piti toteuttaa jono käyttäen Javan valmiita LinkedList- sekä ArrayListtietorakenteita sekä tutkia niiden tehokkuutta. Kuvassa 1 näkyvät suoritettujen mittausten tulokset. 3000 LinkedList ArrayList 2500 2000 ms 1500 1000 500 0 10000 20000 30000 40000 50000 60000 70000 80000 90000 100000 alkioita Kuva 1: Jonon nopeus eri toteutuksilla lisättäessä ensin n alkiota ja tämän jälkeen poistettaessa n alkiota. Mittauksista näkee, että LinkedList on nopeudessaan aivan ylivoimainen. Tutkimalla Javan API-kuvausta selviää, että ArrayList on toteutettu taulukkoon ja Linked- List on toteutettu linkitettynä rakenteena. Poistettaessa ArrayListin alkupäästä tai keskeltä alkio joudutaan siirtämään kaikki sitä seuraavat alkiot askeleella eteenpäin. Linkitetyssä toteutuksessa taas korjataan vain viiteiden paikkaa ja jätetään poistettu listasolmu roskien kerääjän huoleksi. Siis ArrayListista poistettaessa joudutaan tekemään n i askelta, jossa n on alkioiden lukumäärä ja i poistettavan alkion indeksi. Kun alkio poistetaan aina listan edestä, niin n:ssä poisto-operaatiossa joudutaan tekemään (n 1) + (n 2) +... + 1 + 0 = n 1 i=0 i = O(n2 ) askelta. Tällöin yhtä operaatiota kohti kuluu keskimäärin lineaarinen aika. LinkedListia käytettäessä jokainen poisto listan kärjestä sujuu vakioajassa, joten n:n poisto-operatioon kuluvaksi ajaksi saadaan n O(1) = O(n). 1

Tämän perusteella LinkedListin käyttö tuntuu ArrayListin käyttö viisaammalta. Kuitenkin yleisessä tapauksessa riippuu sovelluksesta kumpaa kannattaa käyttää. Esimerkiksi ArrayList toteuttaa Javan RandomAccess rajapinnan, joka tarkoittaa, että jokaiseen listan alkioon päästään käsiksi vakioajassa. LinkedListin tapauksessa listaa joudutaan aina kelaamaan i askelta, kun halutaan päästä sen i:nnenteen alkioon käsiksi. Ohessa on Java-koodit, joita mittauksissa käytettiin. ArrayListJono.java import java.util.arraylist; class ArrayListJono { private ArrayList<Integer> lista; public ArrayListJono() { this.lista = new ArrayList<Integer>(); public boolean enqueue(integer alkio) { /* Tavallinen ArrayList-metodi palauttaa true, jos operaatio * onnistui ja false jos ei onnistunut. Voimme siis palauttaa * arvon suoraan kutsujalle. */ return this.lista.add(alkio); public Integer dequeue() { if (this.isempty()) return null; return this.lista.remove(0); public boolean isempty() { return this.lista.isempty(); LinkedListJono.java import java.util.linkedlist; class LinkedListJono { private LinkedList<Integer> lista; 2

public LinkedListJono() { this.lista = new LinkedList<Integer>(); public boolean enqueue(integer alkio) { return this.lista.add(alkio); public Integer dequeue() { if (this.isempty()) return null; return this.lista.remove(0); public boolean isempty() { return this.lista.isempty(); PerformanceTest.java import java.io.filewriter; import java.io.printwriter; import java.io.ioexception; class PerformanceTest { public static void main(string[] args) { if ( args.length < 3) { System.out.println("Anna testattava väli muodossa \"alku " + "loppu askel\""); System.out.println("Olkoon N = alku + i*askel. Jokaista N " + "<= loppu, molempiin jonoihin" + "tehdään N lisäystä ja poistoa"); System.exit(0); int alku = Integer.parseInt(args[0]); int loppu = Integer.parseInt(args[1]); int askel = Integer.parseInt(args[2]); PrintWriter ulos = null; try { ulos = new PrintWriter(new FileWriter("ArrayListJono.dat")); 3

for (int i = alku; i <= loppu; i += askel) { ArrayListJono jono1 = new ArrayListJono(); long start = System.currentTimeMillis(); for (int j = 0; j < i; j++) jono1.enqueue(j); for (int j = 0; j < i; j++) jono1.dequeue(); long end = System.currentTimeMillis(); ulos.println(i+"\t"+(end - start)); /* Toivotaan, että Javan roskienkeruu tapahtuu * ajanottojen ulkopuolella. */ System.gc(); catch (IOException e) { e.printstacktrace(); finally { if (ulos!= null) ulos.close(); ulos = null; try { ulos = new PrintWriter(new FileWriter("LinkedListJono.dat")); for (int i = alku; i <= loppu; i += askel) { LinkedListJono jono2 = new LinkedListJono(); long start = System.currentTimeMillis(); for (int j = 0; j < i; j++) jono2.enqueue(j); for (int j = 0; j < i; j++) jono2.dequeue(); long end = System.currentTimeMillis(); ulos.println(i+"\t"+(end - start)); System.gc(); catch (IOException e) { e.printstacktrace(); finally { if (ulos!= null) ulos.close(); 2. Tässä tehtävässä oli tarkoituksena toteuttaa pino kahdella eri tavalla. Molemmissa käytettiin taulukkoa alkioiden tallentamiseen. Ensimmäisessä pinossa taulukon kokoa kasvatettiin aina 100:lla kun tila loppui. Toisessa toteutuksessa tilan loppumiseen 4

reagoitiin tuplaamalla taulukon koko. Kuva 2 esittää tulokset. 50000 Pino2 Pino1 40000 30000 ms 20000 10000 0 100000 200000 300000 400000 500000 600000 700000 800000 900000 1e+06 alkioita Kuva 2: Erilaisten pinototeutusten nopeus lisättäessä ensin n alkiota ja tämän jälkeen poistettaessa n alkiota Kuvan 2 Pino2, jossa käytettiin tuplausstrategiaa osoittautui huomattavasti tehokkaammaksi. Pinoa jota kasvatetaan vain 100 alkiota kerrallaan vaivaa se, että joka sadas operaatio on lineaarinen alkioiden lukumäärän suhteen, sillä tällöin alkiot on kopioitava vanhasta taulukosta uuteen isompaan taulukkoon. Pino, jossa taulukko tuplataan on sen paljon nopeampi, koska kallis lineaarisen ajan vievä operaatio suoritetaan verrattain harvoin. Jos ajatellaan taulukon yhden alkion kopiontiin kuluvan yhden askelen niin voidaan laskea kuinka monta askelta joudutaan suorittamaan n:ssä lisäyksessä, jotka tehdään tyhjään pinoon. Laskelmissa on oletuksena, että taulukon koon lisäys aloitetaan yhdestä alkiosta. Tämä ei vaikuta mitenkään laskujen suuruusluokkaan, mutta helpottaa arviointia. Lasketaan ensin Pino1:n tapaus 1. n n 100 1 + 100 i n + 100 i=1 i=1 n 200 ( n 200 + 1) n + 2 100 n 2 40000 2 n2 800 = O(n2 ) Siis operaatioita tehdään vähintään luokkaa Ω(n 2 ). Toisaalta arvioimalla edellistä 1 Lausekkeessa käytetään lattiafunktiota, joka on pyöristys lähimpään lukua pienempään kokonaislukuun. Esimerkiksi 3.14 = 3 ja 3 = 3 5

lauseketta sopivasti ylöspäin nähdään että askeleita tehdään korkeintaan O(n 2 ). Yhdistämällä nämä kaksi tietoa saadaan suoritettavien askeleiden lukumääräksi Θ(n 2 ). Siis yksi askel vie keskimäärin lineaarisen ajan. Tarkastellaan nyt Pino2:n n:ssä peräkkäisessä lisäyksessä käytettävien askelten lukumäärää. Jälkimmäinen summa alla olevassa lausekkeessa seuraa siitä, että taulukko tarvitsee tuplata log 2 n kertaa, ja jokaisella tuplauksella kaikki alkiot pitää kopioida taulukosta toiseen. Oletetaan tässä kuitenkin laskujen yksinkertaistamiseksi, että luku log 2 n on kokonaisluku. Sopivilla ylös-ja alaspäin arvioilla päästäisiin tästä rajoituksesta eroon. Kun taulukko on yhden kokoinen pitää kopioida 1 = 2 0 alkiota, kun taulukko on kahden kokoinen pitää kopioida 2 = 2 1 alkiota jne. n i=1 log 2 n 1 + 2 i = n + 2 log 2 n+1 1 n + 2 2 log 2 n = n + 2 n = 3n i=0 Siis keskimäärin n:n lisäyksen jonossa jokainen lisäys vie vakioajan vaikka pahimman tapauksen aikavaativuus onkin lineaarinen. Tälläisessä tapauksessa sanotaan, että operaatio on tasoitetulta vaativuudeltaan vakioaikainen. Taulukkoon voidaan toteuttaa myös taulukon kutistaminen sopivalla tavalla poistooperaatioiden yhteyteen. Sopiva tapa tarkoittaa tässä sitä, että taulukon koko puolitetaan esimerkiksi silloin, kun sen täyttöaste on 1 4. Tällöin voidaan osoittaa, että peräkkäisistä lisäyksistä ja poistoista saadaan tasoitetulta vaativuudeltaan vakioaikaisia. Ohitamme tässä perustelut, koska tasoitettu vaativuus ei varsinaisesti kuulu kurssin sisältöön. On kuitenkin hyödyllistä tuntea käsite. Lisäksi taulukkoja muuttuvan kokoisia taulukoita tarvitaan melko usein, joten on hyvä tietää miten se voidaan toteuttaa tehokkaasti. Ohessa on vielä Java-koodit, joita mittauksissa käytettiin. Pino1.java import java.lang.runtimeexception; class Pino1 { private int[] array; private int top; public Pino1() { this.array = new int[100]; this.top = 0; public Pino1(int eka) { this.array = new int[100]; this.top = 0; 6

this.array[top++] = eka; public int pop() { if ( this.isempty() ) throw new RuntimeException(); int elem = this.array[--this.top]; /* Varmistetaan, että pinossa on korkeintaan 100 vapaata paikkaa. */ if (this.top < this.array.length - 100 ) { int[] temp = new int[this.array.length - 100]; System.arraycopy(this.array, 0, temp, 0, this.top); this.array = temp; return elem; public void push(int elem) { if ( this.top == this.array.length ) { int[] temp = new int[this.top + 100]; System.arraycopy(this.array, 0, temp, 0, this.top); this.array = temp; this.array[this.top++] = elem; public boolean isempty() { if ( this.top == 0 ) return true; else return false; Pino2.java import java.lang.runtimeexception; class Pino2 { private int[] array; private int top; public Pino2() { this.array = new int[100]; this.top = 0; 7

public Pino2(int eka) { this.array = new int[100]; this.top = 0; this.array[top++] = eka; public int pop() { if ( this.isempty() ) throw new RuntimeException(); int elem = this.array[--this.top]; /* Huomaa, että taulukkoa pienennetään vasta kuin siitä on 3/4 * tyhjänä. Tämä tehdään siksi, ettei olisi mahdollista joutua * peräkkäin kasvattamaan ja pienentämään. */ if (this.top < this.array.length/4 ) { int[] temp = new int[this.array.length/2]; System.arraycopy(this.array, 0, temp, 0, this.top); this.array = temp; return elem; public void push(int elem) { if ( this.top == this.array.length ) { int[] temp = new int[2*this.top]; System.arraycopy(this.array, 0, temp, 0, this.top); this.array = temp; this.array[this.top++] = elem; public boolean isempty() { if ( this.top == 0 ) return true; else return false; PerformanceTest2.java import java.io.filewriter; import java.io.printwriter; import java.io.ioexception; class PerformanceTest2 { 8

public static void main(string[] args) { if ( args.length < 3) { System.out.println("Anna testattava väli muodossa \"alku "+ "loppu askel\""); System.out.println("Olkoon N = alku + i*askel. Jokaista N "+ "<= loppu, molempiin jonoihin" + "tehdään N lisäystä ja poistoa"); System.exit(0); int alku = Integer.parseInt(args[0]); int loppu = Integer.parseInt(args[1]); int askel = Integer.parseInt(args[2]); PrintWriter out = null; try { out = new PrintWriter(new FileWriter("Pino1.dat")); for (int i = alku; i <= loppu; i += askel) { Pino1 jono1 = new Pino1(); long start = System.currentTimeMillis(); for (int j = 0; j < i; j++) jono1.push(j); for (int j = 0; j < i; j++) jono1.pop(); long end = System.currentTimeMillis(); out.println(i+"\t"+(end - start)); /* Tällä pyritään saamaan JVM hoitamaan * roskienkeruu testien ulkopuolella. */ System.gc(); catch (IOException e) { e.printstacktrace(); finally { if (out!= null) out.close(); out = null; try { out = new PrintWriter(new FileWriter("Pino2.dat")); for (int i = alku; i <= loppu; i += askel) { Pino2 jono2 = new Pino2(); long start = System.currentTimeMillis(); for (int j = 0; j < i; j++) jono2.push(j); 9

for (int j = 0; j < i; j++) jono2.pop(); long end = System.currentTimeMillis(); out.println(i+"\t"+(end - start)); System.gc(); catch (IOException e) { e.printstacktrace(); finally { if (out!= null) out.close(); 3. Ohessa olevan listan metodeilla on seuraavat aikavaativuudet. delete(k) vie lineaarisen ajan, koska samalla täytyy suorittaa avaimen haku. Pahimmassa tapauksessa (esimerkiksi jos poistettavaa avainta ei löydy) joudutaan selaamaan listan kaikki alkiot läpi. Linkkien ylläpito-operaatiot, joita tehdään jos alkio poistetaan, sujuvat vakioajassa. search(k) vie samoista syistä lineaarisen ajan kuin delete-operaatiokin. add(k) sujuu vakioajassa laittamalla jokainen lisättävä alkio jonon etupäähän. Tarvittavat linkkien ylläpito-operaatiot sujuvat vakio-ajassa. list() vie lineaarisen ajan, koska lista on pyyhkäistävä läpi kertaalleen. reverse() vie myös lineaarisen ajan, koska listan jok ikisen viitteen suunta on käännettävä. Tämä kyetään tekemään kulkemalla kerran listan läpi. Ohessa on vielä toteutuksen Java-koodi. class MyLinkedList { private class ListNode { /* Sisäluokan attribuutteihin halutaan päästä helposti käsiksi, * joten jätetään ne julkisiksi. */ public int key; public ListNode next; public ListNode(int arvo) { this.key = arvo; this.next = null; private ListNode head; 10

public MyLinkedList() { this.head = null; public boolean search(int avain) { ListNode current = this.head; /* Iteroidaan listaa kunnes löydetään avain tai loppu tulee vastaan.*/ while ( current!= null && avain!= current.key ) { current = current.next; return (current!= null)? true : false; public void delete(int avain) { ListNode current = this.head; ListNode prev = null; while ( current!= null && avain!= current.key ) { prev = current; current = current.next; if (current!= null) { // jos löytyi hoidetaan linkit kuntoon if ( prev!= null ) // poistettu alkio ei ollut ensimmäinen prev.next = current.next; else { // poistettu alkio oli ensimmäinen this.head = current.next; public void add(int avain) { ListNode node = new ListNode(avain); node.next = this.head; this.head = node; public void list() { ListNode current = this.head; while( current!= null ) { System.out.print(current.key + " -> "); current = current.next; System.out.println("null"); 11

public void reverse() { if ( this.head == null this.head.next == null ) return; ListNode prev = this.head; ListNode current = prev.next; prev.next = null; while ( current!= null ) { ListNode temp = current.next; current.next = prev; prev = current; current = temp; this.head = prev; public static void main(string[] args) { MyLinkedList lista = new MyLinkedList(); for (int i = 0; i < 5; i++ ) lista.add( (int) (100*Math.random()) ); lista.add(101); lista.add(101); for (int i = 0; i < 5; i++ ) lista.add( (int) (100*Math.random()) ); lista.list(); lista.delete(101); lista.list(); lista.reverse(); lista.list(); if ( lista.search(101) ) System.out.println("101 löytyi listasta"); else System.out.println("101 ei löytynyt listasta"); 4. Kun lista pidetään järjestyksessä jokainen operaatio paitsi add(k) vievät saman ajan kuin edelläkin. Koska järjestettyyn listaan ei voi lisätä alkioita mielivaltaiseen paikkaan, täytyy uusille alkioille aina etsiä oikea paikka listasta. Pahimmassa tapauksessa kuljetaan lista kerran läpi, jonka vuoksi tämä vie ajan O(n). Ohessa on toteutus Javalla. Huomaa, että Javamaisempi tapa tässä olisi ollut tehdä tästä edellisen kohdan aliluokka. class MyOrderedList { 12

private class ListNode { /* Sisäluokan attribuutteihin halutaan päästä helposti käsiksi, * joten jätetään ne julkisiksi. */ public int key; public ListNode next; public ListNode(int arvo) { this.key = arvo; this.next = null; private ListNode head; public MyOrderedList() { this.head = null; public boolean search(int avain) { ListNode current = this.head; /* Iteroidaan listaa kunnes löydetään avain tai loppu tulee vastaan.*/ while ( current!= null && avain > current.key ) { current = current.next; return (current!= null && current.key == avain)? true : false; public void delete(int avain) { ListNode current = this.head; ListNode prev = null; while ( current!= null && avain > current.key ) { prev = current; current = current.next; if (current!= null) { // jos löytyi hoidetaan linkit kuntoon if ( prev!= null ) // poistettu alkio ei ollut ensimmäinen prev.next = current.next; else { // poistettu alkio oli ensimmäinen this.head = current.next; public void add(int avain) { 13

ListNode node = new ListNode(avain); ListNode prev = null; ListNode current = this.head; while ( current!= null && avain > current.key ) { prev = current; current = current.next; node.next = current; if (prev == null) this.head = node; else prev.next = node; public void list() { ListNode current = this.head; while( current!= null ) { System.out.print(current.key + " -> "); current = current.next; System.out.println("null"); public static void main(string[] args) { MyOrderedList lista = new MyOrderedList(); for (int i = 0; i < 5; i++ ) lista.add( (int) (100*Math.random()) ); lista.add(101); lista.add(101); for (int i = 0; i < 5; i++ ) lista.add( (int) (100*Math.random()) ); lista.list(); lista.delete(101); lista.list(); lista.delete(101); lista.list(); if ( lista.search(101) ) System.out.println("101 löytyi listasta"); else System.out.println("101 ei löytynyt listasta"); 5. Esitetään kaksisuuntaisesti linkitetty lista toteutettuna taulukkoon, jonka koko on n. Taulukon kukin alkio sisältää kentät key, prev sekä next. next- ja prev-kentät sisältävät taulukon indeksit asiaan kuuluville solmuille. Tämän lisäksi talletetaan taulukon yhteyteen kokonaisluku head, joka on ensimmäisen solmun indeksi taulukos- 14

sa. Käytännössä tämä voitaisiin toteuttaa käyttäen kaksiulotteista n 3-taulukkoa tai tallettamalla yksiulotteisen taulukon kenttiin viite tietueeseen, johon on talletettu edellämainitut kentät. Pseudokoodiesityksessä emme ota kantaa varsinaiseen toteutukseen vaan viittaamme esimerkiksi taulukon 3:nnessa paikassa olevan alkion avaimeen merkinnällä A[3 ]. key. Tallettamamme tiedon lisäksi meidän tarvitsee myös ylläpitää informaatiota taulukon vapaista paikoista. Käytetään tähän pinoa P, jossa on talletettuna vapaiden indeksien paikat. Lisättäessä taulukkoon alkioita talletetaan uusi alkio pinon päältä löytyvälle taulukon riville. Poistettaessa alkioita pinoon lisätään poistetun alkion indeksi. Pinon koko on suurimmillaan n, joten listan esitys taulukossa vaatii tilaa jokaisella ajanhetkellä Θ(n). Merkitään numerolla 0 Nil-viitettä. Alussa kun lista on tyhjä pinossa on kokonaisluvut 1, 2,..., n ja headin arvo on 0. Tarvitsemme siis listan L esittämiseen taulukon A, pinon P sekä kokonaisluvun head. Nyt olemme valmiita esittämään pseudokoodit Insert- sekä Delete-operaatioille. Insert ottaa parametrinaan avaimen ja lisää sen listaan sekä palauttaa lisätyn avaimen indeksin. Delete ottaa parametrikseen poistettavan alkion indeksin taulukossa ja poistaa tämän indeksin paikalta löytyneen alkion. Tässä on hyvä huomata, että jos Delete- operaatiolle annetaan indeksi joka ei kuulu listaan, niin koko tietorakenne korruptoituu. Delete(L, x) 1 if head == x // Poistetaan listan ensimmäinen alkio 2 head = A[x].next 3 if A[x].next 0 // Poistettava alkio ei ollut listan viimeinen 4 A[A[x].next].prev = 0 5 else // Poistettavalla alkiolla on ainakin yksi edeltäjä 6 A[A[x].prev].next = A[x].next 7 if A[x].next 0 // Poistettava alkio ei ollut listan viimeinen 8 A[A[x].next].prev = A[x].prev // Lopuksi lisätään poistetun alkion paikka vapaiden alkioiden joukkoon 9 Push(P, x) 15

Insert(L, k) 1 if Empty(P) 2 error lista on täynnä 3 i = Pop(P) 4 A[head].prev = i 5 A[i].key = k 6 A[i].next = head 7 A[i].prev = 0 8 head = i Nähdään molempien operaatioiden sujuvan vakioajassa, sillä osaamme toteuttaa pinon operaatiot tehokkaasti eikä kummassakaan operaatiossa suoriteta korkeintaan kuin vakiomäärän verran askelia. 6. Esitetään ensin vakiotilassa ja neliöllisessä ajassa toimiva algoritmi silmukan havaitsemiseen. Ajatuksena on, että edetään listassa aina yksi solmu kerrallaan ja pidetään yllä invarianttia: Ennen tätä solmua listassa ei ole esiintynyt viitteitä taaksepäin. Jos tämän hetkinen solmu on järjestyksessään n:s, niin käydään läpi solmut 1,..., n 1 ja tarkistetaan onko joku niistä myös n:s solmu. Siis n:n solmun tarkistamisessa tarvitsee suorittaa n 1 askelta. Summaamalla kaikkien solmujen tarkistukseen kuluvan ajan tarvitaan silmukallisessa listassa n 1 i=0 i+o(n) = O(n2 )+O(n) = O(n 2 ) askelta. Kaavan O(n) tulee silmukan havaitsemiseen kuuluvasta ajasta viimeisellä kierroksella. On helppo nähdä että myös silmukattoman listan tarkistamiseen kuluu saman verran aikaa. Esitetään nyt algoritmi pseudokoodina. OnkoSilmukkaa1(L) 1 current = L.head.next 2 n = 1 3 while current Nil 4 check = L. head 5 i = 0 6 repeat 7 if check == current // sykli löytyi 8 return True 9 check = check. next 10 i = i + 1 11 until i n 12 current = current. next 13 n = n + 1 14 return True Tarkastelemalla pseudokoodia havaitaan, että sitä ei tarvitsisi muuttaa kovinkaan paljon, mikäli haluttaisiin myö palauttaa solmu, jonka linkki oli virheellinen. Tämä 16

vaatisi prev- mukana pitämistä, jonka arvo olisi riviltä 6 alkavassa silmukassa aina sellainen, että prev.next == check. Toinen tapa havaita onko listassa silmukka olisi koittaa kääntää listan linkit muokkaamalla tehtävässä 4 suunniteltua Reverse-operaatiota. Tällöin, jos listassa L olisi silmukka, niin L. head pysyisi samana Reverse-operaationkin jälkeen. Jos taas listassa ei olisi silmukkaa niin listan ensimmäinen alkio olisi joku toinen alkio, kuin ennen Reverse-operaatiota. Lista voitaisiin palauttaa alkuperäiseksi kääntämällä se uudelleen. Tämä veisi tilaa ainoastaan vakiomäärän, eikä aikavaativuuskaan olisi kuin Θ(n). Kuitenkin tässä lähestymistavassa on 2 ongelmaa: listaa muokattaisiin laskennan aikana emmekä löytäisi kohtaa, jossa silmukka esiintyy. Ongelma voidaan muotoilla hiukan yleisemmällä tavalla, jolloin puhutaan syklin tunnistamisalgoritmeista (cycle detection). Ongelmaan on esitetty useita ajassa O(n) ja tilassa O(1) toimivia algoritmeja, joista tässä esitellään Floydin kilpikonna ja jänis - algoritmi sovellettuna yksisuuntaisesti linkitettyyn listaan. Yleistettäessä algoritmi sykleihin yleisellä tasolla ei se muutu juuri lainkaan 2. Ajatuksena on lähettää kilpikonna ja jänis kulkemaan listaa eteenpäin. Kilpikonna on jänistä hitaampi ja se ehtii edetä vain yhden solmun verran siinä ajassa kun jänis etenee kahden solmun verran. Jokaisen askeleen jälkeen tarkistetaan ovatko jänis ja kilpikonna samassa kohdassa. Mikäli ne jossain vaiheessa ovat on listassa silmukka. Algoritmi voidaan muokata palauttamaan sen solmun järjestysnumeron, johon listan taaksepäin suuntautuva linkki osoittaa. Tässä esitetään kuitenkin ainoastaan algoritmin alkuosa, joka tutkii onko listassa silmukka. Merkitään kilpikonnaa ja jänistä viitemuuttujilla k ja j. OnkoSilmukkaa2(L) 1 k = j = L.head 2 if j == Nil tai j.next == Nil 3 return False 4 repeat 5 k = k.next 6 j = j.next.next 7 until j == k tai j == Nil tai j.next == Nil 8 if j == k 9 return True 10 else return False Tarkastellaan tapausta, jossa listassa on silmukka. Kun kilpikonna pääsee listan silmukan alkukohtaan on jänis jo kiertämässä silmukkaa. Jänis etenee kilpikonnaan nähden kaksinkertaisella vauhdilla, joten kun kilpikonna on kiertänyt silmukan on jänis kiertänyt sen vähintään kerran. Tämän vuoksi aika, jonka kilpikonna viettää 2 Tällöin tarkastellaan mielivaltaista funktiota f ja lukujonoa x 0, x 1 = f(x 0), x 2 = f(x 1),... Tapauksessamme funktion f korvaa linkitetyn listan next-viite 17

silmukassa on luokkaa O(n). Lisäksi kilpikonnan matka silmukaan kestää O(n), joten koko algoritmille riittää lineaarinen aika. Algoritmissa käytetään vain kahta muuttujaa, joten se toimii vakioajassa. 18