Gå til indholdet

Arrays og lister

Arrays

Et array er et begreb i de fleste programmeringssprog, og det dækker over muligheden for at gemme data i struktureret form i stedet for i enkelte variabler.

Information til undervisere

Alle studerende skal forstå arrays - den klassiske datastruktur fra alle programmeringssprog. Min er erfaring er at det sværeste at forstå for helt nye er formålet (kom med et godt eksempel - som nedbør omtalt nedenfor) og at det er nulbaseret. Samlinger (generiske) kan godt være lidt lakrids fordi man lige skal lære at tænke generisk (brug talemåden “alt er det samme - bortset fra typen”). Ellers handler det om at løse en masse opgaver for at få syntaks i fingerne.

Forestil dig, at du skal beregne en sum og gennemsnit af nedbør over nogle måneder. Det kunne eksempelvis ske således:

double nedbørMåned1 = 4;
double nedbørMåned2 = 12;
double nedbørMåned3 = 2;
double nedbørMåned4 = 8;
double sum = nedbørMåned1 + nedbørMåned2 + nedbørMåned3 + nedbørMåned4;
Console.WriteLine($"Sum {sum:N2}");
double gns = sum / 4;
Console.WriteLine($"Gennemsnit {gns:N2}");

Det fungerer ganske udmærket og beregner både sum og gennemsnit helt korrekt. Problemet opstår, når der skal flere måneder med i beregningen, for det vil kræve ændring i både sum- og gennemsnits-beregning. Det var nemmere, hvis nedbør kunne gemmes i en variabel, som kan indeholde flere værdier, og at disse værdier kunne tilgås ved hjælp af et indeks. Så kan der benyttes løkkestrukturer til at løbe alle data igennem:

Array over nedbør

En sådan struktur hedder et array og giver en masse muligheder for at arbejde med strukturerede data. Et array i C# er nulbaseret forstået således, at det første element skal tilgås med indeks 0.

Koden fra tidligere kan nu skrives således:

double[] nedbør = { 4, 12, 2, 8 };
double sum = 0;
for (int i = 0; i < nedbør.Length; i++)
    sum += nedbør[i];
Console.WriteLine($"Sum {sum:N2}");
double gns = sum / nedbør.Length;
Console.WriteLine($"Gennemsnit {gns:N2}");

Du behøver ikke gå op i syntaks, men blot bemærke, at koden nu nemt kan udvides med flere måneders nedbør ved blot at udvide arrayet i første linje.

Info

Alle samlinger (herunder arrays) er nulbaserede - det første element starter altid med 0.

Info

Når du arbejder i console applikationer kan du som bekendt benytte Console.WriteLine til at udskrive data. Men i debug/læringssammenhæng kan du med fordel benytte en NuGet pakke kaldet Dumpify. Den kan udskrive alle slags datastrukturer på en meget pænere måde. Eksempelvis vil dette:

using Dumpify;
double[] nedbør = { 4, 12, 2, 8 };
nedbør.Dump();

udskrive:

Det er en meget pænere og logisk måde at se data på. Du lærer dog ikke at iterere gennem et array - men det er hurtigt og effektivt.

Oprettelse af et array

Som i de fleste programmeringssprog er et array i syntaks defineret ved hjælp af firkantede parenteser [ ]. Yderligere er C# jo et typestærkt sprog, hvilket betyder, at variabler skal erklæres af en konkret type. Det gælder også for et array.

Således skal en variabel, der kan indeholde referencen til et array, erklæres som følger:

int[] heltalsArray;
double[] doubleArray;
string[] stringArray;
bool[] boolArray;

Bemærk, at det er brugen af firkantede parenteser, der fortæller kompileren, at der er tale om et array. Men dette opretter blot en variabel, som kan pege på et array af en konkret type – der er ikke oprettet en instans i hukommelsen. Det skal ske ved hjælp af new-operatoren samt angivelse af hvor mange elementer, du ønsker. Således opretter følgende kode et heltalsarray med fem elementer, som automatisk bliver initialiseret til defaultværdien (0):

