Urejanje podatkov
V tokratni lekciji si bomo ogledali preprost algoritem
(računski postopek) za urejanje podatkov. Urejanje je eno
najpogostejših opravil v računalniški praksi (urejanje po
abecedi, urejanje po velikosti, ipd.) in znanih je kar nekaj
algoritmov za urejanje.
Obravnavali bomo najpreprostejši primer urejanja: dana je tabela
uredimo po velikosti
Lahko bi tudi urejali tudi nize znakov po abecedi, ali pare
števil v leksikografskem redu. Pomembna predpostavka je, da so
podatki shranjeni v tabeli, tako da lahko do poljubnega podatka
dostopamo hitro. (Če bi bili podatki v seznamu, ali v podatkovni
bazi, ki je prevelika, da bi jo shranili v pomnilnik, bi morali
uporabljati postopke, ki jih tu ne bomo obravnavali.)
a
celih števil, ki jih želimo urediti po velikosti. Na
primer, števila
5 9 8 2 3 1 7 6 0 4
0 1 2 3 4 5 6 7 8 9
Mešanje
Preden se posvetimo urejanju napišimo še metodo, ki naredi prav
nasprotno od urejanja. Potrebujemo jo, da bomo lahko
preizkusili, ali metoda za urejanje deluje. Dana je tabela
števil
ki jo želimo dobro premešati. Kako to storimo? Dostikrat se
pojavi ideja za naslednji algoritem (take algoritme sem dejansko
videl v raznih programih in avtorjem programov se ni dalo
dopovedati, da so slabi):
Tako nekako mešamo tarok karte, le da postopka ne ponovimo
100000-krat. Vendar pa je to zelo slab računski postopek:
Ta algoritem najprej izbere naključni element in ga postavi v
a[0] a[1] a[2] ... a[k]
algoritem zmešaj_tabelo(a): N = 100000 ponovi N-krat: izberi dva naključna elementa tabele a in ju zamenjaj |
- Če ima tabela
a
več kotN
elementov, bo slabo premešana. - Če ima tabelo
a
zelo malo elementov, ni nobene potrebe, da premesamo 100000-krat. - Po nepotrebnem bomo isti element z veliko verjetnostjo prestavili večkrat.
algoritem zmesaj_tabelo(a): n = a.length - 1; ponavljaj za i od 0 do n: j := nakljucno stevilo med i in n; zamenjaj a[i] in a[j]; |
a[0]
. Izmed preostalih elementov izbere naključni
element in ga postavi v a[1]
. Izmed preostalih
elementov izbere naključni element in ga postavi v
a[2]
. In tako naprej. Algoritem opravi natanko toliko
dela, kot je treba:
- Na i-to mesto zapiše naključno izbrani element samo enkrat.
- Vedno opravi ravno toliko zamenjav, kot je potrebno.
- Algoritem zagotavlja, da je na koncu tabela naključno premešana.
java.util
najdemo razred Random
,
s katerim dobimo naključna števila. Najprej naredimo objekt
razreda Random
, nato pa generiramo naključna
cela števila z metodo nextInt(int
n)
.
public static void zmesaj(int[] a) { Random r = new Random(); for (int i = 0; i < a.length; i = i + 1) { int j = i + r.nextInt(a.length - i); int t = a[i]; a[i] = a[j]; a[j] = t; } } |
Naloga 1: Verjetnostno določanje števila π
Kolikšna je verjetnost, da je naključno izbrana točka v
kvadratu vsebovana v krogu, ki je včrtan temu kvadratu?
Napiši metodo
double oceni_pi(int n)
, ki deluje
takole: n
-krat izbere naključno točko v kvadratu in
prešteje, kolikokrat je točka vsebovana v krogu, včrtanem v
kvadrat. Iz dobljenega števila oceni vrednost števila πNamig
Metode za izbiranje realnih števil so opisane v
dokumentaciji o razredu
java.util.Random
.
Naloga 2: Izbiranje točke v krogu
Napiši metodo
double[] nakljucna_krog()
, ki
izbere naključno točko (x, y) v krogu s polmerom
1 in središčem v (0,0). Metoda naj vrne
koordinati izbrane točke v tabeli velikosti 2.
Namig
Pozor: denimo, da naključno izberemo polarne
koordinate, polmer
Take točke niso izbrane naključno, ker bo
preveč izbranih v bližini središča kroga.
p
med 0.0
in r
in kot fi
med 0.0
in 2.0*Math.PI
,
nato pa jih pretvorimo v kartezične koordinate:
x = p * cos(fi)
y = p * sin(fi)
Dodatna naloga: Denimo, da želimo izbrati naključno
točko (x,y) znotraj enotskega kroga s središčem v
(0,0) tako, da izberemo njene polarne koordinate
r in φ. Kakšna je ustrezna porazdelitev za
r, da bo točka enakomerno izbrana glede na kartezične
koordinate?
Urejanje z izbiranjem
Študenti matematike radi igrajo tarok (tudi med predavanji).
Zagotovo zna vsak študent matematike urediti tarok karte po
velikosti. Kako to naredi? Izbere najmanjšo karto in jo
postavi na prvo mesto. Nato izbere drugo najmanjšo karto
in jo postavi na drugo mesto. Nato izbere tretjo
najmanjšo karto in jo postavi na tretjo mesto. In tako naprej.
Temu postopku pravimo urejanje z izbiranjem:
Ta algoritem deluje, a smo ga nekoliko nerodno zapisali. Ko
premislimo, ugotovimo, da ni tako lahko najti i-tega
najmanjšega elementa v tabeli. Lažje pa najdemo najmanjši
element, zato algoritem nekoliko predelamo:
algoritem uredi_z_izbiranjem(a): (*slaba verzija*) n = a.length - 1; ponavljaj za i od 0 do n: j = mesto, na katerem stoji i-ti najmanjši element; zamenjaj a[i] in a[j] |
algoritem uredi_z_izbiranjem(a): n = a.length - 1; ponavljaj za i od 0 do n: j = mesto, na katerem stoji najmanjši izmed a[i], a[i+1], ..., a[n]; zamenjaj a[i] in a[j] |
Naloga 3
Za to nalogo potrebujemo enega študenta in ene tarok karte.
Študenti matematike kart ne urejajo točno tako,
kot smo zapisali v algoritmu
uredi_z_izbiranjem
,
ker ne zamenjajo i-te in j-te karte, ampak
vrinejoj-to karto na i-to mesto.
Temu postopku bi rekli urejanje z vrivanjem.
Tega postopka za urejanje tabel v Javi ne uporabljamo, saj
je dokaj zamuden, ker moramo prestaviti vse elemente od
i-tega do j-tega za eno mesto v desno, da
lahko nato vrinemo j-tega na i-to mesto.
Demonstriraj, kako bi uredil 12 kart z algoritmom
uredi_z_izbiranjem
. Pazi, da boš zvesto sledil(a)
algoritmu, kar pomeni, da kart ne smeš vrivati, ampak moraš
vedno zamenjati ustrezni dve karti.
Algoritem prevedemo v Javo:
public static void uredi_z_izbiranjem(int[] a) { for (int i = 0; i < a.length; i = i + 1) { int j = i; for (int k = i; k < a.length; k = k + 1) { if (a[k] < a[j]) { j = k; } } int t = a[i]; a[i] = a[j]; a[j] = t; } } |
Zanima nas, kako hitro deluje urejanje z izbiranjem. Koliko časa
potrebuje metoda
uredi_z_izbiranjem
, da uredi tabelo
dolžine n? Ali nekatere tabele uredi hitreje kot druge? Ali
hitreje uredi tabelo, ki je že urejena, ali tabelo, ki je
naključno premešana?
Z odgovori na taka vprašanja se ukvarja veja računalništva, ki
se imenuje računska zahtevnost algoritmov. Čas
izvajanja metode, se imenuje časovna zahtevnost. Čas
je seveda odvisen od velikosti argumentov in argumentov samih.
Časovno zahtevnost lahko dobimo na dva načina: analiziramo
metodo in preštejemo, koliko korakov bo opravila, ali pa
preprosto izmerimo, koliko časa potrebuje in narišemo graf
odvisnosti časa od velikosti tabele.
Meritve je lažje opraviti. Povejo nam, kako se metoda zares
obnaša v praksi na pravem računalniku. S teoretično analizo pa
predvsem razumemo, zakaj ima metoda dano časovno
zahtevnost. Včasih po opravljeni analizi časovne zahtevnosti
dobimo idejo, kako je mogoče algoritem izboljšati. Tokrat se
posvetimo teoretični analizi časovne zahtevnosti
uredi_z_izbiranjem
, na vajah po bomo izmerili koliko
časa potrebuje metoda.
Metoda
uredi_z_izbiranjem
je dvojna for zanka. Zunanja
zanka šteje s števcem i
od 1 do n = a.length -
1
, notranja pa s števcem j
od i
do
n
. Znotraj notranje zanke se izvedeta največ dve
osnovni operaciji (eno primerjanje in eno prirejanje). Ob koncu
zunanje zanke se izvedejo tri osnovne operacije (tri
prirejanja). To seštejemo:
(n * 2 + 3) + ((n-1) * 2 + 3) + ... + (1 * 2 + 3) =
2 * (n + (n-1) + ... + 2 + 1) + n * 3 =
n * (n + 1) + n * 3 = n * (n + 4) =
n2 + 4 * n
Urejanje z izbiranjem torej potrebuje kvečjemu n2 + 4
* n korakov. Korake smo šteli zelo približno, saj na primer
nismo šteli korakov, ki so potrebni za primerjanja v for zanki
in povečevanje števcev. Pri ugotavljanju časovne zahtevnosti je
običajno, da ne štejemo vsakega najmanjšega koraka. Pravilno pa
moramo prešteti, koliko korakov ima vsaka for in while zanka,
ter kolikokrat se pokliče kaka metoda.
2 * (n + (n-1) + ... + 2 + 1) + n * 3 =
n * (n + 1) + n * 3 = n * (n + 4) =
n2 + 4 * n
Časovna odvisnost urejanja z izbiranjem je pravzaprav enaka
C * (n2 + 4 * n)
kjer je C neznana konstana, ki pove koliko osnovnih korakov se
zgodi v eni sekundi. To konstanto bi lahko izmerili s poskusi, a
je odvisna od računalnika, na katerem poganjamo program. Zato
ponavadi namesto neznane konstante C zapišemo časovno odvisnost
takole:
O(n2)
To preberom "veliki O n kvadrat" in pomeni, da je časovna
odvisnost manjša od n2 krat neka neznana konstanta.
Zanemarili smo tudi linearni člen, saj je bistveno manjši od
n2.
Naloga 5
Napiši program
Meritev.java
, ki meri, kako hitro
deluje urejanje z izbiranjem. Program z ukazne vrstice
sprejme štiri argumente:
> java Meritev a b c d |
a
= velikost najmanjše tabele, za katero opravimo meritevb
= velikost največje tabele, za katero opravimo meritevc
= korak, s katerim povečujemo velikost tabeled
= ime datoteke, v katero se zapišejo meritve
java Meritev 0 20000 1000 podatki.dat
bi
opravil meritev za tabele velikosti 0, 1000, 2000, ...,
19000, 20000 in rezultate zapisal v datoteko
podatki.dat
.
Za merjenje časa uporabi razred
Uporabi jo takole:
Podatke zapiši tako, da jih boš lahko narisal z Mathematico.
Predlagam naslednji format zapisa:
Poleg tega, da podatke zapisuješ v datoteko, jih izpisuj še
na ekran, da bomo videli, kaj se dogaja.
V Mathematici lahko narišeš graf odvisnosti časa od
velikosti takole:
Kako bi preizkusil, ali je časovna odvisnost res
O(n2)?
Stoparca.java
:
Stoparca.java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import java.util.*; public class Stoparca { private long zacetek; public Stoparca() { zacetek = (new GregorianCalendar()).getTimeInMillis(); } public void start() { zacetek = (new GregorianCalendar()).getTimeInMillis(); } public double poglej() { long konec = (new GregorianCalendar()).getTimeInMillis(); return (double)(konec - zacetek)/1000.0; } } |
Stoparca s = new Stoparca(); ... s.start(); uredi_z_izbiranjem(a); double cas = s.poglej(); // cas v sekundah ... |
{ { velikost1, cas1 }, { velikost2, cas2 }, ... { velikostN, casN } } |
podatki = Get["podatki.dat"]; ListPlot[podatki]; |
Urejanje z vstavljanjem
Urejanje z vstavljanjem bomo spoznali na vajah.
Naloga 6: Urejanje z vstavljanjem
V tej vaji se bomo naučili urejati po postopku, ki se
imenuje urejanje z vstavljanjem.
Imamo tabelo celih števil, na primer:
Začnemo pri drugem elementu in ga vstavimo na pravo mesto:
Nato vstavimo tretji element na pravo mesto:
Tako vstavimo še ostale elemente, vsakega na pravo mesto:
Algoritem opišemo takole:
Napiši metodo
4 1 2 1 5 6 3
1 4 2 1 5 6 3
1 2 4 1 5 6 3
1 1 2 4 5 6 3
1 1 2 4 5 6 3
1 1 2 4 5 6 3
1 1 2 3 4 5 6
algoritem uredi_z_vstavljanjem(a): n = a.length - 1; ponavljaj za i od 1 do n: j = i; t = a[j] ponavljaj dokler je j > 0 in a[j-1] > t: a[j] = a[j-1] j = j -1 konec a[j] = t konec |
uredi_z_vstavljanjem
, ki uredi dano tabelo
celih števil z po metodi urejanja z vstavljanjem.
Izračunaj časovno zahtevnost urejanja z vstavljanjem.
- Koliko korakov potrebuje metoda
uredi_z_vstavljanjem
za tabelo velikostin
v najslabšem primeru? Kateri primer je to? - Koliko korakov potrebuje metoda
uredi_z_vstavljanjem
za tabelo velikostin
v najboljšem primeru? Kateri primer je to?