280 likes | 305 Views
Explore linked lists: creating nodes, manipulating content, implementing stack ADT, and more with dynamic memory allocation in C++.
E N D
CSE 143 Linked Lists [Chapter 8.1-8.6, 8.8]
Linked Lists • A linked list is a collection of dynamically allocated nodes • Each node contains at least one member that links to another node in the list. • In the simplest case, each node has two members: a data item and a link field which points to the successor node. • Example: a list of 3 integers: 4 8 16 head
Creating Nodes • First we’ll declare a struct which we’ll use to represent a node: • struct Node { • int data; • Node* next; • }; • Now we can create new nodes: • Node* p; • p = new Node; • p->data = 100; // shorthand for: (*p).data = 100 • p->next = NULL; // shorthand for: (*p).next = NULL • Note the use of the -> operator
Manipulating Nodes • Draw the picture that results from the following code: • Node* front, * temp; • front = new Node; • front->data = 1; • front->next = new Node; // adding nodes • front->next->data = 2; • front->next->next = NULL; • temp = front; // deleting nodes • front = front->next; • delete temp;
Using Linked Lists • Let’s write a Stack ADT which uses a linked list: • struct Node; // partial or "forward" declaration. • //the full decl is hidden in the impl. file • class IntStack { • public: • IntStack(); • void push(int item); • int pop(); • bool isEmpty(); • bool isFull(); • private: • Node* items; • };
Stack Implementation: • struct Node { • int data; • Node* next; • }; • IntStack::IntStack() { items = NULL; } • void IntStack::push(int item) { • Node* temp = new Node; • temp->data = item; • temp->next = items; • items = temp; • }
Stack Implementation (2) • int IntStack::pop() { • assert(!isEmpty()); • Node* p = items; • int rval = p->data; • items = items->next; • delete p; • return rval; • } • bool IntStack::isEmpty() { return (items == NULL); } • bool IntStack::isFull() { return false; }
Stack Wrapup • What’s missing? • top() • sizeOf() - how would you write this? • Dynamic memory suite: copy constructor, destructor, assignment operator… • What are advantages/disadvantages of this implementation of a stack? • We’d like to separate out the linked list from the Stack. • The “right” way to do this is to re-implement our List ADT using a linked list
Implementing a List ADT • Again, we’ll declare a struct to represent a node: • struct Node { • int data; • Node* next; • }; • Now our List ADT will look like this: items 4 8 16 cursor
List Specification • We can use the same old List specification with a minor change: • struct Node; // again, a partial declaration • class IntList { • public: • IntList(); // The public interface remains • int sizeOf(); // the same! • void reset(); • // etc… • private: • Node* items; // Different representation!! • Node* cursor; • };
Implementation • We need to implement at least the following suite of member functions: • IntList() // constructor • bool isEmpty(); // predicates • bool isFull(); • int data(); • int sizeOf(); • void reset(); // cursor manipulators • void advance(); • bool endOfList(); • void insertBefore(int item); // insertion & deletion • void insertAfter(int item); • void deleteItem();
Implementation (1) • IntList::IntList() { • cursor = items = NULL; • } • bool IntList::isEmpty() { • return (items==NULL); • } • bool IntList::isFull() { • return false; // Can we do better than this? • } • bool IntList::endOfList() { • return cursor==NULL; • //doesn't quite match original List semantics • }
Implementation (2) • void IntList::advance() { • assert(!endOfList()); • cursor = cursor->next; • } • void IntList::reset() { cursor = items; } • int IntList::sizeOf() { • int i=0; • Node* p; • for (Node* p = items; p != NULL; p = p->next) • i++; • return i; • }
after implementation (1) • Inserting after the cursor. This code is a little broken... • void IntList::insertAfter(int item) { • Node* nodePtr = new Node; • nodePtr->data = item; • nodePtr->next = cursor->next; • cursor->next = nodePtr; • cursor = nodePtr; • } items 4 8 16 cursor
Examples items 4 8 16 cursor items 4 8 16 cursor
after implementation fixed: • void IntList::insertAfter(int item) { • assert(items == NULL || !endOfList()); • Node* nodePtr = new Node; • nodePtr->data = item; • if (items == NULL) { // empty list • nodePtr->next = NULL; • items = nodePtr; • } • else { • nodePtr->next = cursor->next; • cursor->next = nodePtr; • } • cursor = nodePtr; • }
before Implementation • Inserting before the cursor is tricky too. • void IntList::insertBefore(int item) { • Node* nodePtr = new Node; • nodePtr->data = item; • nodePtr->next = cursor; • if (cursor == items) // cursor points to head • items = nodePtr; • else { • Node* prevPtr = Previous(); • prevPtr->next = nodePtr; • } • cursor = nodePtr; • }
delete Implementation • Case 1: cursor points at first item • Case 2: cursor points somewhere else • void IntList::deleteItem() { • assert(cursor != NULL); // cursor must be valid • Node* temp = cursor; • if (cursor == items) • items = items->next; • else { • Node* previous = Previous(); • previous->next = cursor->next; • } • cursor = cursor->next; • delete temp; • }
before and delete helper • The above code depends on a helper member function Previous(), which we’ll make private: • // return the pointer to the node previous to the • // node the cursor points at... • Node* IntList::Previous() • { • Node* p = items; • while (p->next != cursor) • p = p->next; • return p; • }
Exercise 1: Manipulations • Draw the picture that results from this code: • IntList aList; • for (i=0; i<4; i++) • aList.insertAfter(i); • for (aList.reset(); !aList.endOfList(); • aList.advance()) • cout << aList.data();
Exercise 2: Traversals • Count the occurrences of an item: • int occurrencesOf(List& l, int item) { int count = 0; • for (l.reset(); !l.endOfList(); l.advance()) • if (l.data() == item) • count++; • return count; • }
Exercise 3: Deletion • A function to find and delete an item: • bool • findAndDelete(IntList& l, int item) { • for (l.reset(); !l.endOfList(); l.advance()) • if (l.data() == item) { • l.delete(); • return true; • } • return false; • }
Variations on a theme • Doubly linked lists • Makes finding the previous pointer a breeze • Takes a little more space and complexity to manage the extra pointers • Circular lists • Can remove some special cases • Head and tail pointers. • Good for queues • Dummy nodes • Can remove some special cases
Some Issues • Previous( ) is an expensive operation • Could maintain a "previous" pointer • Probably want our full dynamic memory suite: • Copy constructor • Deep copying assignment • Destructor • Making deep copies of large lists is expensive. • Solutions: always pass by reference. Can enforce this by making the copy constructor private...
A Bigger Issue • Our list is not as generic as it could be: • It has the name of the element type (in this case, ints) hard-coded into it. • Solutions: • Rewrite the code (a poor - but common - solution) • Templates (cf. Lippman) • Inheritance (Chapters 10 and 11)
List ADT Summary • We’ve seen 2 implementations of a List ADT: • Arrays: support efficient random access, but inefficient insertion and deletion, because we need to shift elements around • Linked-lists: support efficient insertion and deletion, but inefficient random access • What kind of tradeoffs are we making? • List as an ADT vs. a List as a data structure.
Linked-Lists Summary • We’ve seen a very simple linked-list implementation. • Examples: Stack ADT, List ADT (more general) • Many variations: singly vs doubly linked, head and/or tail pointers, list cursors, dummy nodes • Pick the structure to fit the problem being solved • Common operations: traversals, insertions, deletions