int[] heltalsArray = new int[5];

Koden opretter en variabel kaldet heltalsArray, der kan pege på et array med fem elementer (heltal) i hukommelsen:

Array af heltal

Og følgende kode opretter en variabel kaldet boolArray med tre elementer:

bool[] boolArray = new bool[3];

Da defaultværdien til en bool er false, bliver alle elementerne initialiseret til false:

Array af bool

Du kan også vælge at splitte erklæring og oprettelse af array over to instruktioner:

bool[] boolArray;
boolArray = new bool[3];

Du skal læse koden således, at første instruktion erklærer en variabel, der kan indeholde referencen til et bool-array, og næste instruktion opretter et array med tre elementer og tildeler referencen af arrayet i hukommelsen til variablen.

Hvis du kender værdierne, når du opretter arrayet, kan du vælge at benytte lidt syntaks sukker:

double[] nedbør = { 4, 12, 2, 8 };

// eller fra C# 12
double[] nedbør2 = [ 4, 12, 2, 8 ];

Her erklæres en variabel nedbør som peger på et array med fire elementer, der med det samme har fået en værdi.

Info

Lige som en streng er et array baseret på en klasse og er dermed en reference-type. Det betyder, at der gemmes en reference til et array i hukommelsen. I basal C# programmering er det ikke så væsentligt, men det vil hurtigt blive noget, du skal være opmærksom på.

Tilgang til elementer

Når først du har et array tilgængeligt, kan du tildele og aflæse data på flere måder. De fleste vælger at angive et indeks i firkantede parenteser:

double[] array = new double[4];
// Tildeler data
array[0] = 4;
array[1] = 12;
array[2] = 2;
array[3] = 8;

// aflæser data
double a = array[0];   // 4

Hvis du forsøger at tilgå et element uden for det antal elementer, der er angivet ved oprettelse, vil runtime smide en fejl (husk at et array er nulbaseret, så det første indeks er nul).

Gennemløb af arrays

Info

Brug Length for at finde ud af hvor mange elementer der er i et array

Fordi data kan tilgås gennem et indeks, og fordi alle instanser af et array har en egenskab kaldet Length, der returnerer antallet af elementer i arrayet, kan du benytte en for-løkke til at løbe elementerne igennem:

double[] nedbør = { 4, 12, 2, 8 };
for (int i = 0; i < nedbør.Length; i++)
{
    Console.WriteLine($"Indeks={i} Værdi={nedbør[i]}");
}

Det vil resultere i følgende:

Indeks=0 Værdi=4
Indeks=1 Værdi=12
Indeks=2 Værdi=2
Indeks=3 Værdi=8

Alternativt kan du benytte for ForEach-løkke, som ikke benytter sig af en tællevariabel, men blot løber alle elementer igennem, og tildeler den aktuelle værdi til variablen angivet i løkken:

double[] nedbør = { 4, 12, 2, 8 };
foreach (double item in nedbør)
{
    Console.WriteLine(item);
}

Det resulterer i følgende:

4
12
2
8

Hvis du ikke har brug for en tællevariabel, kan ForEach-løkken være lidt hurtigere at skrive.

Manipulering af arrays

Ethvert array er baseret på klassen System.Array, og denne klasse indeholder en del praktiske instans-metoder (metoder tilgængelig på instansen) og statiske metoder (metoder tilgængelige på typen).

På selve instansen er metoderne Clone og CopyTo interessante. Clone-metoder kopierer et array til et nyt array, og CopyTo kopierer værdier. Se følgende eksempler:

int[] a = new int[] { 10, 5, 1, 7, 1, 6 };

// Kopiering af array til et nyt array (typekonvering er nødvendig)
int[] b = a.Clone() as int[];
// b er nu
// [10, 5, 1, 7, 1, 6]

int[] c = new int[8];
c[0] = 20;
c[1] = 30;
a.CopyTo(c, 2);
// c er nu
// [ 20, 30, 10, 5, 1, 7, 1, 6]

Af statiske metoder findes især Resize, som ændrer størrelsen på et array, samt Sort og Reverse som sorterer værdier. Se følgende eksempler:

// Et array med 6 elementer
int[] a = new int[] { 10, 5, 1, 7, 1, 6 };


System.Array.Sort(a);
// a er nu
// [1, 1, 5, 6, 7, 10]

System.Array.Reverse(a);
// a er nu
// [10, 7, 6, 5, 1, 1]


// Nu har det 10 elementer
System.Array.Resize(ref a, 10);
// a er nu
// [10, 7, 6, 5, 1, 1, 0, 0, 0, 0]

Lige nu kan du se bort fra ref-kodeordet i kaldet til Resize, men du skal være bevidst om, at alle metoder tilretter værdier i det eksisterende array.

Warning

Du bør nøjes med at bruge arrays med en statisk/fast størrelse. Metoden Resize kan koste en del i performance.

Info

Senere vil du lære om LINQ og der er metoder der kan erstatte mange af de indbyggede metoder (herunder Sort).

Brug af Split og Join

Når du arbejder med strenge kan du med fordel bruge metoderne Split (instans metode), der splitter en streng til et array, og Join (statisk metode) der sammensætter en streng fra et array. Se følgende:

string csv = "1;5;7;65";
string[] data = csv.Split(';');
Console.WriteLine(string.Join(",", data));


/*
 ---------- Output: ----------

1,5,7,65

*/

System.Index og System.Range

I C# 8 introducerede man nye muligheder for at arbejde med indekser og intervaller, hvilket gør det lettere at håndtere underudsnit af datastrukturer som f.eks. arrays. Denne funktionalitet er udtrykt gennem System.Index og System.Range klasserne, samt syntaksen ^ og ...

^ operatoren anvendes til at referere til elementer fra slutningen af en datastruktur. For eksempel, ^1 refererer til det sidste element, ^2 til næstsidste element, og så videre. Dette er især nyttigt, når man ikke på forhånd kender længden af datastrukturen.

System.Index repræsenterer en position i en datastruktur. Det kan bruges til at angive et specifikt element enten fra starten eller slutningen. For eksempel, array[^1] giver det sidste element i arrayet.

.. operatoren skaber et Range objekt, der repræsenterer et intervaller af elementer. Det kan bruges til at udtrække dele af en array. For eksempel, array[1..4] udtrækker elementerne fra index 1 til 3 (elementet ved index 4 er ikke inkluderet).

System.Range repræsenterer et intervaller mellem to Index værdier. Det giver en fleksibel måde at arbejde med underudsnit af arrays og andre datastrukturer.

Samlet set gør disse funktioner det meget nemmere at manipulere dele af arrays og lignende strukturer i C#, hvilket før kunne kræve egen kode med stor risiko for fejl.

Se følgende eksempel:

int[] nr = new[] { 5, 7, 1, 3, 9, 10, 0, 1, 2, 7 };

VisArray(nr[..]);           // 5 7 1 3 9 10 0 1 2 7
VisArray(nr[..4]);          // 5 7 1 3
VisArray(nr[4..]);          // 9 10 0 1 2 7
VisArray(nr[1..5]);         // 7 1 3 9


// Bagfra
Console.WriteLine(nr[^1]);  // 7
Console.WriteLine(nr[^5]);  // 10

VisArray(nr[^2..]);         // 2 7
VisArray(nr[^5..^3]);       // 10 0

System.Index fra = ^4;
System.Index til = ^2;
VisArray(nr[fra..til]);     // 0 1

System.Range r = 1..4;
VisArray(nr[r]);            // 7 1 3

void VisArray(int[] a)
{
    Console.WriteLine(string.Join(' ', a));
}

Brug af params

params er et nøgleord i C#, der tillader en metode at acceptere et variabelt antal argumenter af en bestemt type. Dette gør det muligt at sende et vilkårligt antal argumenter til en metode ved at bruge et enkelt parameter.

For at bruge params nøgleordet i en metode, skal parameteren være af typen array. Her er et eksempel på en metode, der anvender params:

public static int Sum(params int[] numbers)
{
    int sum = 0;
    foreach (int number in numbers)
    {
        sum += number;
    }
    return sum;
}

