previous next Title Contents Index

22. Etsiminen ja lajittelu


Mitä tässä luvussa käsitellään?

* yksinkertainen etsiminen

* etsiminen taulukkoon

* lajittelu valmiilla qsort- aliohjelmalla

* permutaatiotaulukko

* "oikeaoppinen lajittelu" Etsiminen ja lajittelu ovat eräitä yleisimmin tietojenkäsittelyssä vastaan tulevia tilanteita. Esimerkiksi C- merkkijonon pituuden laskeminen on oikeastaan NUL- merkin paikan etsimistä.

HUOM! Tämän luvun ohjelmalistaukset ovat "pseudo"- koodia, ja niissä voi olla pieniä syntaksivirheitä.

22.1 Etsiminen

Etsimistä voidaan suorittaa monella tavalla. Mikäli tietorakenne ei tarjoa parempaa vaihtoehtoa, ei ole muuta mahdollisuutta kuin peräkkäishaku. Järjestetystä taulukosta pystyttiin hakemaan binäärihaulla. Puumaisesta rakenteesta voidaan hakea puun ominaisuuksia käyttäen. Joskus voidaan tehdä avuksi hakemistoja, jotka ohjaavat haun suurinpiirtein oikealle paikalleen, josta sitten jatketaan jollakin toisella hakumenetelmällä.

C- kielen kirjastosta löytyy funktio bsearch, joka etsii lajitellusta aineistosta binäärihaulla ja palauttaa NULL mikäli ei löydy ja muuten palauttaa osoittimen taulukon löytyneeseen alkioon. Funktion käyttö on vastaava kuin myöhemmin esiteltävän qsort - funktion käyttö, joten emme tässä puutu siihen enempää.

Kerhorekisterissä tietorakenne on varsin alkeellinen, eikä sitä ole ainakaan toistaiseksi edes järjestetty. Siis tietyn jäsenen etsiminen on suoritettava raakana peräkkäishakuna.

Tehtävä 22.173 etsi_nimi

Kirjoita metodi etsi_nimi, joka etsii jäsenistöstä ensimmäisen henkilön, jolla on TÄSMÄLLEEN parametrina annettu nimi. Funktio palauttaa nimessään löytöpaikan indeksin ja - 1 jollei löydy.
Voitaisiinko metodia käyttää apuna lisäyksessä tarkistamaan onko jäsen jo ennestään tiedostossa?
Voitaisiinko metodia käyttää apuna korjaamisessa tarkistamaan onko nimi muuttunut sellaiseksi, joka on jo ennestään tiedostossa?

22.1.1 wildmat

Jos etsimisessä halutaan sallia jokerimerkkien käyttö, saattaa etsiminen johtaa joka tapauksessa peräkkäishakuun. Voitaisiin kuitenkin erotella erikoistapauksia:
	Ankka Aku -  voitaisiin käyttää mitä tahansa järkevää
	            järjestetyn joukon hakualgoritmia
	Ankka*    -  voitaisiin etsiä järjestetystä joukosta 1. Ankka
	            ja tämän jälkeen loppu peräkkäishakuna
	*Aku*     -  vaikea keksiä muuta kuin peräkkäishaku
Lähdimme kuitenkin alunperin oletuksesta, että kerhon koko on pieni. Siis tuskin peräkkäishakukaan vie kohtuuttomasti aikaa. Näin ollen käsittelemme kaikki hakutilanteet samalla tavalla.

Entäpä mikäli etsimisessä löytyy useita ehdon täyttäviä henkilöitä. Eräs tyypillinen tapa on tehdä aliohjelmapari:

	etsi_ensimmainen
	etsi_seuraava
Vähän samaa ideaa sovellettiin jo aikaisemmin palanen - aliohjelmassa.

Tehtävä 22.174 etsi_nimi_alkaen

Kirjoita metodi etsi_nimi_alkaen, joka etsii kerhosta nimeä aloittaen etsimisen parametrina välitetystä indeksistä. Tällä kertaa hakuehtona saa olla myös jono "*Aku*". Metodi palauttaa löytöpaikan indeksin tai - 1, mikäli etsittävää ei löydy.

22.1.2 Etsiminen taulukkoon

Toinen mahdollisuus etsimiseen on tallettaa taulukkoon kukin löytynyt ehdot täyttävä tietue. Määrittelemme tyypin

jarjestys.h - taulukko löytyneistä

	class cJarjestys {   /* Luokka jonka avulla voidaan pitää eri järjestyksiä.  */
	  int max_koko;                 /* Indeksitaulukon maksimikoko.              */
	  int indekseja;                /* Taulukosta nyt käytetty indekseja.        */
	  int *indeksit                 /* Osoitin indeksitaulukkoon (dynaaminen).   */
	...
	};  

Jos seuraavasta aineistosta etsittäisiin vaikkapa hakuehdolla nimi=="*k*"

                   **jasenet    |      o      |
cJarjestys                      +------+------+                cJasen
   etsi  +---+                         |                     +--------------+   
