Download as pdf or txt
Download as pdf or txt
You are on page 1of 68

Lists

Outline

We will now look at our first abstract data structure


– Relation: explicit linear ordering
– Operations
– Implementations of an abstract list with:
• Linked lists
• Arrays
– Memory requirements
– Strings as a special case
– The STL vector class
Definition

An Abstract List (or List ADT) is linearly ordered data where the
programmer explicitly defines the ordering

We will look at the most common operations that are usually


– The most obvious implementation is to use either an array or linked list
– These are, however, not always the most optimal
Operations

Operations at the kth entry of the list include:

Access to the object Erasing an object

Insertion of a new object Replacement of the object


Operations

Given access to the kth object, gain access to either the previous or
next object

Given two abstract lists, we may want to


– Concatenate the two lists
– Determine if one is a sub-list of the other
Locations and run times

The most obvious data structures for implementing an abstract list


are arrays and linked lists
– We will review the run time operations on these structures

We will consider the amount of time required to perform actions


such as finding, inserting new entries before or after, or erasing
entries at
– the first location (the front)
– an arbitrary (kth) location
– the last location (the back or nth)

The run times will be Θ(1), O(n) or Θ(n)


Lists
7

Linked List Basics

• Linked lists and arrays are similar since they both store collections
of data.
• The array's features all follow from its strategy of allocating the
memory for all its elements in one block of memory.
• Linked lists use an entirely different strategy: linked lists allocate
memory for each element separately and only when necessary.
Lists
8

Disadvantages of Arrays
1. The size of the array is fixed.
– In case of dynamically resizing the array from size S to 2S, we
need 3S units of available memory.
– Programmers allocate arrays which seem "large enough” This
strategy has two disadvantages: (a) most of the time there are
just 20% or 30% elements in the array and 70% of the space in
the array really is wasted. (b) If the program ever needs to
process more than the declared size, the code breaks.
2. Inserting (and deleting) elements into the middle of the array
is potentially expensive because existing elements need to be
shifted over to make room
Lists
9

Linked lists

• Linked lists are appropriate when the number of data elements to be


represented in the data structure at once is unpredictable.
• Linked lists are dynamic, so the length of a list can increase or
decrease as necessary.
• Each node does not necessarily follow the previous one physically
in the memory.
• Linked lists can be maintained in sorted order by inserting or
deleting an element at the proper point in the list.
Lists
10

Singly Linked Lists


head

next next next next

a b c d

First node Last node


Lists
11

Empty List
• Empty Linked list is a single pointer having the value of NULL.

head = NULL;

head
Lists
12

Basic Ideas

• Let’s assume that the node is given by the following type


declaration:
struct Node {
Object element;
Node *next;
};
Lists
13

Basic Linked List Operations

• List Traversal
• Searching a node
• Insert a node
• Delete a node
Lists
14

Traversing a linked list


Node *pWalker;
int count = 0;

cout <<“List contains:\n”;

for (pWalker=pHead; pWalker!=NULL;


pWalker = pWalker->next)
{
count ++;
cout << pWalker->element << endl;
}
Lists
15

Searching a node in a linked list


pCur = pHead;

// Search until target is found or we reach


// the end of list
while (pCur != NULL &&
pCur->element != target)
{
pCur = pCur->next;
}

//Determine if target is found


if (pCur) found = 1;
else found = 0;
Lists
16

Insertion in a linked list


a b
… …

current
x
tmp

tmp = new Node;


tmp->element = x;
tmp->next = current->next;
current->next = tmp;

Or simply (if Node has a constructor initializing its members):


current->next = new Node(x,current->next);
Lists
17

Deletion from a linked list

a x b
… …
current

Node *deletedNode = current->next;


current->next = current->next->next;
delete deletedNode;
Lists
18

Special Cases (1)

• Inserting before the first node (or to an empty list):


tmp = new Node;
tmp->element = x;
if (current == NULL){
tmp->next = head;
head = tmp;
}
else { // Adding in middle or at end
tmp->next = curent->next;
current->next = tmp;
}
Lists
19