I eksemplet ovenfor kan metoden Sum tage et variabelt antal heltalsargumenter og returnerer deres sum. Metoden kan kaldes med forskellige antal argumenter:

int result1 = Sum(1, 2, 3); // result1 = 6
int result2 = Sum(10, 20);  // result2 = 30
int result3 = Sum(5);       // result3 = 5

Der er nogle begrænsninger, når man bruger params nøgleordet i C#:

  • Der må kun være én params parameter i en metode.
  • params parameteren skal være den sidste parameter i metoden.
  • params nøgleordet kan kun bruges med enkelt-dimensionale arrays.

Det er muligt at kombinere params parameteren med andre parametre i en metode. Her er et eksempel:

public static string BuildGreeting(string name, params string[] messages)
{
    StringBuilder sb = new StringBuilder($"Hello, {name}!");

    foreach (string message in messages)
    {
        sb.Append($" {message}");
    }

    return sb.ToString();
}

I eksemplet ovenfor er name en almindelig parameter, og messages er en params parameter. Metoden kan kaldes som følger:

string greeting = BuildGreeting("John", "How are you?", "Have a nice day!");

Dette vil resultere i følgende streng: "Hello, John! How are you? Have a nice day!"

Flere dimensioner

Et array har som udgangspunkt kun én dimension forstået således, at det kan ses som en enkelt kolonne i et regneark med mange rækker. Så følgende kode:

int[] a = { 5, 1, 3, 7, 8 };

kunne anskues som et regneark som følger:

Et endimensionelt array i et regneark

Men det står dig frit for at oprette et array med flere dimensioner ved at benytte et eller flere kommaer i erklæringen:

int[,] a = new int[3, 3];
a[0, 0] = 1;
a[0, 1] = 2;
a[0, 2] = 3;
a[1, 0] = 4;
a[1, 1] = 5;
a[1, 2] = 6;
a[2, 0] = 7;
a[2, 1] = 8;
a[2, 2] = 9;

Det kan jo se noget teknisk ud, men prøv at se det som data i et regneark:

Et todimensionelt array i et regneark

Nu er det lidt nemmere at gennemskue, hvorfor a[1, 2] indeholder værdien 6.

Her er et lidt mere komplekst eksempel på brug af et todimensionelt array:

string[,] skakBræt = new string[8, 8];
skakBræt[0, 0] = "Ts";
skakBræt[0, 1] = "Hs";
skakBræt[0, 2] = "Ls";
skakBræt[0, 3] = "Ds";
skakBræt[0, 4] = "Ks";
skakBræt[0, 5] = "Ls";
skakBræt[0, 6] = "Hs";
skakBræt[0, 7] = "Ts";

for (int i = 0; i < 8; i++)
{
    skakBræt[1, i] = "Bs";
    skakBræt[6, i] = "Bh";
    skakBræt[7, i] = skakBræt[0, i].Replace("s", "h");
}

// Koden skaber et skakbræt med 64 felter og placerer samtidig brikker (Ts = tårn sort, Hh = Hest hvid og så videre). Skakbrættet kan udskrives med:
for (int række = 0; række < 8; række++)
{
    for (int kolonne = 0; kolonne < 8; kolonne++)
    {
        Console.Write(skakBræt[række, kolonne] + " ");
    }
    Console.WriteLine();
}

Hvilket giver følgende resultat:

Ts Hs Ls Ds Ks Ls Hs Ts
Bs Bs Bs Bs Bs Bs Bs Bs




Bh Bh Bh Bh Bh Bh Bh Bh
Th Hh Lh Dh Kh Lh Hh Th

Hvis du er ny i programmering, så se om du kan følge logikken – både i koden, der skaber skatbrættet, og koden der udskriver det.

Du må gerne skabe arrays med mange dimensioner, men efter 3. dimension begynder det hele at blive noget teknisk at overskue.

Opgaver

Samlinger

Et traditionelt array er en meget statisk datastruktur forstået således, at når det først er oprettet med en type og en størrelse, koster det en del at ændre størrelsen, hvis man ønsker flere data i strukturen.