max_koko | 7 |                         v                     |Kassinen Katto|   
indekseja| 2 |                    +----------+               |Katto         |   
*indeksit| o |  +--->jasenet[0]   |    o-----+-------------->|3452          |   
         +-+-+  |                 +----------|               |...           |    
           |    |    jasenet[1]   |    o-----+--------+      +--------------+   
           v    |                 +----------|        |      +-------------+    
         +---+  | +->jasenet[2]   |    o-----+------+ |      |Susi Sepe    |    
       0 | 0 +--+ |               +----------|      | +----->|Takametsä    |    
         +---|    |     ...       |          |      |        |-            |    
       1 | 2 +----+               +----------|      |        |...          |   
         +---|                    |          |      |        +-------------+    
       2 | ? |                    +----------|      |        +-------------+
         +---|                    |          |      +------->|Ankka Aku    |
       3 | ? |                    +----------|               |Ankkalinna   |
         +---|                    |          |               |1234         |
       4 | ? |                    +----------|               |...          |   
         +---|                    |          |               +-------------+
       5 | ? |                    +----------+ 
         +---|               osoitintaulukko henkilöihin
       6 | ? |
         +---|
       7 | ? | indeksitaulukko löytyneisiin
         +---+

löytyisi jasenet[0] ja jasenet[2], jotka täyttävät hakuehdon. Tällöin etsimisaliohjelma palauttaisi tämän tiedon seuraavasti:

	
	cJarjestys etsi
           +-----+ 
  max_koko |  7  |         0  1  2  3  4  5  6 
  indekseja|  2  |       +--------------------+ 
 *indeksit |  o--+------>| 0| 2| ?| ?| ?| ?| ?|
           +-----+       +--------------------+

Nyt tietäisimme heti, että meillä on kaksi löytynyttä, joten löytyneet voitaisiin tulostaa:

	for (i=0; i<etsi.Indekseja(); i++)
	  Kerho- >Jasenet()- >anna(etsi.anna(i)).tulosta(cout) 

22.1.3 Etsiminen valitun kentän mukaan

Mitä jos haluamme etsiä jonkin tietyn kentän mukaan. Välitämme etsimisaliohjelmalle tiedoksi kentän numeron, jonka mukaan haluamme etsiä. Käytännössä osoittautuu helpoimmaksi välittää myös etsittävä isona tietueena (tai taulukkona), johon on täytetty nimenomaan etsittävään kenttään halutut tiedot.

22.1.4 etsi ja indeksit

Etsimistä voitaisiin hieman monipuolistaa siten, että voitaisiin laittaa ehto jokaiseen tutkittavan jasen- luokan kenttään. Sitten löytymisen ehtona voisi olla, että jokin kentistä täsmää (TAI) tai että kaikki kentät täsmäävät (JA). Tämä voitaisiin ilmaista etsimisaliohjelmalle vaikkapa siten, että JA etsimisessä kentta=- 1 ja TAI etsimisessä kentta=- 2.

Tehtävä 22.175 etsi- olio

Hahmottele mitä metodeja tulisi olla luokassa cJarjestys ja mitä sitä käyttävälle etsimismetodille tulisi viedä parametrina ja mihin luokkaan etsimismetodi tulisi kirjoittaa?

Tehtävä 22.176 TAI- etsiminen

Miten käyttäjä saisi TAI- etsimisen avulla vastauksen kysymykseen: Onko missään kentässä missään kohti sana aku?

22.1.5 cHaku

Etsimisaliohjelmalla kannattaa ehkä viedä parametrina cHaku- luokkaa oleva olio sen sijaan, että vietäisiin cJasen - tyyppinen olio. Tähän on kaksi syytä.

Ensinnäkin on käyttäjän kannalta mukavaa, että jos hän on joskus hakenut nimihaussa nimellä "*aku*", niin seuraavassakin nimihaussa hänellä on oletuksena sama hakuehto. Siis eri kentissä viimeksi käytetyt hakuehdot kannattaa kukin säilyttää erikseen.

Toisaalta jos haluamme toteuttaa JA ja TAI - tyyppiset haut, tarvitsemme etsimisaliohjelmalle hakuehdon kullekin kentälle.

cHaku voisi olla myös cJasen, mutta koska cJasen kentillä on mahdollisuus saada myös numeerisia arvoja, ei numeeriseen kenttään pystyttäisi tallettamaan esimerkiksi hakuehtoa "4*" tai "<50". Siis cHaku on parasta tehdä merkkijono taulukoksi

	class cHaku {
	  string kentat[KENTTIA];
	...
	}
	

22.2 Selailu

Kun etsittävän ehdon täyttävät tietueet ovat löytyneet, voitaisiin niitä selailla suhteellisen helposti:

22.2.1 selaile

	  suunta = 0;
	  while (1) {
	    - lisää kohdalla olevaan suunta
	    - korjaa sallitun välin sisälle 
	    - tulosta kohdalla oleva
	    painettu = odota_nappain(sallitut,RET,VAIN_ISOT);
	    switch (painettu) {
	      case '- ': suunta = - 1; break;
	      case '+': suunta =  1; break;
	      default : return painettu;
	    }
	  } 