Special Cases (2)

Node *deletedNode;
if (current == NULL){
// Deleting first node
deletedNode = head;
head = head ->next;
}
else{
// Deleting other nodes
deletedNode = current->next;
current->next = deletedNode ->next;
}
delete deletedNode;
Lists
20

Header Nodes

• One problem with the basic description: it assumes that whenever


an item x is removed (or inserted) some previous item is always
present.
• Consequently removal of the first item and inserting an item as a
new first node become special cases to consider.
• In order to avoid dealing with special cases: introduce a header
node (dummy node).
• A header node is an extra node in the list that holds no data but
serves to satisfy the requirement that every node has a previous
node.
Lists
21

List with a header node


header

a b c d

Empty List
header
Lists
22

Doubly Linked Lists


pHead

a b c

Advantages:
• Convenient to traverse the list backwards.
• Simplifies insertion and deletion because you no
longer have to refer to the previous node.
Disadvantage:
• Increase in space requirements.
Lists
23

Deletion
head current

oldNode = current;
oldNode->prev->next = oldNode->next;
oldNode->next->prev = oldNode->prev;
delete oldNode;
current = head;
Lists
24

Insertion
head current

newNode = new Node(x);


newNode->prev = current; newNode
newNode->next = current->next;
newNode->prev->next = newNode;
newNode->next->prev = newNode;
current = newNode;
Lists
25

The List ADT in C++

 A list is implemented as three separate classes:


1. List itself (List)
2. Node (ListNode)
3. Position iterator(ListItr)
Lists
26
Linked List Node
template <class Object>
class List; // Incomplete declaration.

template <class Object>


class ListItr; // Incomplete declaration.

template <class Object>


class ListNode
{
ListNode(const Object & theElement = Object(),
ListNode * n = NULL )
: element( theElement ), next( n ) { }
Object element;
ListNode *next;

friend class List<Object>;


friend class ListItr<Object>;
};
Lists
27
Iterator class for linked lists
template <class Object>
class ListItr
{
public:
ListItr( ) : current( NULL ) { }
bool isValid( ) const
{ return current != NULL; }
void advance( )
{ if(isValid( ) ) current = current->next; }
const Object & retrieve( ) const
{ if( !isValid( ) ) throw BadIterator( );
return current->element; }

private:
ListNode<Object> *current; // Current position

ListItr( ListNode<Object> *theNode )


: current( theNode ) { }

friend class List<Object>; //Grant access to constructor


};
Lists
28
List Class Interface
template <class Object>
class List
{
public:
List( );
List( const List & rhs );
~List( );

bool isEmpty( ) const;


void makeEmpty( );
ListItr<Object> zeroth( ) const;
ListItr<Object> first( ) const;
void insert(const Object &x, const ListItr<Object> & p);
ListItr<Object> find( const Object & x ) const;
ListItr<Object> findPrevious( const Object & x ) const;
void remove( const Object & x );

const List & operator=( const List & rhs );

private:
ListNode<Object> *header;
};
Lists
29
Some List one-liners
/* Construct the list */
template <class Object>
List<Object>::List( )
{
header = new ListNode<Object>;
}
/* Test if the list is logically empty.
* return true if empty, false otherwise.*/
template <class Object>
bool List<Object>::isEmpty( ) const
{
return header->next == NULL;
}

29
Lists
30
/* Return an iterator representing the header node. */
template <class Object>
ListItr<Object> List<Object>::zeroth( ) const
{
return ListItr<Object>( header );
}
/* Return an iterator representing the first node in
the list. This operation is valid for empty lists.*/
template <class Object>
ListItr<Object> List<Object>::first( ) const
{
return ListItr<Object>( header->next );
}

30
Lists
31
Other List methods
template <class Object>
ListItr<Object> List<Object>::find( const Object & x )
const
{
ListNode<Object> *itr = header->next;

while(itr != NULL && itr->element != x)


itr = itr->next;

return ListItr<Object>( itr );


}