Heldigvis findes der andre strukturtyper, du kan benytte som alternativ – herunder lister, stakke, køer og nøgle-/værdi-strukturer, og der findes både typesvage (System.Collections) og typestærke versioner (System.Collections.Generic).

Du skal fokusere på de typestærke i System.Collections.Generic, fordi de er langt mere effektive i både udvikling og afvikling.

Generiske samlinger

Inden vi kigger på de konkrete samlinger, er det dog vigtigt, at du er introduceret til et begreb, du vil falde over mange gange i C# – nemlig begrebet generics. Det kan vel oversættes til generel og er relateret til brugen af konkrete typer.

Det er en avanceret feature i C#, som giver mulighed for at fortælle kompileren, at du på en type (eller en metode) ønsker at arbejde med en konkret variabeltype. I generiske typer er logik og funktionalitet således ens for alle typer (som dog kan filtreres), og ved brug skal den type, du ønsker, angives. Hertil benyttes en speciel syntaks med mindre og større end tegn samt en angivelse af typen. Tit ser du generiske typer angivet med en variabeltype kaldet T (forkortelse for type), og T kan du mentalt erstatte med den type, du ønsker at benytte:

// Forskellige eksempler på generiske typer
List<T> = Liste af MANGE FORSKELLIGE typer
Queue<T> =  af MANGE FORSKELLIGE typer
Point<T> = Point af MANGE FORSKELLIGE typer

Når du i kode ser en konkret datatype omkranset af < > skal du mentalt tilføje af:

List<int> = Liste AF heltal 
Queue<bool> =  AF boolske værdier
Stack<string> = Stak AF strenge

Det smarte ved generiske typer er, at både Visual Studio og kompileren kan optimere brug og afvikling, fordi du fortæller, hvilken konkret datatype, du ønsker at arbejde med.

Liste

I langt de fleste tilfælde vil du arbejde med List, som er en simpel og effektiv dynamisk liste af instanser af en konkret datatype. Følgende opretter eksempelvis en liste af heltal i hukommelsen og tildeler refe-rencen til en variabel:

List<int> a = new List<int>();

En List kan også bruges på alle andre typer – eksempelvis:

List<bool> a = new List<bool>();
List<DateTime> b = new List<DateTime>();
List<string> a = new List<string>();

Den kan ligeledes benyttes med dine egne typer (se senere i bogen). Ligesom arrays og andre typer kan en liste initialiseres med data direkte ved oprettelse:

List<int> tal = new List<int> { 4, 12, 2, 8 };
List<string> strenge = new List<string> { "a", "x", "y", "b" };
List<DateTime> datoer = new List<DateTime> { 
  new DateTime(2020, 1, 1), 
  new DateTime(2021, 1, 1) 
};

Når først instansen er oprettet, består den af en masse medlemmer til at indsætte, fjerne, sortere, filtrere, gennemløbe og meget mere. Her er eksempler på de mest benyttede metoder. Der tages udgangspunkt i en liste af strenge, men husk at du kan skabe en liste af mange forskellige typer:

// Liste af strenge
List<string> lst = new List<string>();

// Tilføj "a", "b" og "c"
lst.Add("a");
lst.Add("b");
string c = "c";
lst.Add(c);

// Hvor mange strenge er der i listen
int antal = lst.Count; // 3

// Indsæt "*" i position 1 (husk - alt er nulbaseret)
lst.Insert(1, "*"); // lst = a, *, b, c

// Indsæt data fra et array
lst.InsertRange(2, new string[] { "!", "@" });  
// lst = a, *, !, @, b, c

// Fjern *
lst.Remove("*"); // lst = a, !, @, b, c

// Fjern streng i position 2
lst.RemoveAt(2); // lst = a, !, b, c

// finder der en ! i listen
bool test = lst.Contains("!");  // test = true

// Løb alle igennem og skriv ud
Console.WriteLine("Vis alle:");
foreach (var item in lst)   // item = string
{
    Console.WriteLine(item);
}