Tehtävä 22.177 Koko kerhon selailu

Jos meillä on käytössä hakuehdon kysyminen, etsiminen ja selailu, niin tarvitseeko koko kerhon selaamista varten tehdä oma aliohjelma?

22.3 Lajittelu

Lajittelun vaatimia algoritmeja olemme käsitelleet jo aikaisemmin. Mikä tahansa aikaisemmin mainituista algoritmeista olisi helppo soveltaa jäsentaulukkoon. Kuitenkin lähes kaikki oppimamme algoritmit olivat kompleksisuudeltaan O(n2). Jos jäsenmäärä kasvaa paljon yli sadan, tarkoittaa tämä suhteellisen hidasta lajittelua.

C- kielen standardikirjasto stdlib.h tarjoaa valmiin QuickSort lajittelualiohjelman qsort, jonka kompleksisuus on O(n log n). Miten lajitteluohjelma voidaan tehdä yleiskäyttöiseksi? Idea on siinä, että lajiteltiin mitä aineistoa tahansa, niin lähes ainoa aineistosta riippuva seikka on se, miten päätetään kahdesta alkiosta kumpiko on toistaan suurempi vai ovatko ne samoja.

Siis alkioiden vertailun suorittava aliohjelma kirjoitetaan itse. Sitten tämän aliohjelman osoite viedään qsort aliohjelmalle, jolloin tarvittaessa qsort kutsuu tätä vertailijaa.

22.3.1 qsort

Seuraavassa on eräs esimerkki qsort - aliohjelman käytöstä. Ohjelma arpoo (rand) satunnaisen kokonaislukutaulukon ja tämän jälkeen järjestää sen. qsort kutsuu vertailualiohjelmaa kahdella osoitintyyppisellä muuttujalla, jotka osoittavat verrattaviin alkioihin. Osoittimet on määritelty void * - tyyppisiksi. Koska esimerkissämme alkiot ovat kokonaislukuja, pitää osoittimet käytön yhteydessä muuttaa int * - tyyppisiksi.

Vertailualiohjelman pitää palauttaa negatiivinen luku, mikäli 1. alkio on pienempi kuin toinen, 0 mikäli ne ovat samoja ja positiivinen luku, mikäli 1. on suurempi ("erotuksen etumerkki"). Siis tässä tapauksessa lukujen vähennyslasku tuottaa valmiiksi halutunlaisen tuloksen.

	...
	/* Alkioiden vertailufunktio.                          */
	int sort_function(const void *a, const void *b)
	{
	  return *(int *)a -  *(int *)b;
	}
	
	int main(void)
	{
	  int i,x[KOKO];
	
	  for (i=0; i<KOKO; i++)  x[i] = rand();
	  tulosta(x);
	
	  qsort((void *)x,KOKO,sizeof(x[0]),sort_function);
	
	  tulosta(x);
	  return 0;
	}

qsort - aliohjelman kutsussa pitää kertoa lajiteltavan taulukon (osaa lajitella vain taulukoita) alkuosoite, koko alkioina sekä kunkin alkion koko, sekä vertailufunktion osoite.

Huomattakoon, ettei kutsun viimeinen parametri sort_function aiheuta kutsua aliohjelmaan sort_function, vaan välittää qsort aliohjelmalle lajittelufunktion osoitteen. Kääntäjä tietää tämän eron siitä, ettei funktion nimen perässä ole sulkuja!

22.3.2 const

Edellä const void *a tarkoitti vain sitä, että tällä korostetaan ettei aliohjelmassa muuteta osoittimen a osoittamaa muistipaikkaa. Jokin kääntäjä antaisi virheen sijoituksesta
	int ali(const int *a)
	{
	  int b;
	  *a = 5;  // Laiton sijoitus, koska const osoitin ei saa muuttaa kohdettaan
	  a = b;   // Sallittu sijoitus, kohde ei muutu.
	}

22.3.3 volatile

const- määreelle lähes päinvastainen on volatile- määre (suom. epävakaa). Tällä korostetaan sitä, että jokin taustaprosessi tai rinnakkainen aliohjelma saattaa muuttaa muistipaikan sisältöä ilman mitään ohjelmassa näkyvää sijoitusta. Esimerkiksi tietoliikenneohjelmassa voisi olla muuttuja linja_varattu, jota muuttaisi taustalla toimiva kommunikointipaketti:
	
	volatile int linja_varattu;
	...
	set_param(LINE_RESERVED,&linja_varattu);
	...
	dial_number("112"); /* soittelee taustalla kunnes pääsee läpi */
	while ( linja_varattu && !lopetus() ) 
	  do_sound(VARATTU_AANI);
	... 

Esimerkissä voisi olla myös

	const volatile int linja_varattu; 
koska itse pääohjelmalla ei ole mielekästä sijoittaa arvoa ko. muuttujaan. Nämä ominaisuudet ovat kuitenkin edistyneempien kurssien asioita.