31
Lists
32

Deletion routine
/* Remove the first occurrence of an item x. */
template <class Object>
void List<Object>::remove( const Object & x )
{
ListItr<Object> p = findPrevious( x );

if( p.current->next != NULL )


{
ListNode<Object> *oldNode = p.current->next;
p.current->next = p.current->next->next;
delete oldNode;
}
}

32
Lists
33

Finding the previous node


/* Return iterator prior to the first node containing an
item x. */
template <class Object>
ListItr<Object> List<Object>::findPrevious( const Object
& x ) const
{
ListNode<Object> *itr = header;

while(itr->next!=NULL && itr->next->element!=x)


itr = itr->next;

return ListItr<Object>( itr );


}

33
Lists
34

Insertion routine
/* Insert item x after p */
template <class Object>
void List<Object>::insert(const Object & x,
const ListItr<Object> & p )
{
if( p.current != NULL )
p.current->next = new ListNode<Object>(x,
p.current->next );
}

34
Lists
35

Memory Reclamation
/* Make the list logically empty */
template <class Object>
void List<Object>::makeEmpty( )
{
while( !isEmpty( ) )
remove( first( ).retrieve( ) );
}
/* Destructor */
template <class Object>
List<Object>:: ~List( )
{
makeEmpty( );
delete header;
}

35
Lists
36

operator =
/* Deep copy of linked lists. */
template <class Object>
const List<Object> & List<Object>::operator=( const
List<Object> & rhs )
{
ListItr<Object> ritr = rhs.first( );
ListItr<Object> itr = zeroth( );

if( this != &rhs )


{
makeEmpty( );
for( ; ritr.isValid(); ritr.advance(), itr.advance())
insert( ritr.retrieve( ), itr );
}
return *this;
}

36
Lists
37

Copy constructor
/* copy constructor. */
template <class Object>
List<Object>::List( const List<Object> & rhs )
{
header = new ListNode<Object>;
*this = rhs; // operator= is used here
}

37
Lists
38

Testing Linked List Interface


#include <iostream.h>
#include "LinkedList.h“

// Simple print method

template <class Object>


void printList( const List<Object> & theList )
{
if( theList.isEmpty( ) )
cout << "Empty list" << endl;
else {
ListItr<Object> itr = theList.first( );
for( ; itr.isValid( ); itr.advance( ) )
cout << itr.retrieve( ) << " ";
}
cout << endl;
}

38
Lists
int main( ) 39
{ List<int> theList;
ListItr<int> theItr = theList.zeroth( );
int i;
printList( theList );
for( i = 0; i < 10; i++ )
{ theList.insert( i, theItr );
printList( theList );
theItr.advance( );
}
for( i = 0; i < 10; i += 2 )
theList.remove( i );
for( i = 0; i < 10; i++ )
if((i % 2 == 0)!=(theList.find(i).isValid()))
cout << "Find fails!" << endl;
cout << "Finished deletions" << endl;
printList( theList );
List<int> list2;
list2 = theList;
printList( list2 );
return 0;
}

39
Lists

Comparing Array-Based and Pointer-Based 40

Implementations
• Size
– Increasing the size of a resizable array can waste storage and time
• Storage requirements
– Array-based implementations require less memory than a pointer-
based ones

40
Lists

Comparing Array-Based and Pointer-Based 41

Implementations
• Access time
– Array-based: constant access time
– Pointer-based: the time to access the ith node depends on i
• Insertion and deletions
– Array-based: require shifting of data
– Pointer-based: require a list traversal
Lists
Saving and Restoring a Linked List by Using a42
File
• Use an external file to preserve the list
• Do not write pointers to a file, only data
• Recreate the list from the file by placing each item at the end of the
list
– Use a tail pointer to facilitate adding nodes to the end of the list
– Treat the first insertion as a special case by setting the tail to head
Passing a Linked List to a Function
• A function with access to a linked list’s head pointer has access
to the entire list
• Pass the head pointer to a function as a reference argument