// Sorter listen 
lst.Sort();

// Sorter listen (omvendt rækkefølge)
lst.Reverse();

Her er et konkret eksempel på brug af en liste til at beregne en sum og et gennemsnit af nedbør (samme eksempel som blev gennemgået i kapitlet om arrays):

List<int> nedbør = new List<int> { 4, 12, 2, 8 };
int sum = 0;
foreach (int tal in nedbør)
    sum += tal;

// eller 
// for (int i = 0; i < nedbør.Count; i++)
//    sum += nedbør[i];

double gns = sum / nedbør.Count;    // sum = 26, gns = 6

Her er samme eksempel hvor sum og gennemsnit beregnes i metoder:

List<int> nedbør = new List<int> { 4, 12, 2, 8 };
int sum = BeregnSum(nedbør);            // 26
double gns = BeregnGennemsnit(nedbør);  // 6

int BeregnSum(List<int> l) {
    int sum = 0;
    foreach (int tal in nedbør)
        sum += tal;
    return sum;
}

double BeregnGennemsnit(List<int> l)
{
    int sum = BeregnSum(l);
    double gns = sum / l.Count;
    return gns;
}

Brug en liste (List) alle de steder, hvor du skal gemme værdier af samme type – det er meget nemmere end et array.

Stak

En anden klassisk datastruktur er en stak (stack på engelsk), som også findes under System.Collections.Generic. Du kan tænke på en stak, som en bunke af spillekort med bagsiden opad. Du kan lægge kort på bunken (Push-metoden), og tage kort af bunken (Pop-metoden), og det kort, du har lagt på bunken sidst, er også det kort, du tager af bunken først. Derfor kaldes en stak en LIFO-struktur (last in – first out).

Strukturen kan være brugbar i mange situationer – herunder i kortspil, hvor den naturligvis er meget brugt. En Stack kan ligesom List oprettes ud fra en konkret type. Det gælder både de simple variabeltyper samt dine egne typer. Her er eksempelvis en stak af strenge – men husk, det kan være hvad som helst:

Stack<string> s = new Stack<string>();

// Tre kort på bunken
s.Push("Ruder konge");
s.Push("Spar es");
s.Push("Hjerter to");

// Kig på det øverste kort - men fjern det ikke
string a = s.Peek();

// Fjern et kort fra bunken og gem det i en variabel
string b = "";
b = s.Pop(); // nu består s af "Ruder konge" og "Spar es"
// "Hjerter to" kan findes i b

// Hvor mange kort er der i bunken
int antal = s.Count; // 2

// Udskriv alle kort
foreach (var item in s)
    Console.WriteLine(item);    // Spar es, Ruder konge

At benytte en kø som datastruktur er også meget brugt i programmering – blandt andet til at afvikle funktionalitet i den korrekte rækkefølge. Du kan tænke på en kø, som en kø til kassen i et supermarked. Kunde X står først, så kunde Y, så kunde Z og så videre. Efterhånden som kunderne bliver ekspederet, rykker de andre frem i køen. En kø kaldes derfor for en FIFO-struktur (First In – First Out).

I C# kan du finde en FIFO-datastruktur i klassen Queue:

Queue<string> kunder = new Queue<string>();

// Sæt kunder i kø
kunder.Enqueue("Y");
kunder.Enqueue("Z");
kunder.Enqueue("W");

// hvor mange er i kø
int antal = kunder.Count;   // 3

// Hvem står først (kigger uden at fjerne)
string a = kunder.Peek(); // a = "Y"

// Ekspeder kunde
string b = kunder.Dequeue(); // b = "Y" og kunder = "Z", "W"

// hvor mange er i kø
antal = kunder.Count;   // 2

// Skriv alle ud
foreach (var kunde in kunder)
{
    Console.WriteLine(kunde);   // "Z", "W"
}

Da Queue er generisk, kan du bruge den til alle typer.

Nøgle og værdi

