Datorer och programmering TDB2: Lista - en dynamisk datastruktur Eva Pärt-Enander Vad är en lista? En lista är en sekventiell struktur oftast av likadana saker. Listor denieras i C++ ofta med hjälp av klasser, men representationen (dvs vilka dataattribut klassen har) kan ske på olika sätt: Indexerad lista: Varje element åtkoms med ett index. Delvis knyckta delar av Olle Eriksson Statisk array. Längden bestäms under kompileringen. Dynamisk array. Längden bestäms under exekveringen. Dynamisk, tänjbar array. Kan växa och krympa under exekveringen. Länkad lista: Varje element åtkoms med hjälp av pekare från t ex föregående element. Kan växa och krympa under exekveringen. 0-0 1 Vad bör en lista klara av? 0 1 2 3 4 5 maxlist-1 Vilka saker kan en lista göra? Nästan alltid vill man t ex kunna lägga till ett element eller söka om ett speciellt element nns i listan eller ej. 8 4 6 Indexerad lista 4 0-2 3 En enkel lista bör klara av Tala om sin aktuella längd, dvs antalet listelement. 8 4 6 4 Länkad lista 0-2 3 Tala om sin maximala kapacitet, dvs max antal element som listan kan ha. Ta bort alla element, dvs göra sig själv till tom lista. Ta bort ett element ur listan. Figur 1: Länkad lista. Lägga till ett nytt element, t ex sist. Indikera att ett element redan nns i listan eller ej. Indikera om listan är tom eller ej. 2 3
Array-baserad Olika sorters listor implementation av lista Ordnad lista: eller sorterad lista är en lista i vilken elementen ligger sorterade i stigande eller fallande ordning. Ex. Telefonkatalogen, olika kartotek. Stack: LIFO-struktur (Last In First Out), dvs den som kommer sist blir behandlad först. Ex. Olika former av travar, pappershögar och liknande. Kö: FIFO-struktur (First in First Out), dvs den som kommer först blir behandlad först. Ex. En lång rad konsumenter som hyser en Array-baserade listor representeras av tre delar: const int maxlist: en heltalskonstant som indikerar maximala antalet tillåtna element. int size: ett heltal som anger antalet element som nns vid ett visst tillfälle. int listitem[maxlist]: en array som kan ha maxlist antal heltalselement som mest. Tillåtna indexvärden i arrayen listitem är 0, 1,..., maxlist. gemensam längtan att träa producenten av en resurs. Att ta bort alla element i listan motsvaras av size = 0; Dock nns det dataelement kvar i arrayen, men i och med att size nollställts, så bildar de ingen lista. 4 5 Exempel på klassdeklaration för klassen SimpleList med statisk array: Traversera en lista const int maxlist = 25; class SimpleList private: int listitem[maxlist]; int size; public: SimpleList(); void clear(); void addtoend( int newitem ); void insert( int newitem ); void remove( int itemtogo ); int member( int anyitem ); int maximumcapacity(); int currentsize(); Antag att vi vill komma åt alla element i listan, då kan man iterera sig igenom listan med en loop. Denna process kallas för traversering. Exempel: for ( int i = 0; i < size; i++ ) access( listitem[i] ); där access symboliserar en godtycklig funktion med ett heltal som parameter eller någon sats som involverar listitem[i], t ex nollställande av element i konstruktorn eller utskrift i metoden write(). De esta programerare använder for-loop istället för while-loop då de traverserar en lista. int empty(); ; void write(); Om listan är tom, dvs size är 0, så kommer loopen ej att utföras. 6 7
Addera element till ordnad lista Addera element till en lista Processen att addera ett element till en lista beror på vilken sorts lista man har. Enklast är att lägga till element i slutet av en array-baserad lista. Antag sorterad i stigande ordning. Enklaste sättet att bibehålla ordnade strukturen är att stoppa in nya element på rätt plats direkt. (Jämför instickssortering.) Om detta görs i array-baserad lista, så måste vissa element skiftas ett steg. void SimpleList:: addtoend( int newitem ) Algoritm: Starta sist if ( size < maxlist ) listitem[size] = newitem; size++; 1. Starta med sista elementet i listan, dvs sätt indexpekaren current = size-1. Om detta element större än newitem så skifta ett steg åt höger till plats size, eftersom vi vet att den ändå måste ytta på sig. 2. Minska indexpekaren med 1. Fortsätt med vänster del-array. 8 9 void SimpleList:: insert( int newitem ) int current = size - 1; // Aktuellt index. // Leta efter rätt plats, skifta. while ( current >=0 listitem[current] > newitem ) listitem[current + 1] = listitem[current]; current--; // Nytt tal skall in efter. listitem[current + 1] = newitem; size++; // Öka listlängd. Antagandet har gjorts att listan inte är full, dvs size < maxlist. Ta bort ett element från en lista Processen att ta bort ett element ur en lista beror också på vilken sorts lista man har. I en stack lägger man till och tar bort element i ena änden, men i en kö lägger man till i ena änden, men tar bort från andra änden. Många andra listor kan ta bort element var som helst, beroende på elementvärden mer än på position i listan. Man måste först söka i listan för att hitta elementet som skall bort. För en array-baserad lista, så medför elementborttagande att vissa andra element måste skiftas (i motsatt riktning jämfört med att addera element till en lista). 10 11
void SimpleList:: remove( int itemtogo ) int psn = 0; // Position för nästa element // Sök efter elem. att ta bort while ( psn < size listitem[psn]!= itemtogo ) psn++; // Om elementet funnet. if ( psn < size ) for ( int i = psn; i < size-1; i++ ) listitem[i] = listitem[i+1]; // Skifta. size--; // Minska listan. Sortering och sökning Ordnade listor behöver inte sorteras de är redan sorterade. Många listor är osorterade, men sorteras om man så behöver. Bra att känna till följande sorteringsalgoritmer: Instickssortering. Urvalssortering. Bubbelsortering. Bra att känna till följande sökningsalgoritmer: Lineär sökning. (Oordnad datamängd). Binär sökning. (Ordnad datamängd). 12 13 Exempel på huvudprogram som använder SimpleList: int main() SimpleList list; list.insert( 34 ); list.insert( 27 ); list.remove(55); list.insert( 345 ); list.insert( 58 ); if ( list.empty() ) cout << "Tom lista!!!" << endl; list.write(); list.clear(); list.write(); return 0; ger följande körning: SimpleList another; another.insert( 55 ); another.insert( 78 ); another.insert( 15 ); Antal element är 4: 27 34 58 345 Antal element är 0. Antal element är 3: 15 55 78 Tom lista!!! list = another; list.write(); list.remove(15); list.remove(78); 14 15
Metodimplementation för övriga metoder/konstruktorer: SimpleList:: SimpleList() : size(0) for ( int i = 0; i < maxlist; i++ ) listitem[i] = 0; int SimpleList:: maximumcapacity() return maxlist; void SimpleList:: clear() size = 0; int SimpleList:: currentsize() return size; int SimpleList:: member( int anyitem ) for ( int i = 0; i < size; i++ ) if ( listitem[i] == anyitem ) int SimpleList:: empty() return (size == 0); // 1 om tom, annars 0. return 1; // TRUE return 0; // FALSE 16 17 void SimpleList:: write() Avancerad listklass med dynamisk tänjbar array cout << "Antal element är " << size; if (!empty() ) cout << ":"; Man skulle i SimpleList kunna ha en dynamisk array listitem istället för statisk: private: int *listitem; int maxlist; for ( int i = 0; i < size; i++ ) int size; cout << \t << listitem[i]; else cout << "."; cout << endl; I princip kan allt vara som förut, men man allokerar och avallokerar minne i konstruktorer respektive destruktorn. Dessutom byts const int maxlist = 25; i header-len mot att användaren t ex ger värdet för attributet maxlist. Vinsten är inte så stor om man inte gör arrayen tänjbar... 18 19
En tänjbar array upptar alltid precis så mycket minne som behövs. Den kan växa eller krympa om t ex: användaren skickar ett meddelande om detta. Meddelandet innehåller nya längden. den tilldelas en annan tänjbar array. Dess nya längd blir längden på det som tilldelades. man adderar eller tar bort element. Vi skall studera klassen ExtArray (extensible array) lite närmare nu: class ExtArray private: Först en liten varning: Kom ihåg att det här med pekare som attribut inte är enkelt! Pekare och arrayer är relaterade till varandra, men de är inte samma sak! Så fort man har med pekare att göra så måste man vara försiktig, eftersom det lätt blir konstiga fel annars. T ex pekare som pekar fel, tappar bort vad de pekar på eller konstiga kopieringar av data som sker, etc. int *array; // Dynamisk array. int bufsize; // Aktuella längden. public:... 20 21 Frågor: Vad händer om: Tilldelningsoperatorn (=): man gör delete [] på ena pekarvärdet? Den inbyggda tilldelningsoperatorn fungerar utmärkt för varianten räckvidderna (scope) är olika stora för objekt1, objekt2? objekt1 = objekt2; då motsvarande klass inte innehåller pekarattribut. Datavärde för datavärde kopieras då över från objekt2 till objekt1. Botemedel: Man skriver en egen tilldelningsfunktion, t ex: void ExtArray:: assign( const ExtArray a ); Om vi har pekare, som i klassen ExtArray, så innebär objekt1 = objekt2; att även pekarna kopieras direkt! Dvs pekarna i objekt1 och objekt2 pekar ut samma data (samma minnesutrymme), så om man senare ändrar i objekt2:s array, så sker samma ändring i objekt1:s array, fast man troligen inte ville det! S k grund kopiering (ej fullständig, ickedjup, attributkopiering, shallow copy). dvs tilldelningen: objekt1.assign( objekt2 ); fungerar då rätt om man implementerar assign så att den skapar en helt egen kopia av arrayen genom att kopiera element för element, s k djup kopiering (fullständig, deep copy). Om -tecknet glöms så måste en egen kopieringskonstruktor nnas, annars blir det fel i allafall... const medför att objekt2 ej kan förstöras/ändras i metoden. 22 23
Kopieringskonstruktorn Alternativt botemedel: Skriv en överlagrad tilldelningsoperator så att objekt1 = objekt2; innebär djup kopiering. Deklareras i stil med det här: Det nns en inbyggd kopieringskonstruktor i C++ som automatiskt används vid vissa tillfällen fast man inte tänker på det. Allt fungerar bra så länge som man inte har pekare bland attributen. Kopieringskonstruktorn anropas då man const ExtArray ExtArray:: operator= ( const ExtArray a ) initierar objektvariabler, vid ickereferensparameteröverföring till funktioner eller när man returnerar objektvärden från funktioner. :- Inte helt trivialt med C++... :-0 Både den inbyggda kopieringskonstruktorn och tilldelningsoperatorn gör alltså grund kopiering! 24 25 Botemedel: Man skriver en egen kopieringskonstruktor som kopierar elementvis (dvs djupt). Kod för hela klassen ExtArray + main + körning: // Extensible array. Tänjbar array. const int blank = 0; ExtArray( const ExtArray a ); medför att t ex ExtArray arr2; // default. ExtArray arr3 = ExtArray( 2, -99 ); //överlagra ExtArray arr4(arr3); // kopierings. ExtArray arr5 = arr2; // kopierings. fungerar! class ExtArray private: int *array; int bufsize; public: ExtArray(); ExtArray( int siz, int val ); ~ExtArray(); ExtArray( const ExtArray a ); Summering: Om man har pekare som attribut, så bör man alltså ha egendenierad void change(int i, int num); int size(); void resize( int n ); destruktor, kopieringskonstruktor, tilldelningsoperator/metod. void assign( const ExtArray a ); ; void write(); void addtoend( int newitem ); 26 27
int main() ExtArray arr1( 5, -1 ); // 5 element, -1:or ExtArray arr2; // 0 element. ExtArray arr3 = ExtArray( 2, -99 ); ExtArray arr4(arr3); // kopiering.. ExtArray arr5 = arr2; // kopiering.. cout << "arr1: "; arr1.write(); cout << "arr2: "; arr2.write(); cout << "arr3: "; arr3.write(); cout << "arr5: "; arr5.write(); cout << "Tilldela arr1 = arr3:" << endl; // arr1 = arr3; // Kopierar ickedjupt!!! arr1.assign( arr3 ); // OKAY. cout << "arr1: "; arr1.write(); cout << "arr2: "; arr2.write(); cout << "arr3: "; arr3.write(); cout << "arr1: "; arr1.write(); cout << "arr2: "; arr2.write(); cout << "arr3: "; arr3.write(); cout << "arr3: byt värden till 11 och 22..." << endl; arr3.change(0,11); arr3.change(1,22); 28 29 Körning ger: cout << "arr3: ändra längd dessutom" << endl; arr3.resize(6); cout << "arr1: "; arr1.write(); cout << "arr2: "; arr2.write(); cout << "arr3: "; arr3.write(); cout << "Lägg till element 22, 33 på " << "slutet i arr2, arr3:" << endl; arr2.addtoend( 22 ); // Nytt element sist arr3.addtoend( 33 ); // Nytt element sist cout << "arr1: "; arr1.write(); cout << "arr2: "; arr2.write(); cout << "arr3: "; arr3.write(); return 0; Hello överlagrad 5 Hello default 0 Hello överlagrad 2 Hello kopiering: ny längd: 2 Hello kopiering: ny längd: 0 arr1: Antal element är 5: -1-1 -1-1 -1 arr2: Antal element är 0. arr3: Antal element är 2: -99-99 arr4: Antal element är 2: -99-99 arr5: Antal element är 0. Tilldela arr1 = arr3: Hello assign 2 arr1: Antal element är 2: -99-99 arr2: Antal element är 0. arr3: Antal element är 2: -99-99 arr3: byt värden till 11 och 22... arr1: Antal element är 2: -99-99 arr2: Antal element är 0. arr3: Antal element är 2: 11 22 arr3: ändra längd dessutom arr1: Antal element är 2: -99-99 arr2: Antal element är 0. 30 31
arr3: Antal element är 6: 11 22 0 0 0 0 Lägg till element 22, 33 på slutet i arr2, arr3: arr1: Antal element är 2: -99-99 arr2: Antal element är 1: 22 arr3: Antal element är 7: 11 22 0 0 0 0 33 Implementation av metoder för dynamisk tänjbar arrayklass: ExtArray:: ExtArray() : bufsize(0) array = 0; //NULL-pekaren cout << "Hello default " << bufsize << endl; ExtArray:: ExtArray( int siz, int val ) : bufsize(siz) if ( bufsize > 0 ) else array = new int[bufsize]; for ( int i = 0; i < bufsize; i++ ) array[i] = val; cout << "Hello överlagrad " << bufsize << endl; array = 0; //NULL-pekare 32 33 ExtArray:: ExtArray( const ExtArray a ) : bufsize(a.bufsize) cout << "Hello kopiering: ny längd: " << bufsize << endl; void ExtArray:: resize( int n ) int *p = 0; if ( n > 0 ) // Om ny storlek ej är noll // Skapa array med n elem. // Fyll med element. if ( bufsize > 0 ) // Fyll resten med blanka. p = new int[n]; // Allokera nytt minne. // Kopiera element för element. array = new int[bufsize]; for ( int i = 0; i < bufsize; i++ ) array[i] = a.array[i]; int i; for ( i = 0; i < n i < bufsize; i++ ) p[i] = array[i]; else array = 0; //NULL for ( ; i < n; i++ ) p[i] = blank; ExtArray:: ~ExtArray() delete [] array; // Avallokera. array = p; // Sätt array. delete [] array; bufsize = n; // Sätt bufsize. 34 35
int ExtArray:: size() return bufsize; void ExtArray:: write() cout << "Antal element är " << bufsize; if ( bufsize > 0 ) cout << ":"; for ( int i = 0; i < bufsize; i++ ) cout << \t << array[i]; else cout << "."; cout << endl; void ExtArray:: assign( const ExtArray a ) // Tilldela: aktuella objektet = a. bufsize = a.bufsize; cout << "Hello assign " << bufsize << endl; if ( bufsize > 0 ) // Frigör minne. // Allokera nytt minne. // Kopiera elementen delete [] array; array = new int[bufsize]; for ( int i = 0; i < bufsize; i++ ) array[i] = a.array[i]; else array = 0; //NULL 36 37 void ExtArray:: addtoend( int newitem ) // Öka på arrayen med ett element till. resize(bufsize+1); // Fixar även bufsize++; Överkurs Pekarbaserad implementation av lista Vad är en länkad lista? first // Tilldela sista elem., dvs plats bufsize-1: array[bufsize-1] = newitem; void ExtArray:: change(int i, int num) if ( i < bufsize ) array[ i ] = num; link link link 4 3 5 Figur 2: Länkad lista. Består av ett antal noder eller listelement. Värdet kan vara av vilken typ som helst (int, double, klasstyp,...) Implementeras vanligen som objekt som binds samman med pekare. 38 39
Varje nod har två komponenter: värde samt en pekare till nästa element i listan. Har en början och ett slut. ett När en nod behövs så skaar vi en ny med hjälp av new och lägger in den i listan. Listans första element kallas listans huvud. Det brukar pekas ut av en speciell pekare. (Vi kallar den first). Det sista elementet har en tom pekare, dvs pekarvärde = 0 (NULL). Den markerar att listan är slut. Om man inte behöver noden så kan den återvinnas med delete. Då måste vi förstås trassla loss den ur listan först annars blir det inte så bra. Varför ha en länkad lista? Är enkelriktad. (Man kan ha dubbellänkade listor också...) Element kan tas bort och läggas till var som helst utan att andra element behöver yttas. Dynamisk växer fram vartefter den behövs; kan krympa och växa med tiden. Har aldrig någon bestämd maximal storlek. Fördelar: Mer dynamiskt än en array. Lätt att lägga till och ta bort saker. Kan hållas sorterad utan att noder behöver yttas. Ingen fast storlek. 40 41 Vad bör en länkad lista klara av? Nackdelar: Svårare att hitta en viss nod eftersom de inte numreras utan vi måste söka från början. Är en sekventiell struktur där vi bara kan gå framåt. Det nns varianter där vi kan backa också men det ändrar inte så mycket. En nod blir större än motsvarande arrayelement eftersom en pekare måste nnas. Andelen pekare i noden beror ju på hur resten ser ut. Om vi har en lista av tecken kommer 80 av listan att vara pekare, men om informationsdelen är större så minskar problemet. Vi har redan sett exempel på vilka operationer man önskar göra på listor (implementerad med en array). Samma saker vill vi nu kunna göra med länkade listor. En enkel lista bör klara av Skapa tom lista. Ta bort alla element, dvs göra sig själv till tom lista. Ta bort ett element ur listan. Lägga till ett nytt element, t ex sist. Indikera att ett element redan nns i listan eller ej. Indikera om listan är tom eller ej....etc. Detaljer kommer i OOP/C++-kursen! 42 43