MHA,2015,AAiT 43
Circular Linked Lists
• Last node references the first node
• Every node has a successor
• No node in a circular linked list contains NULL

A circular linked list

MHA,2015,AAiT 44
Lists
45

Circular Doubly Linked Lists


• Circular doubly linked list
– prev pointer of the dummy head node points to the last node
– next reference of the last node points to the dummy head node
– No special cases for insertions and deletions
Lists
46

Circular Doubly Linked Lists

(a) A circular doubly linked list with a dummy head node


(b) An empty list with a dummy head node

46
Lists
47

Processing Linked Lists Recursively


• Recursive strategy to display a list
– Write the first node of the list
– Write the list minus its first node
• Recursive strategies to display a list backward
– writeListBackward strategy
• Write the last node of the list
• Write the list minus its last node backward

47
Lists
48

Processing Linked Lists Recursively


– writeListBackward2 strategy
• Write the list minus its first node backward
• Write the first node of the list
• Recursive view of a sorted linked list
– The linked list to which head points is a sorted list if
• head is NULL or
• head->next is NULL or
• head->item < head->next->item, and
head->next points to a sorted linked list
Linked lists

We will consider these for


– Singly linked lists
– Doubly linked lists
Singly linked list

Front/1st node kth node Back/nth node


Find Θ(1) Ο n)( * Θ(1)
Insert Before Θ 1)( Ο n)( * Θ(n)
Insert After Θ(1) Θ(1)* Θ(1)
Replace Θ(1) Θ(1)* Θ(1)
Erase Θ(1) Ο(n)* Θ(n)
Next Θ(1) Θ(1)* n/a
Previous n/a Ο(n)* Θ(n)
* These assume we have already accessed the kth entry—an O(n) operation
Singly linked list

Front/1st node kth node Back/nth node


Find Θ(1) Ο n)( * Θ(1)
Insert Before Θ 1)( Θ(1)* Θ(1)
Insert After Θ(1) Θ(1)* Θ(1)
Replace Θ(1) Θ(1)* Θ(1)
Erase Θ(1) Θ(1)* Θ(n)
Next Θ(1) Θ(1)* n/a
Previous n/a Ο(n)* Θ(n)

By replacing the value in the node in question, we can speed things up


– useful for interviews
Doubly linked lists

Front/1st node kth node Back/nth node


Find Θ(1) Ο n)( * Θ(1)
Insert Before Θ 1)( Θ(1)* Θ(1)
Insert After Θ(1) Θ(1)* Θ(1)
Replace Θ(1) Θ(1)* Θ(1)
Erase Θ(1) Θ(1)* Θ(1)
Next Θ(1) Θ(1)* n/a
Previous n/a Θ(1)* Θ(1)
* These assume we have already accessed the kth entry—an O(n) operation
Doubly linked lists
Accessing the kth entry is O(n)
kth node
Insert Before Θ(1)
Insert After Θ(1)
Replace Θ(1)
Erase Θ(1)
Next Θ(1)
Previous Θ(1)
Other operations on linked lists

Other operations on linked lists include:


– Allocation and deallocating the memory requires Θ(n) time
– Concatenating two linked lists can be done in Θ(1)
• This requires a tail pointer
Arrays

We will consider these operations for arrays, including:


– Standard or one-ended arrays
– Two-ended arrays
Standard arrays

We will consider these operations for arrays, including:


– Standard or one-ended arrays
– Two-ended arrays
Run times

Accessing Insert or erase at the


the kth entry Front kth entry Back
Singly linked lists Θ(1) or Θ(n)
O(n) Θ(1) Θ(1)*
Doubly linked lists Θ(1)
Arrays Θ(n)
Θ(1) O(n) Θ(1)
Two-ended arrays Θ(1)

* Assume we have a pointer to this node


Data Structures

In general, we will only use these basic data structures if we can


restrict ourselves to operations that execute in Q(1) time, as the only
alternative is O(n) or Q(n)

Interview question: in a singly linked list, can you speed up the two
O(n) operations of
– Inserting before an arbitrary node?
– Erasing any node that is not the last node?

If you can replace the contents of a node, the answer is “yes”


– Replace the contents of the current node with the new entry and insert
after the current node
– Copy the contents of the next node into the current node and erase the
next node
Memory usage versus run times

All of these data structures require Q(n) memory


– Using a two-ended array requires one more member variable, Q(1), in
order to significantly speed up certain operations
– Using a doubly linked list, however, required Q(n) additional memory to
speed up other operations
Memory usage versus run times

As well as determining run times, we are also interested in memory


usage
In general, there is an interesting relationship between memory and
time efficiency

For a data structure/algorithm:


– Improving the run time usually
requires more memory
– Reducing the required memory
usually requires more run time
Memory usage versus run times

Warning: programmers often mistake this to suggest that given any


solution to a problem, any solution which may be faster must require
more memory

This guideline not true in general: there may be different data


structures and/or algorithms which are both faster and require less
memory
– This requires thought and research
The sizeof Operator

In order to determine memory usage, we must know the memory


usage of the various built-in data types and classes
– The sizeof operator in C++ returns the number of bytes occupied by a
data type
– This value is determined at compile time
• It is not a function
The sizeof Operator

#include <iostream>
using namespace std;

int main() {
cout << "bool " << sizeof( bool ) << endl;
cout << "char " << sizeof( char ) << endl;
cout << "short " << sizeof( short ) << endl;
cout << "int " << sizeof( int ) << endl;
cout << "char * " << sizeof( char * ) << endl;
cout << "int * " << sizeof( int * ) << endl;
cout << "double " << sizeof( double ) << endl;
cout << "int[10] " << sizeof( int[10] ) << endl;

return 0; {eceunix:1} ./a.out # output


} bool 1
char 1
short 2
int 4
char * 4
int * 4
double 8
int[10] 40
{eceunix:2}
Abstract Strings

A specialization of an Abstract List is an Abstract String:


– The entries are restricted to characters from a finite alphabet
– This includes regular strings “Hello world!”

The restriction using an alphabet emphasizes specific operations


that would seldom be used otherwise
– Substrings, matching substrings, string concatenations

It also allows more efficient implementations


– String searching/matching algorithms
– Regular expressions
Standard Template Library

In this course, you must understand each data structure and their
associated algorithms
– In industry, you will use other implementations of these structures

The C++ Standard Template Library (STL) has an implementation of


the vector data structure
– Excellent reference:
http://www.cplusplus.com/reference/stl/vector/
Standard Template Library

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v( 10, 0 );

cout << "Is the vector empty? " << v.empty() << endl;
cout << "Size of vector: " << v.size() << endl;
$ g++ vec.cpp
$ ./a.out
v[0] = 42; Is the vector empty? 0
v[9] = 91; Size of vector: 10
v[0] = 42
for ( int k = 0; k < 10; ++k ) { v[1] = 0
cout << "v[" << k << "] = " << v[k] << endl; v[2] = 0
} v[3] = 0
v[4] = 0
v[5] = 0
return 0;
v[6] = 0
} v[7] = 0
v[8] = 0
v[9] = 91
$
Summary

In this topic, we have introduced Abstract Lists


– Explicit linear orderings
– Implementable with arrays or linked lists
• Each has their limitations
• Introduced modifications to reduce run times down to Q(1)
– Discussed memory usage and the sizeof operator
– Looked at the String ADT
– Looked at the vector class in the STL
References

[1] Donald E. Knuth, The Art of Computer Programming, Volume 1:


Fundamental Algorithms, 3rd Ed., Addison Wesley, 1997, §2.2.1, p.238.
[2] Weiss, Data Structures and Algorithm Analysis in C++, 4th Ed., Addison
Wesley, §3.3.1, p.75.

You might also like