Dictionaries i C# er baseret på en datastruktur kaldet en hash-tabel. En hash-tabel er en samling, der bruger en hash-funktion til at beregne en unik indeks for hver nøgle i samlingen. Denne indeks bruges til at gemme og hente værdier hurtigt. Hash-funktionen sikrer, at nøglerne er jævnt fordelt i tabellen, hvilket gør det muligt at opnå en gennemsnitlig konstant tid for søgning, indsættelse og sletning af elementer.

En af de største fordele ved dictionaries er, at de giver hurtig adgang til værdier baseret på nøgler. Dette skyldes, at hash-funktionen konverterer nøglen til en indeks i hash-tabellen, og dette skridt tager generelt konstant tid. Efter at indekset er beregnet, kan værdien findes og returneres direkte fra tabellen.

Når der er flere værdier med den samme hash-indeks (kollisioner), anvendes en teknik kaldet separat chaining, hvor kolliderende elementer opbevares i en linked-liste-struktur. Selvom dette kan føre til en længere søgetid i værste fald, er den gennemsnitlige søgetid stadig meget hurtig, især når hash-funktionen er effektiv og tabellen har en passende størrelse.

På samme måde som med søgning og adgang til værdier, er indsættelse og sletning af elementer i et dictionary også hurtigt. Indsættelse involverer beregning af hash-indekset for nøglen og placering af værdien på den relevante placering i tabellen. Hvis der opstår en kollision, vil elementet blive tilføjet til den linked-liste, der er forbundet med det pågældende indeks.

Sletning af et element indebærer at finde elementet baseret på nøglen (ved at beregne hash-indekset og søge i den relevante linked-liste, hvis nødvendigt) og derefter fjerne det fra tabellen og linked-listen. Ligesom med indsættelse og søgning er sletning generelt hurtigt, med en gennemsnitlig konstant tid.

I C# findes der flere hash-tabel implementeringer inden for System.Collections.Generic-namespace. Disse datastrukturer tilbyder forskellige egenskaber og funktionaliteter, der kan være nyttige afhængigt af den specifikke anvendelse og krav. Nedenfor er en gennemgang af nogle af de mest almindelige hash-tabel implementeringer i C#.

Dictionary

Dictionary<TKey, TValue> er en af de mest almindeligt anvendte hash-tabel implementeringer i C#. Det er en generisk samling, der tillader hurtig adgang, indsættelse og sletning af nøgle-værdi par baseret på unikke nøgler. Dictionary<TKey, TValue> bruger separate chaining med linked lister til at håndtere kollisioner.

Eksempel på brug af Dictionary<TKey, TValue>:

var people = new Dictionary<int, string>
{
    {1, "Mette"},
    {2, "Anders"},
    {3, "Svend"}
};

string name = people[1]; // Mette

Her er et andet eksempel:

// Nøgle = string, Værdi = int
Dictionary<string, int> dic = new Dictionary<string, int>();

// indsæt værdier - først nøgle så værdi
dic.Add("a", 100);
dic.Add("b", 200);
dic.Add("c", 300);

// værdier kan så findes ud fra nøglen
Console.WriteLine(dic["b"]);    // 200

// Findes der en nøgle
Console.WriteLine(dic.ContainsKey("a"));    // true

// Samlingen kan også gennemløbes
foreach (var item in dic)
    Console.WriteLine($"{item.Key} {item.Value}");

Opgaver

Diverse samlinger

ConcurrentDictionary

ConcurrentDictionary<TKey, TValue> er en trådsikker version af Dictionary<TKey, TValue>. Denne implementering er designet til at kunne bruges sikkert i flertrådede programmer og giver trådsikker adgang, indsættelse og sletning af nøgle-værdi par. Det er en del af System.Collections.Concurrent-namespace, men det er nævnt her, fordi det er baseret på en hash-tabel.

Eksempel på brug af ConcurrentDictionary<TKey, TValue>:

var concurrentPeople = new ConcurrentDictionary<int, string>();
concurrentPeople.TryAdd(1, "Mette");
concurrentPeople.TryAdd(2, "Anders");
concurrentPeople.TryAdd(3, "Svend");

string concurrentName;
concurrentPeople.TryGetValue(1, out concurrentName); // Mette

