Föreläsning 3.1: Datastrukturer, en översikt

Föreläsning 3.1: Datastrukturer, en översikt
Hittills har vi i kursen lagt mycket fokus på algoritmiskt tänkande. Vi har inte egentligen ägna så
mycket uppmärksamhet åt det andra som datorprogram också består, nämligen data. Man inser
ganska snabbt att man inte kommer någon vidare stans utan variabler i sitt program. Nu är det dags
att ta ett steg vidare med denna insikt. På den här föreläsningen skall jag ge en översikt över på
vilka sätt man kan lagra data. Man brukar kalla detta för datastrukturer.
Vilka datastrukturer har vi pratat om hittills? Egentligen kan man se de datatyper som vi träffat på
som väldigt grundläggande "datastrukterer".
Integer / Float
Character
Boolean
64 bitar = 8 byte
32 bitar = 4 byte
1 byte
bit
String
char
char
char
char
char
Med dessa bilder försöker jag förmedla att olika datatyper tar olika mycket plats. Vi kan börja med
Character som borde vara tämligen enkel att förstå eftersom den representerar ett enskilt tecken t.ex.
'A'. Nu visar det sig att det finns många olika sätt att göra detta på och det kan faktiskt behövas
olika mycket minne för olika tecken. Om man dock håller sig till de allra vanligaste tecknen, d.v.s
'A'-'Z', 'a'-'z', '.' '!' '?', '0'-'9' m.fl. så räcker det med 1 byte minne för att kunna representera alla
dessa. Vad är då en byte? Tja, det är 8 bitar. En bit är det vi kallar den minsta enheten minne i
datorn, d.v.s. en etta eller en nolla. Rent tekniskt så brukar man säga att ett symboliserar "ström på"
och noll står för "ström av". Tänk dig en glödlampa: på = 1, av = 0. Med endast en sån lampa kan
man inte symbolisera mer än två saker, men tänk om man hade fler?
0
1
0
1
0
1
1
1
Med 8 lampor kan man få till ganska många kombinationer. Vi kan faktiskt representera 256 olika
situationer med en sådan uppsättning! Allt vi behöver göra är att bestämma vilken kombination som
skall betyda vilket tecken. Ganska smart! Just denna kombination skulle faktiskt motsvara tecknet
'W'.
Man kan ju även se sekvensen ovan som ett heltal i basen två. I vårt "vanliga" talsystem, d.v.s basen
tio, har vi ju ental, tiotal, hundratal och så vidare. För att t.ex. representera 356 tar vi 3 * 100 + 5 *
10 + 6 * 1. Precis på detta sätt fungerar det i alla talbaser. I basen två har man då istället för 1, 10
och 100, faktorerna 1, 2, 4, 8, o.s.v. D.v.s de tal som man får när man tar 2⁰, 2¹, 2², 2³... Om man nu
tänker på samma sätt som med exemplet i basen tio men för lamporna får vi:
0*2⁷ + 1*2⁶ + 0*2⁵ + 1*2⁴ + 0*2³ + 1*2² + 1*2¹ + 1*2⁰ = 87
(Alltså stod 'W' på den 87:e raden i teckentabellen...)
Det är självklart inte så att det sitter en massa glödlampor i en dator. Men det finns små
minneselement som kan lagra antingen 1 eller 0 på precis detta sätt. Klumpar vi ihop dessa så kan vi
lagra precis vad vi vill. Vi har här sett ett exempel på hur 8 bitar (= 1 byte) kan användas för att
antingen lagra ett tecken eller ett tal. Det beror helt på vilken datatyp som vi anser att just dessa
minnesbitar har!
För just datatypen Integer vill man dock vanligtvis ha mer än 1 byte eftersom tal vanligtvis kan bli
mycket större än vad man kan representera med 1 byte. Om vi tar 4 byte (32 bitar), och dessutom
låter en av bitarna tala om huruvida talet skall vara negativt eller positivt så kan man representera
tal från ca minus två miljarder till ca plus två miljarder. Har man större eller mindre tal än så så får
man helt enkelt använda sig av en annan datatyp.
För reella tal inser man direkt att det blir problem. Även om vi begränsar oss till -N...+N så finns
det ju ändå oändligt många tal däremellan om vi tänker på alla "decimaltal". För att representera
oändligt många tal skulle det behövas oändligt mycket minne, något som datorer tyvärr inte har. Det
enda rimliga är att helt enkelt klippa av en bit minne och sedan göra det bästa av situationen. Exakt
hur man representerar ett reellt tal kan vi prata om någon senare gång.
Om vi tittar på datatypen Boolean så är den lite speciell. Den används för att representera sant och
falskt och är praktisk i många logiska sammanhang. Eftersom det bara är två värden som skall
representeras så skulle det räcka med en enda bit för att fixa detta. I praktiken går detta egentligen
inte, eftersom datorers arkitektur inte tillåter att man manipulerar enskilda bitar i minnet. När man
hanterar minnet måste man hantera minst 1 byte, d.v.s 8 bitar. Därför tar själva datatypen boolean 1
byte minne, det är faktiskt bara 1 bit i denna byte som används.
Vad det gäller String så ser man ju att denna egentligen inte är så "enkel" som de övriga. Det är en
s.k. sammansatt datatyp. Vi vet sedan tidigare att String är en samling tecken, och är behändig när
man vill lagra text. Vi kommer tillbaka till sammansatta datatyper strax.
Alla dessa datatyper är toppen när man vill lagra enkla saker. T.ex. ett lotterinummer, veckans ord,
gårdagens maxtemperatur. Vi tänker oss nu en situation där vi vill lagra ett mer komplext data, t.ex.
datat som representerar en person, det kan ju bestå av flera delar:
Namn
Skonummer
Kön
Rutorna här kan få representera storleken på de datatyper som skall representera de olika delarna.
För namnet är String bra, för skonummer duger en Float. Könet behöver bara kunna ha två värden,
en boolean skulle kunna funka. Utan att göra något mer nu så kan vi faktiskt säga att detta är en
datastruktur för en person. Det behöver alltså inte vara krångligare än att man har tre variabler. Det
som är tråkigt är att programmeraren överallt i sitt program behöver komma ihåg att datastrukturen
består av dessa tre. Det är förvisso inte så svårt, men om man har många datastrukturer, med väldigt
många delar och det kanske inte är en själv som har skapat datastrukturen så blir detta väldigt
bökigt - inte omöjligt, men bökigt. Det man egentligen skulle vilja göra är att klumpa ihop dessa tre,
så att man inte kan tappa bort något på vägen. Då skulle man också kunna se det som ett data - datat
för en person. Jag vill förtydliga detta genom att rita en ruta kring dessa tre variabler. Jag vill att
dessa tre skall bilda en ny, sammansatt datatyp, så att jag hädan efter aldrig behöver vara tvungen
att komma ihåg exakt hur den ser ut inuti.
Just detta mantra har ni nu redan hört en gång. Jag vill inte behöva var tvungen att veta de exakta
detaljerna över det jag använder. När vi hörde det sist handlade de om att skapa underprogram, så
man slapp bekymra sig om detaljer i huvudprogrammet - man skapade abstraktion. Vi köra samma
sak här faktiskt, fast vi gör det med data.
Att klumpa ihop data på detta sätt kallas för att man gör en Post (på engelska "record"). Vår nya
datatyp, som vi kan kalla för Person_Type, är alltså en Post med tre delar.
När vi ändå håller på och fantiserar över vilka nya datastrukturer som vi skulle kunna hitta på så kan
vi tänka oss att man skulle vilja föra ett register över olika personer. Man skulle då åter igen kunna
klumpa ihop data som tidigare och skapa en ny form av post som innehåller ett stort antal personer.
Det ända som hindrar oss är egentligen att komma på namnen på dessa delar i posten. Varje del av
posten måste nämligen ha ett namn. Okej, vi skulle kunna köra på "Person1", "Person2", "Person3",
o.s.v. men det visar sig att det finns ett smidigare sätt.
Att klumpa ihop många av samma data är nämligen mycket vanligt. Det händer mycket ofta att man
t.ex. vill lagra etthundra heltal. Eftersom det är så vanligt har man i de allra flesta
programmeringsspråk en annan form av sammansättning som gör att man istället för att namnge
delarna i datastrukturen, kan numrera dem med ett intervall. Detta kallas för ett fält (på engelska
"array"). Vi kan illustrera det så här:
1
Namn
Skonummer
Kön
2
Namn
Skonummer
Kön
3
Namn
Skonummer
Kön
...
N
Namn
Skonummer
Kön
Utöver att det är siffror, eller index som de kallas, finns det en annan skillnad jämt emot posten: det
måste vara samma datatyp på varje index i fältet. Anledningen till detta är att det skall vara lika stort
data på varje plats, eftersom datorn "hoppar" till rätt person genom att multiplicera storleken av en
Person_Type med det indexet man vill komma åt. Man skall alltså inte glömma att en sådan
datastruktur behöver en del minne. Hur mycket? Vi kan ju faktiskt räkna på det. Om vi antar att
namnet är en sträng på 40 tecken, det blir 40 byte. Skonummret är en float á 8 byte, och könet är en
boolean, vilket kostade 1 byte. Allt som allt är detta ca 50 byte. Om N är två miljoner blir detta
alltså sammanlagt ca hundra miljoner byte. Är det mycket? Tja, det beror på vad man jämför med.
Men man ska komma ihåg att ingenting är gratis när man börjar skapa datastrukturer.
Värt att poängter är att jag nu ritade detta fält på ett lodrätt sätt. Men jag hade lika gärna ritat det
vågrätt. Datorer gör ingen egentlig skillnad på detta, utan det får man hålla reda på själv. Om vi
tänker efter en stund så inser vi nu att String självklart också är ett fält. Det är helt enkelt ett annat
exempel på när man vill lagra en sekvens av en viss datatyp. Det spelar faktiskt ingen egentlig roll
vad som lagras i ett fält. Alla fungerar på samma sätt. Vi skulle t.ex. kunna ha ett fält med sju flyttal
för att representera en veckas temperaturer:
18.2 19.0 17.5 13.5 16.0 18.1 16.9
1
2
3 4
5 6 7
Man kan till och med tänka sig en datastruktur som är ett fält, som består av fält. Antag att vi skall
göra ett lite tre-i-rad (eller tic-tac-toe som det också kallas). Då skulle det vara praktiskt med ett fält
som ser ut på följande vis:
1
1 ' ' 1 'o' 1 'x'
2 'o' 2 'x' 2 'x'
3 'x' 3 ' ' 3 'o'
1
2
1 2 3
eller så här
3
2
3
1 2 3
1 2 3
Själva kryssen och ringarna skulle man kunna representera med tecken, eller kanske heltal. Det
finns många andra sätt att rita upp en datastruktur som skulle vara lämplig för just detta ändamål.
Oavsett vilket man använder så ser vi att detta koncept är så pass generellt att man även kan göra
sammansatta datatyper av andra sammansatta datatyper.
Men fält och poster kan vi alltså skapa oss precis hur komplexa strukturer som vi vill. Det enda som
egentligen sätter gränsen är just hur mycket minne som dessa strukturer tar. För eller senare så
kommer man komma till en punkt där man inte längre kan lagra allt sitt data i internminnet utan
behöver pytsa ut saker på hårddisken. Det vi pratar om då är lagring av data på fil.
En fil på datorns hårddisk består också av ettor och nollor. Det lättaste är att tänka sig det som en
lång radda med ettor och nollor, tills det kommer ett filslut.
F : 011001100110101010010010010100101001010101
f
Exakt vad som ligger på filen beror ju på hur man tolkar datat. Om vi ser det här som characters så
får vi alltså titta på 8 bitar i stöten. När vi pratar om textfiler så är det detta vi menar. Textfiler har
har också det kravet att det skall vara ett radslutstecken precis innan filslutet, (om filen inte är tom
d.v.s.).
Om man bortser från textfiler och pratar om det vi brukar kalla för binärfiler finns det egentligen
två typer. Den ena typer är sådana filer som i princip fungerar på samma sätt som textfiler, d.v.s. det
är samma datatyp hela vägen i filen. T.ex. en fil med bara heltal. Den andra typen är sådan att det
kan vara olika data i filen, då ser vi det mer som det vi kallar för en ström. Dessa två typer av filer
kommer vi inte jobba särskilt mycket med, men det är bra att veta att de finns, och att det i grund
och botten är samma principer som gäller för dessa som för textfiler.
Oavsett vad det är för data som ligger på filen som måste man känna till formatet om man skall
kunna tillgodogöra sig det data som finns lagrat däri. Det kan t.ex. vara så att man vet att det först
kommer tre heltal och sedan ett tabulatortecken på varje rad. Eller att man vet hur långa raderna är,
eller något annat. Ofta finns en hel del av informationen om hur filen skall tolkas i filens första del,
i en s.k. "header".
Detaljerna kring hur man läser och skriver till filer tar vi upp i C++ i nästa period.
Om vi nu går tillbaka till just internminnet så skulle man kunna rita upp ett litet diagram över ett
programs minnesanvändning:
Använt
Minne
Program B
Program A
När det kommer till de datatyper och datastrukturer som vi hittills pratat om så har de alla den
egenskapen att man måste deklarera i förväg hur mycket minne man behöver. Mer än så kan
programmet helt enkelt inte hantera! Detta betyder också att operativsystemet måste låta
programmet ha detta minne under hela sin körtid. Vill man köra fler program så gäller samma sak
för dem, och minnet blir snabbt fullt. Om det kommer fler program som vill köra, så får de antingen
vänta eller avbryta sin körning om det inte finns tillräckligt med minne för att de skall kunna starta.
Den egentliga minnesanvändningen varierar antagligen mycket under programmets exekvering,
både uppåt och nedåt, jag har ritat den med en streckad linje. Om man istället kunde få till så att
program kunde få begära mer minne då de behövde det, och sedan lämna tillbaka minne då de var
färdiga med det så kanske fler program skulle kunna köras samtidigt! Detta är vad vi brukar kalla
för dynamiskt minne (till skillnad från statiskt minne som vi hittills har tittat på).
För att åstadkomma detta kan man använda sig av pekare. En pekare är en datatyp som inte
innehåller något egentligt data i sig, utan en referens till en plats i datorns minne (man brukar säga
att den lagrar en adress) där det ligger data. Man kan rita upp det så här:
I:
7
7
P:
Här ser vi en vanlig heltalsvariabel I och en pekare P. Minnesplatsen som pekaren P refererar till
ligger alltså utanför vårt program. När ett program startar brukar pekarna inte refererar någon
särskild stans, utan det är först när programmet själv ber operativsystemet att låta P referera till en
ny plats i minnet som detta sker. Detta kan ske när som helst under programmet! Efteråt kan
programmet komma åt minnesplatsen genom att följa referensen i P. Naturligtvis är det också
viktigt att programmet lämnar tillbaka minnet innan programmet kört klart. Annars kan
operativsystemet missa att markera minnet som "ledigt" igen, och det kan bli minnesläckor.
Att som i figuren ovan använda en pekare för att peka ut ett heltal är inte aktuellt så ofta. Man gör ju
gärna på detta vis när datastrukturen är stor och onödig att ha i minnet hela tiden. Det är absolut
vanligast att det man pekar på är en sammansatt datatyp. Om vi nu leker med tanken att det vi pekar
på är en post så kan denna post, förutom annat data som man kanske vill lagra där också ha
utrymme för en till pekare, ungefär så här:
D:
D:
L:
L:
L:
Då skulle den pekare också i sig kunna peka vidare till en ny sådan post, o.s.v. Detta blir som en
kedja som kan bli så lång, eller så kort som man önskar just för ögonblicket. Vi brukar kalla detta
för en länkad lista. "D" skulle ju kunna vara precis vilken annan datatyp som helst som man nu vill
lagra många av.
Lite förenklat kan man rita detta på följande sätt:
L:
Den sista pekaren i listan kommer ju inte att peka på en ny post, så vi ritar den som ett snett streck.
Man säger även att detta är en "nullpekare", eller att "den pekar på null". Exakt vad detta betyder
kommer vi tillbaka till.
Man kan direkt tänka sig ett par varianter på sådana "länkade" strukturer. I listan ovan kan man ta
sig till nästa post genom att följa pekaren, men det går inte att ta sig tillbaka. Det finns dock
dubbellänkade listor som råder bot på detta. Där består varje post i listan av ett data och två pekare,
en som pekar framåt till nästa post, och en som pekar bakåt till föregående.
L:
Den sista strukturen som kan vara roliga att känna till är en trädstruktur. Om vi tar en post som i
föregående exemplet, d.v.s. en post med ett data och två pekare. Men vi låter pekarna peka på två
helt nya poster, så kan vi få en helt annan struktur:
T:
Detta är en helt annan lagringsform som kallas för binärt träd. Det går att utnyttja till många olika
saker. Ni kommer att få lära er mycket mer om det i senare kurser.
Nu har vi pratat igenom alla datastrukturer som ni kommer stöta på i den här kursen, plus lite till.
Vad man nu än skall göra, vilket problem man än har framför sig, så lönar det sig enormt att tänka
igenom hur man skall lagra sitt data. Att rita upp datastrukturer är viktigt för att förklara för andra
(och för sig själv!) hur man har tänkt att datat skall lagras. Ni kommer därför att få en
inlämningsuppgift som handlar om just detta.