22.3.4 Permutaatiotaulu

Jäsenrekisteristä voitaisiin lajitella se taulukko, jossa on osoittimet jäseniin. Mikäli joku kuitenkin haluaisi säilyttää alkuperäisenkin järjestyksen, voisimme yrittää keksiä tapoja lajitella aineisto siten, että siihen olisi yhtäaikaa useita eri järjestyksiä.

Helpoin ratkaisu tähän on permutaatiotaulukoiden käyttö. Aikaisempi etsimisessä mallina ollut jäsenrekisteri voitaisiin lajitella vaikkapa nimen ja postinumeron mukaan:

                   **jasenet    |      o      |
cJarjestys                      +------+------+                Jasen_tyyppi
         +---+nimen_mukaan             |                     +--------------+   
max_koko | 7 |                         v                     |Kassinen Katto|   
indekseja| 3 |                    +----------+               |Katto         |   
*indeksit| o |  +--->jasenet[0]   |    o-----+-------------->|3452          |   
         +-+-+  |                 +----------|               |...           |    
           |    |+-->jasenet[1]   |    o-----+--------+      +--------------+   
           v    ||                +----------|        |      +-------------+    
         +---+ +++-->jasenet[2]   |    o-----+------+ |      |Susi Sepe    |    
       0 | 2 +-+||                +----------|      | +----->|Takametsä    |    
         +---|  ||      ...       |          |      |        |-            |    
       1 | 0 +--+|                +----------|      |        |...          |   
         +---|   |                |          |      |        +-------------+    
       2 | 1 +---+                +----------|      |        +-------------+
         +---|                    |          |      +------->|Ankka Aku    |
       3 | ? |                    +----------|               |Ankkalinna   |
         +---|                    |          |               |1234         |
       4 | ? |                    +----------|               |...          |   
         +---|                    |          |               +-------------+
       5 | ? |                    +----------+ 
         +---|               osoitintaulukko henkilöihin
       6 | ? |
         +---|
       7 | ? | indeksitaulukko joka osoittaa järjestyksen
         +---+
cJarjestys nimen_mukaan
           +-----+ 
  max_koko |  7  |         0  1  2  3  4  5  6 
  indekseja|  3  |       +--------------------+ 
 *indeksit |  o--+------>| 2| 0| 1| ?| ?| ?| ?|
           +-----+       +--------------------+

cJarjestys postin_mukaan
           +-----+ 
  max_koko |  7  |         0  1  2  3  4  5  6 
  indekseja|  3  |       +--------------------+ 
 *indeksit |  o--+------>| 1| 2| 0| ?| ?| ?| ?|
           +-----+       +--------------------+
Lajittelu aloitetaan siten, että järjestettävä permutaatiotaulukko alustetaan johonkin järjestykseen. Esimerkiksi:
cJarjestys nimen_mukaan
           +-----+ 
  max_koko |  7  |         0  1  2  3  4  5  6 
  indekseja|  3  |       +--------------------+ 
 *indeksit |  o--+------>| 0| 1| 2| ?| ?| ?| ?|
           +-----+       +--------------------+
                          ^   ^
                      a---+   +----b 
Tämän jälkeen lajitteluohjelmalle annetaan lajiteltavaksi taulukko nimen_mukaan.indeksit. qsort välittää vertailualiohjelmalle verrattavaksi osoittimet a ja b. Meidän täytyy siis katsoa miten suhtautuvat toisiinsa nimet
	"kerho- >jasenet[*(int *)a]- >nimi"
	"kerho- >jasenet[*(int *)b]- >nimi"
Tämän homman hoitaa esimerkiksi strcmp - funktio, joka tosin järjestää skandit hieman väärään paikkaan, mutta olkoon tällä kertaa. Aliohjelma on helppo kirjoittaa uusiksi, mikäli ominaisuus haittaa enemmän. Vertailu on case- sensitive, eli Zorro < aapinen!

Jos haluamme lajittelusta aliohjelman, joka osaa järjestää minkä kentän mukaan tahansa, täytyisi kentän numero saada välitettyä vertailu- aliohjelmalle! Tätä mahdollisuutta qsort ei tarjoa, joten joudumme keksimään jonkin toisen ratkaisun.

22.3.5 Aliohjelmien yhteiset muuttujat

Haluamme lisäksi säilyttää koko kerhorekisterin läpi säilyneen ominaisuuden, että meillä olisi mahdollista rinnakkain käsitellä useita eri kerhoja (!), joten myös kerho pitäisi välittää tavalla tai toisella vertailu- aliohjelmalle.