HashSet

HashSet<T> er en samling, der indeholder unikke elementer og er baseret på en hash-tabel. Denne implementering er nyttig, når der er behov for at arbejde med et sæt af unikke værdier og udføre operationer som forening, snit og forskel mellem sæt. HashSet<T> bruger også separate chaining med linked lister til at håndtere kollisioner.

Eksempel på brug af HashSet<T>:

var setA = new HashSet<int> {1, 2, 3};
var setB = new HashSet<int> {2, 3, 4};

setA.IntersectWith(setB); // setA indeholder nu {2, 3}

Disse er nogle af de mest almindelige hash-tabel implementeringer i C# inden for System.Collections.Generic-namespace. Valget af den rigtige implementering afhænger af de specifikke krav og begrænsninger i det givne problem og programmeringssammenhæng.

Opgaver

O(1)

I computer videnskab, bruges Big O notation til at beskrive ydelsen - eller tidskompleksiteten - af en algoritme. “O” står for “ordre af”, og det efterfølgende udtryk (i dette tilfælde “1”) beskriver hvordan algoritmens køretid vokser med stigende datasæt.

O(1) beskriver en algoritme, der udfører en operation i konstant tid, uanset størrelsen af input data. Med andre ord, det tager samme tid at fuldføre operationen, uanset om du arbejder med 10 elementer eller 10 millioner.

I C#, har flere datastrukturer operationer, der kan udføres i O(1) tid. For eksempel, at finde en værdi i en Dictionary<TKey, TValue> ved hjælp af en nøgle, eller at tilgå et element i et Array ved hjælp af dets indeks, begge har en tidskompleksitet på O(1) i gennemsnit.

Dette betyder ikke, at disse operationer altid tager samme tid - forskellige nøgler eller indekser kan tage forskellige mængder tid at behandle. Men gennemsnittet af disse tider forbliver konstant, uanset størrelsen på det data, der arbejdes med.

Bemærk at den faktiske tidskompleksitet kan variere i praksis, afhængig af faktorer som kvaliteten af hash-funktionen (for dictionaries), systemets hukommelsestilstand, og andre detaljer. Men generelt er det nyttigt at have en forståelse for Big O notation og tidskompleksitet, når man skriver og optimerer kode.

Her er nogle eksempler med forskellige C# datastrukturer til at finde en Person givet et fiktivt CPR nummer:

public class Person
{
    public string CPR { get; set; }
    public string Name { get; set; }
    // Andre egenskaber...
}

// Eksempel med Dictionary
Dictionary<string, Person> personsDictionary = new Dictionary<string, Person>();
// Tilsæt personer til dictionary...
string CPRToFind = "1234567890";
Person personFromDictionary = personsDictionary[CPRToFind];

// Eksempel med Array
Person[] personsArray = new Person[10];
// Tilsæt personer til array...
Person personFromArray = Array.Find(personsArray, person => person.CPR == CPRToFind);

// Eksempel med List
List<Person> personsList = new List<Person>();
// Tilsæt personer til listen...
Person personFromList = personsList.Find(person => person.CPR == CPRToFind);

I disse eksempler finder vi en Person givet et fiktivt CPR nummer. I Dictionary eksemplet er dette en O(1) operation, da vi kan bruge CPR-nummeret som nøgle til direkte at finde den tilsvarende Person. I Array og List eksemplerne skal vi dog søge gennem hver Person indtil vi finder en med det ønskede CPR nummer. Dette er en O(N) operation, da vi potentielt skal se på hver Person i samlingen.

IEnumable

Senere vil du lære om interfaces - herunder IEnumable. Hvis du falder over IEnumable i noget kode kan du indtil videre blot opfatte det som en måde at betegne en type der overordnet repræsenterer samling som eksempelvis en List eller et array.

Mutable typer

Arrays (System.Array) og lister er alle baseret på klassen og dermed referencebaserede. Det kan give fordele OG ulemper når de benyttes som argumenter til metoder. Se sektion om mutable- og immutable typer og Hukommelsesteori.