Datorer och programmering TDB2: Lista - en dynamisk datastruktur 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: ett index. • • • & $' 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. ∗ Delvis knyckta delar av Olle Eriksson ' 0-0 1 2 3 4 5 8 4 6 4 0 -2 maxList-1 3 Indexerad lista 8 4 0 6 4 -2 3 Länkad lista & 1 Vad bör en lista klara av? 0 Figur 1: Länkad lista. % $ 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. En enkel lista bör klara av • Tala om sin aktuella längd, dvs antalet listelement. • 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. Lägga till ett nytt element, t ex sist. • %& • • 2 $ Indexerad lista: Varje element åtkoms med ∗ Eva Pärt-Enander ' Indikera att ett element redan nns i listan eller ej. Indikera om listan är tom eller ej. 3 % $' ' Olika sorters listor Array-baserad implementation av lista Ordnad lista: eller sorterad lista är en lista Array-baserade listor representeras av tre delar: 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 gemensam längtan att träa producenten av en resurs. & ' 4 Exempel på klassdeklaration för klassen med statisk array: SimpleList 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(); int empty(); void write(); }; 6 $ • 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. 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. %& $' 5 Traversera en lista % $ 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. Om listan är tom, dvs size är 0, så kommer loopen ej att utföras. %& 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. void SimpleList:: addToEnd( int newItem ) { if ( size < maxList ) { listItem[size] = newItem; size++; } } & ' 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. Algoritm: Starta sist 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 { int current = size - 1; // Aktuellt index. // Leta efter rätt plats, skifta. • > newItem ) { listItem[current + 1] = listItem[current]; current--; 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). // Nytt tal skall in efter. listItem[current + 1] = newItem; // Öka listlängd. Antagandet har gjorts att listan inte är full, dvs . & size < maxList 10 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. • } } % $ Processen att ta bort ett element ur en lista beror också på vilken sorts lista man har. while ( current >=0 && listItem[current] size++; 9 Ta bort ett element från en lista void SimpleList:: insert( int newItem ) $ %& 11 % $' ' $ 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: 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. & ' 12 Exempel på huvudprogram som använder SimpleList: 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). %& $' 13 % $ int main() { SimpleList list; list.insert( 34 ); list.remove(55); list.insert( 27 ); list.insert( 345 ); if ( list.empty() ) list.insert( 58 ); cout << "Tom lista!!!" << endl; list.write(); return 0; list.clear(); } list.write(); ger följande körning: SimpleList another; another.insert( 55 ); another.insert( 78 ); another.insert( 15 ); & list = another; list.write(); list.remove(15); list.remove(78); 14 Antal element är 4: 27 34 58 15 55 78 345 Antal element är 0. Antal element är 3: Tom lista!!! %& 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() { int SimpleList:: currentSize() { size = 0; return size; } } int SimpleList:: member( int anyItem ) { for ( int i = 0; i < size; i++ ) int SimpleList:: empty() { return (size == 0); { if ( listItem[i] == anyItem ) & ' return 1; // TRUE } return 0; // FALSE } 16 void SimpleList:: write() { cout << "Antal element är " << size; if ( !empty() ) %& $' 17 % $ Avancerad listklass med dynamisk tänjbar array Man skulle i SimpleList kunna ha en dynamisk array listItem istället för statisk: private: { int *listItem; cout << ":"; int maxList; for ( int i = 0; i < size; i++ ) cout << '\t' << listItem[i]; } else cout << "."; cout << endl; } & // 1 om tom, annars 0. } 18 int size; 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... %& 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. $' 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. Vi skall studera klassen ExtArray (extensible array) lite närmare nu: class ExtArray { private: & ' int *array; // Dynamisk array. int bufSize; // Aktuella längden. public: ... 20 Tilldelningsoperatorn (=): • då motsvarande klass inte innehåller pekarattribut. Datavärde för datavärde kopieras då över från objekt2 till objekt1. • %& $' 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). & 22 21 Frågor: Vad händer om: man gör delete [] på ena pekarvärdet? räckvidderna (scope) är olika stora för Den inbyggda tilldelningsoperatorn fungerar utmärkt för varianten objekt1 = objekt2; $ % $ objekt1, objekt2? • Botemedel: Man skriver en egen tilldelningsfunktion, t ex: void ExtArray:: assign( const ExtArray & a ); 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. %& 23 % $' ' $ Kopieringskonstruktorn • Alternativt botemedel: Skriv en överlagrad tilldelningsoperator så att • 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 initierar objektvariabler, vid ickereferensparameteröverföring till funktioner eller när man returnerar objektvärden från funktioner. • Både den inbyggda kopieringskonstruktorn och tilldelningsoperatorn gör alltså grund kopiering! objekt1 = objekt2; innebär djup kopiering. Deklareras i stil med det här: const ExtArray & ExtArray:: operator= ( const ExtArray & a ) :-| Inte helt trivialt med C++.... :-0 & ' • 24 Botemedel: Man skriver en egen %& $' Kod för hela klassen ExtArray + main + körning: kopieringskonstruktor som kopierar elementvis (dvs djupt). // Extensible array. Tänjbar array. const int blank = 0; class ExtArray ExtArray( const ExtArray & a ); { medför att t ex private: ExtArray arr2; int *array; // default. ExtArray arr3 = ExtArray( 2, -99 ); //överlagra ExtArray arr4(arr3); // kopierings. ExtArray arr5 = arr2; // kopierings. int bufSize; public: ExtArray(); ExtArray( int siz, int val ); ~ExtArray(); fungerar! ExtArray( const ExtArray & a ); Summering: Om man har pekare som attribut, så bör man alltså ha egendenierad & 25 % $ destruktor, kopieringskonstruktor, tilldelningsoperator/metod. void change(int i, int num); int size(); %& void resize( int n ); 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 << "arr1: "; arr1.write(); cout << "arr5: "; arr5.write(); cout << "arr2: "; arr2.write(); cout << "arr3: "; arr3.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 << "arr3: %& $' arr3.change(0,11); arr3.change(1,22); 28 29 Körning ger: Hello överlagrad 5 cout << "arr3: ändra längd dessutom" << endl; arr3.resize(6); cout << "arr2: "; arr2.write(); cout << "arr3: "; arr3.write(); cout << "Lägg till element 22, 33 på " << Hello default 0 Hello överlagrad 2 Hello kopiering: ny längd: 2 cout << "arr1: "; arr1.write(); "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(); & % $ byt värden till 11 och 22..." << endl; 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 arr3: Antal element är 2: -99 arr3: %& -99 byt värden till 11 och 22... arr1: Antal element är 2: -99 return 0; -99 arr2: Antal element är 0. -99 arr2: Antal element är 0. } arr3: Antal element är 2: 11 22 arr1: Antal element är 2: -99 -99 arr3: ändra längd dessutom arr2: Antal element är 0. 30 31 % ' $' Implementation av metoder för dynamisk tänjbar arrayklass: $ ExtArray:: ExtArray() : bufSize(0) { array = 0; //NULL-pekaren cout << "Hello default " << bufSize << endl; } arr3: Antal element är 6: 11 22 0 0 0 ExtArray:: ExtArray( int siz, int val ) 0 Lägg till element 22, 33 på slutet i arr2, arr3: arr1: Antal element är 2: -99 arr2: Antal element är 1: -99 : bufSize(siz) { if ( bufSize > 0 ) 22 { arr3: Antal element är 7: 11 22 0 0 0 0 array = new int[bufSize]; 33 for ( int i = 0; i < bufSize; i++ ) array[i] = val; } else & ' array = 0; //NULL-pekare } } 32 33 void ExtArray:: resize( int n ) { int *p = 0; bufSize(a.bufSize) if ( n > 0 ) { // Om ny storlek ej är noll { cout << "Hello kopiering: ny längd: " // Skapa array med n elem. << bufSize << endl; // Fyll med element. // Fyll resten med blanka . if ( bufSize > 0 ) p = new int[n]; { // Allokera nytt minne. int i; // Kopiera element för element. for ( i = 0; i < n array = new int[bufSize]; && i < bufSize; i++ ) for ( int i = 0; i < bufSize; i++ ) p[i] = array[i]; array[i] = a.array[i]; } for ( ; i < n; i++ ) else p[i] = blank; array = 0; //NULL } } & % $ cout << "Hello överlagrad " << bufSize << endl; ExtArray:: ExtArray( const ExtArray & a ) : %& $' { ExtArray:: ~ExtArray() { delete [] array; %& delete [] array; // Avallokera. array = p; // Sätt array. bufSize = n; // Sätt bufSize. } } 34 35 % ' $' void ExtArray:: assign( const ExtArray & a ) $ { int ExtArray:: size() // Tilldela: aktuella objektet = a. { return bufSize; bufSize = a.bufSize; } cout << "Hello assign " << bufSize << endl; void ExtArray:: write() if ( bufSize > 0 ) { { cout << "Antal element är " << bufSize; // Frigör minne. if ( bufSize > 0 ) // Allokera nytt minne. { // Kopiera elementen cout << ":"; delete [] array; array = new int[bufSize]; for ( int i = 0; i < bufSize; i++ ) cout << '\t' << array[i]; for ( int i = 0; i < bufSize; i++ ) } array[i] = a.array[i]; else } cout << "."; & ' %& $' else cout << endl; array = 0; //NULL } } 36 37 Överkurs Pekarbaserad implementation av lista void ExtArray:: addToEnd( int newItem ) % $ Vad är en länkad lista? { // Öka på arrayen med ett element till. first resize(bufSize+1); // Fixar även bufSize++; // Tilldela sista elem., dvs plats bufSize-1: array[bufSize-1] = newItem; link } link 4 3 link 5 void ExtArray:: change(int i, int num) { Figur 2: Länkad lista. if ( i < bufSize ) array[ i ] = num; • } & %& • 38 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. 39 % ' • Varje nod har två komponenter: ett värde samt en pekare till nästa element i listan. • Har en början och ett slut. • 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. • Ä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. 40 • När en nod behövs så skaar vi en ny med hjälp av new och lägger in den i listan. • 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? • 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. %& $' 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 %