Aliohjelmien väliseen tiedonvälitykseen tehdään tietue, johon kasataan kaikki yhteisesti tarvittavat muuttujat. Ongelmassamme niitä on kentan_nro ja osoitin käyttämämme kerhon jasenet - taulukon alkuun. Siis ne tietueeseen. Seuraavasta esimerkistä puuttuvat saantimetodit, joten se on vain hahmotelma:

	typedef struct {   /* Lajittelussa apuna käytettävä tietue.                 */
	  int kentan_nro;               /* Kenttä jonka mukaan lajitellaan.         */
	  cKerho *kerho;                /* Lajiteltava kerho                        */
	} tVertaa;                      /*                                          */
	
	static tVertaa VERTAA;          /* Tiedonvälitys lajittele - > vertaile      */
	
	int vertaile(const void *a, const void *b)
	{
	  string s1,s2;
	  s1 = VERTAA.kerho->anna(*(int *)a).kentta_jonoksi(VERTAA.kentan_nro,);
	  s2 = VERTAA.kerho->anna(*(int *)b).kentta_jonoksi(VERTAA.kentan_nro,);
	  return (strcmp(s1.c_str(),s2.c_str());
	}
	
	void cKerho::lajittele(cJarjestys *jarj,int kentan_nro)
	{
	  int j,ind,max_ind;
	
	  if ( (kentan_nro < 0) || !jarj- >jarjesta ) return;
	
	  if ( jarj- >Indekseja() == 0 ) { /* Alustetaan perm. taulukko */
	    for (j=0; j<jasenia; j++) 
	       jarj- >lisaa(j);
	  }
	
	  VERTAA.kentan_nro = kentan_nro;
	  VERTAA.kerho      = this;
	
	  qsort( (void *)(jarj- >Indeksit()), /* Lajiteltava taulukko                 */
	         jarj- >Indekseja(),          /* Taulukon alkioiden lukumäärä         */
	         sizeof(jarj- >anna(0)),      /* Yksittäisen alkion koko              */
	         vertaile);                  /* Vertailualiohjelman nimi             */
	}

lajittele - aliohjelma tekee tarvittavat alustukset, mm. laittaa permutaatiotaulukon johonkin järjestykseen (tässä tapauksessa ehdollisesti, eli jos taulukolla jo on järjestys, ei sitä sotketa), laittaa arvot yhteisille muuttujille ja kutsuu qsort - aliohjelmaa tarvittavilla parametreilla.

Tehtävä 22.178 Etsimisen tuloksen lajittelu

Tarvitseeko lajittele - aliohjelma muutoksia, jotta se voisi lajitella myös etsimisen tuloksena saadun taulukon? Miten etsimisen tulos lajiteltaisiin etsimisen päätteeksi?

Tehtävä 22.179 Selailu puhelinnumeron mukaan

Miten käyttäjä voisi selata koko kerhon jäsenistöä puhelinnumeron mukaisessa järjestyksessä?

Tehtävä 22.180 Vertailu isot ja pienet samaistaen

Mikäli haluttaisiin järjestys, jossa aapinen < Zorro, pitäisi vertailun ajaksi isot ja pienet kirjaimet samaistaa. Ongelmaa voitaisiin ratkaista seuraavalla aliohjelmalla
	int vertaile(const void *a, const void *b)
	{
	  string s1,s2;
	  s1 = VERTAA.kerho->anna(*(int *)a).kentta_jonoksi(VERTAA.kentan_nro,);
	  s2 = VERTAA.kerho->anna(*(int *)b).kentta_jonoksi(VERTAA.kentan_nro,);
	  jono_isoksi(s1); jono_isoksi(s2);
	  return (strcmp(s1.c_str(),s2.c_str());
	}
	
Mitä vikaa aliohjelmassa kuitenkin on?

22.4 Oikeaoppinen lajittelu

Käytännössä lajittelusta on hyvin tarkat säännöt. Esimerkiksi kirjaston lajittelusääntöjen mukaan kaikki välimerkit samaistetaan. Saksalaisessa lajittelussa Ä:t samaistetaan A kirjaimeen ja englantilaisessa McAnkka ja MacAnkka lajitellaan samaan kohtaan.

Näiden sääntöjen kirjoittaminen vertaile aliohjelmaan hidastaisi lajittelua huomattavasti. Käytännössä ongelma ratkaistaan siten, että lajittelussa käytetään avaimia, jotka muodostetaan ennen lajittelun alkua.

Esimerkiksi tyypissä Jasen_tyyppi voisi olla yksi ylimääräinen tietue, joka toimisi avaimena. Mikäli lajittelu tehdään nimen mukaan, muodostetaan nimestä lajittelusäännöt täyttävä avain tähän kenttään.

	saksa:  Äystö Yrjö   - >  AYSTO YRJO
	        Gauß Karl F  - >  GAUSS KARL F
	
	suomi:  Äystö Yrjö   - >  \YST] YRJ] (koska ASCIIssa XYZ[\] ) 
Mikäli lajittelu tehtäisiin sotun mukaan, muodostettaisiin sotu- kentästä avain siten, että vuosiluku siirrettäisiin ensimmäiseksi, kuukausi seuraavaksi jne.
	010245- 123U          - >  450201- 123U
	020347- 123T          - >  470302- 123T 

22.4.1 vertaile

Itse vertailu olisi
	int vertaile(const void *a, const void *b)    
	{
	  return strcmp(VERTAA.kerho->anna(*(int *)a).avain,
	                VERTAA.kerho->anna(*(int *)b).avain);
	}

22.4.2 lajittele

lajittele aliohjelmaan lisättäisiin lauseet avaimien muodostamiseksi:
	  ...
	  for (j=0; j<jarj- >Indekseja(); j++) 
	    anna(jarj- >anna(j)).tee_avain(kentan_nro);
	   
	  qsort(...

22.4.3 alusta_jarjestys

Kirjainten samaistus suoritettaisiin siten, että olisi käytössä taulukko, jossa olisi kunkin kirjaimen ASCII- koodiarvon kohdalla kirjaimelle uusi koodi, sen mukaan mihin kohti kirjain lajitellaan. Voisimme esimerkiksi numeroida:
	
	0x20  = välimerkit
	0x30 = '0'
	0x31 = '1'
	..
	0x39 = '9'
	0x41 = 'A' ja 'a' (saksassa myös 'Ä' ja 'ä')
	0x42 = 'B' ja 'b'
	... 

Tätä varten tarvitsemme luokan:

	
	cJarjestys_avain {   /* Avaimen muodostuksessa käytettä tyyppi             */
	  char avainarvo[MERKKEJA];     /* Miten kukin kirjain muuttuu              */
	  void alusta();
	public:
	 cJarjestys_avain() { alusta(); )
	 int Avainarvo(int i) { return avainarvo[i]; }
	} ;      
	
	static cJarjestys_avain JARJESTYS;

Taulukko alustettaisiin seuraavasti:

	void cJarjestys_avain::alusta() {
	 avainarvo[(unsigned char) ','] = 0x20;
	 avainarvo[(unsigned char) ' '] = 0x20;
	...
	 avainarvo[(unsigned char) 'A'] = 0x41;
	 avainarvo[(unsigned char) 'a'] = 0x41;
	 avainarvo[(unsigned char) 'B'] = 0x42;
	 avainarvo[(unsigned char) 'b'] = 0x42;
	...
	 avainarvo[(unsigned char) 'Ä'] = 0x5c;
	 avainarvo[(unsigned char) 'ä'] = 0x5c;
	... 
	}

Ehkä on kuitenkin helpompi käyttää muutamaa makroa:

	
	#define A_1 (mika,miksi) avainarvo[(unsigned char)mika] = miksi
	#define A_A (mi,mp,miksi) A_1(mi,miksi); A_1(mp,miksi)
	
	/****************************************************************************/
	void cJarjestys_avain::alusta() {
	{
	  int i;
	
	  for (i=0; i<MERKKEJA; i++) A_1(i,0x20); /* Kaikki tuntemat. merkit välil. */
	
	  A_1('0',0x30); A_1('1',0x31); A_1('2',0x32); A_1('3',0x33); A_1('4',0x34); 
	  A_1('5',0x35); A_1('6',0x36); A_1('7',0x37); A_1('8',0x38); A_1('9',0x39); 
	
	  A_A('A','a',0x41);  A_A('B','b',0x42);  A_A('C','c',0x43);  
	  A_A('D','d',0x44);  A_A('E','e',0x45);  A_A('F','f',0x46);
	  A_A('G','g',0x47);  A_A('H','h',0x48);  A_A('I','i',0x49);
	  A_A('J','j',0x4a);  A_A('K','k',0x4b);  A_A('L','l',0x4c);
	  A_A('M','m',0x4d);  A_A('N','n',0x4e);  A_A('O','o',0x4f);
	  A_A('P','p',0x50);  A_A('Q','q',0x51);  A_A('R','r',0x52);
	  A_A('S','s',0x53);  A_A('T','t',0x54);  A_A('U','u',0x55);
	  A_A('V','v',0x56);  A_A('W','w',0x57);  A_A('X','x',0x58);
	  A_A('Y','y',0x59);  A_A('Z','z',0x5a);
	  A_A('[sinvcircumflex]','',0x59);  
	  A_A('Å','å',0x5b);  A_A('Ä','ä',0x5c);  A_A('Ö','ö',0x5d);
	
	}
	#undef A_1
	#undef A_A

Avain muodostetaan nyt JARJESTYS.avainarvo - taulukkoa käyttäen. Kuitenkin erikoistapaukset kuten Mac ja Mc täytyisi käsitellä ensin erikseen vaikkapa Mc - >Mac.

Tehtävä 22.181 A_1 ja A_A

Kirjoita auki muutama makrojen A_1 ja A_A esiintymä.

22.4.4 tee_avain

Seuraava on hahmotelma siitä, miten cJasen voisi alustaa avaimensa. Jos on käytössä "viisaat kenttä- luokat", niin silloin tietenkin kukin kenttä itse osaa muuttua avaimeksi.
	/****************************************************************************/
	void cJasen::tee_avain(int kentan_nro)    
	{
	  void *p; char s[20]; unsigned char *a; 
	  int i; double d; 
	  int tyyppi;
	
	  i = sizeof(avain);
	  if ( i < 20 ) {
	    printf("TEE AVAIN: Liian pieni avainkenttä!\n");
	    return;
	  }
	
	  tyyppi = kentan_tyyppi(kentan_nro);
	  p      = kentan_osoite(kentan_nro);
	
	  switch (tyyppi) {
	    case Tjono:
	      kopioi_jono(N_S(avain),p);
	      poista_tyhjat(avain);
	      for (a=(unsigned char *)avain; *a; a++) {
	        *a = JARJESTYS.avainarvo[*a];
	      }
	      poista_tyhjat(avain);
	      return; 
	
	    case Tsotu:
	      kopioi_jono(N_S(s),p); liita_jono(N_S(s),"      ");
	      kopioi_jono(N_S(avain),s);
	      avain[0] = s[4];  /* Vaihdetaan pv ja vuosi keskenään! */
	      avain[1] = s[5];
	      avain[4] = s[0];
	      avain[5] = s[1];
	      return;
	
	    case Tint: 
	      i = *(int *)p;
	      sprintf(avain,"%+05d",i);
	      if ( avain[0] == '- ' ) avain[0]='+'- 1; /* jotta -  < + */
	      return;
	
	    case Tdouble: 
	      d = *(double *)p;
	      sprintf(avain,"%+015.7lf",d);
	      if ( avain[0] == '- ' ) avain[0]='+'- 1; /* jotta -  < + */
	      return;
	
	    default:
	      printf("TEE AVAIN: Ei ole kenttää %d!\n",kentan_nro); 
	      return;
	      
	  }
	}

Tehtävä 22.182 tee_avain

Miksi kokonais- ja reaaliluvut käsiteltiin kuten edellä eikä käyttäen aliohjelmaa kentta_jonoksi?
"Kommentoi" (eli miksi tehdään?) tee_avain aliohjelman jokainen rivi.
Miten MacAnkka ja McAnkka käsiteltäisiin?
Miten käsiteltäisiin Gauß?

22.5 Haku epäyhtälö - ehdoilla

Miten toteuttaisimme haun sillä tavoin, että haku olisi mahdollista myös ehdoilla:
	>=50
	!=*ankka*
	<=Kassinen
	==               eli etsitään tyhjää kenttää
Puuttuuko ohjelmastamme paljon, jotta tämä voitaisiin tehdä? Mistä yleensä on kyse?

Ensin pitäisi erottaa annetusta hakuehdosta onko kyseessä normaali haku vaiko epäyhtälöhaku. Miten tämä tehtäisiin? Tutkitaan onko hakuehdon alussa jokin merkkijonoista

	"==", "!=", "<=" "<" ">" ">="

Mitenkä tutkittaisiin? Esimerkiksi:

	if ( strstr(maski,"==") == maski )...
	else if ( strstr(maski,"!=") == maski )...
	... 

22.5.1 EHDOT - taulukko (kerhoets.c)

Tällainen vertailu sopii, mikäli erikoisehtoja olisi hyvin vähän. Nyt niitä kuitenkin on suhteellisen monta, joten on ehkä helpompi tehdä taulukko, jossa on ehdot ja mitä ehdoilla tehdään:
	
	/****************************************************************************/
	typedef enum { 
	 YHT,
	 ERIS,
	 PIEN,
	 PIENYHT,
	 SUUR,
	 SUURYHT
	} Vertailu_oper_tyyppi;
	
	typedef struct {
	  char *ehto;
	  int  pit;
	  Vertailu_oper_tyyppi kasky;
	} Vertailu_tyyppi;
	
	static Vertailu_tyyppi EHDOT [] = {
	  { ""  , 0, YHT},
	  { "==", 2, YHT},
	  { "!=", 2, ERIS},
	  { "<=", 2, PIENYHT},
	  { "<" , 1, PIEN},
	  { ">=", 2, SUURYHT},
	  { ">" , 1, SUUR},
	  { NULL, 0, YHT}
	};

22.5.2 tutki_tasmaako

Metodissa etsi_indeksit muutetaan wildmat - kutsu kutsuksi tutki_tasmaako. Aliohjelma tarvitsee muutaman lisäparametrin, jotta tarpeen vaatiessa osataan muuttaa kenttiä avaimiksi. Miksikö avaimiksi? No tietysti siksi, että avain rakennettiin siten, että aakkosjärjestys, sotujen järjestys yms. pitävät paikkansa.

Aliohjelman aluksi etsitään onko kyse jostakin erikoisoperaatiosta. Mikäli ei ole toimitaan kuten ennenkin. Mikäli on kyse jostakin erikoisesta operaatiosta, poistetaan hakumaskin alusta operaatio.

	
	/****************************************************************************/
	int                       /* 1 = ei täsmää                                  */
	cJasen::tutki_tasmaako(   /* 0 = täsmää                                     */
	  const char *maski      ,/* s   Maski muutettuna isoiksi kirjaimeksi       */
	  int knro                /* s   Kentän numero.                             */
	)    
	{
	  int i,tulos,ehto_ind=0; const char *m; cJasen mjasen;
	  char ikentta[80]; // Kentän knro sisältö isoksi muutettuna merkkijonona
	
	  kopioi_jono(N_S(ikentta),kentta_jonoksi(knro));
	
	  for (i=1; EHDOT[i].ehto; i++) 
	    if ( strstr(maski,EHDOT[i].ehto) == maski ) { 
	      ehto_ind = i; break; 
	    }
	
	  m = maski+EHDOT[ehto_ind].pit;
	
	  switch ( EHDOT[ehto_ind].kasky ) {
	
	    case YHT:     
	      return wildmat(ikentta,m);
	
	    case ERIS:    
	      return !wildmat(ikentta,m);
	
	    case PIEN:
	    case PIENYHT:
	    case SUUR:
	    case SUURYHT:
	      mjasen.sijoita(knro,m);
	      mjasen.tee_avain(knro);
	      tee_avain(knro);
	      tulos = strcmp(Avain(),mjasen.Avain());
	      switch ( EHDOT[ehto_ind].kasky ) {
	        case PIEN:    return !(tulos<0);
	        case PIENYHT: return !(tulos<=0);
	        case SUUR:    return !(tulos>0);
	        case SUURYHT: return !(tulos>=0);
	      }
	
	    default:      
	      return 1;
	  }
	}

Tehtävä 22.183 tutki_tasmaako

Miksi EHDOT taulukossa "<=" on ennen "<" operaatiota?
"Kommentoi" metodin tutki_tasmaako jokainen lause!

22.6 enum

Tyypissä Vertailu_oper_tyyppi "luotiin" luettelo vakioita, joille ei kuitenkaan tarvinnut itse antaa arvoa. Sama olisi voitu tehdä myös vakioilla tai #defineillä:
	// Vakioilla                            // #definellä
	const YHT     = 0;                      #define YHT      0
	const ERIS    = 1;                      #define ERIS     1
	const PIEN    = 2;                      #define PIEN     2
	const PIENYHT = 3;                      #define PIENYHT  3
	const SUUR    = 4;                      #define SUUR     4 
	const SUURYHT = 5;                      #define SUURYHT  5 

Oman tyypin Vertailu_oper_tyyppi luomisella on kuitenkin se etu, että voidaan tehdä myös muuttuja joka on samaa tyyppiä. Tällaiseen muuttujaan ei voi sijoittaa muita arvoja (vaikka muuttuja olisikin todellisuudessa tyyppiä int). Lisäksi useat debuggerit näyttävät luettelotyyppisen muuttujan arvon nimellä eikä mitään sanomattomalla kokonaisluvulla.

Hyvässä ohjelmassa on jokaista selvästi eri luettelomaista tyyppiä kohti tehty oma luettelo ja käytetään vain ko. luettelotyypin tyyppisiä muuttujia. Esimerkiksi aliohjelmien virhepalautukset voisivat olla luettelotyyppiä. Valitettavasti luettelotyypin arvon tulostaminen tulostaa vain kokonaisluvun. Usein tulostuksia varten joudutaan tyypin rinnalle tekemään vähän EHDOT- taulukon kaltainen taulukko, jolla tyyppejä voidaan muuttaa selväkielisiksi ja päinvastoin.

Tehtävä 22.184 Viikonpäivät

Esitä sekä #define että enum avulla miten symboleille Su, Ma, Ti... saataisiin arvot 0,1,2...

Tehtävä 22.185 Pelikortit

Esitä sekä #define että enum avulla miten symboleille Pata, Hertta, Risti ja Ruutu saataisiin arvot 0,1,2,3. Määrittele sitten cKortti- luokka, jossa on kortin maa ja arvo. Määrittele vielä luokka cKorttipakka, jossa on 52 korttia jotka ovat tyyppiä cKortti.

22.7 Lopuksi

Tässä luvussa ohjelma on kerralla kehittynyt varsin paljon! Lukijan ei kuitenkaan pidä tälläkään kertaa erehtyä luulemaan, että kaikki olisi tehty kerralla ja oikein. Ohjelmaan on jälleen lisätty aina pienin mahdollinen lisä kerrallaan ja tämä on testattu.

Näin hitaasti edeten koko kokonaisuus on saatu valmiiksi. Välillä on jouduttu muuttamaan joidenkin aliohjelmien parametreja, jotta ne kävisivät myös jonkin toisen ongelman ratkaisemiseen.

Jos kaikki tässä luvussa esitetty yritettäisiin lisätä kerralla ohjelmaan, tulisi sen testaamisesta varsinainen ongelma: Kuka muistaa testata kaikki kohdat joihin lisäykset vaikuttivat?

Samoin tehdyistä ratkaisuista alkaa näkyä lävitse eräs ohjelmoinnin tärkeimpiä ominaisuuksia: pyrkimys yleisyyteen ja uudelleen käytettävyyteen. Nämä ovat olio- ohjelmoinnin tunnusmerkkejä.


previous next Title Contents Index