CSE 211: Data Structures
Lecture Notes VII
LINKED LISTS
In the previous lectures we have seen the representation of ordered lists using an array and sequential
mapping. These representations had the property that successive nodes of the data object were stored
in a fixed distance apart.
For example: If the element a
ij of a table was stored at location Lij then a ij+1 was at the location Lij+cfor a known constant c.
Problems:
•
The sequential storage schemes are adequate if one wished to access for given node in a table,
insertion or deletions of nodes within a stack or queue. However insertions or deletions of arbitrary
elements become expensive.
•
If we have several ordered lists of varying sizes, by storing each list in a different array of
maximum size, storage may be wasted. Additionally, in several cases we will need more than
accessing the top item as in the stacks or inserting to the tail and accessing the head (queues). We
need more general operations such as finding or removing any item in the list, inserting a new item
at any point.
Solution:
The problem of data movement in sequential representations may be solved by using linked
representations and dynamic memory allocation. Unlike a sequential representation where successive
items of a list are located a fixed distance apart, in a linked representation each item of the list may be
placed anywhere in memory. To access elements in the list in the correct order, with each element we
store the location of the next element in that list. Associated with each data item in a linked
representation there is a pointer to the next item.
In modern languages providing built-in support for linked structures, the compiler associates a special
storage with an object program which it leaves initially unassigned to any program variable. This area
is usually called the heap or the dynamic storage pool. There is a heap manager program for the
allocation of this storage from the heap at execution time. The allocated space can be referenced using
pointers. Remember that pointer is just an abstraction for a machine address.
Linked List Building Blocks: A linked list is an ordered sequence of elements called nodes. A list has a
beginning, or head and an end , or tail. Every node on the list is the same type, although that type can
take different forms.
A central property of linked lists is their dynamic nature. Nodes can be added to or removed from a list
at any time. Because the number of nodes cannot be predicted before run time. You should use
pointers, rather than arrays, to achieve an efficient and reliable implementation.
Each node of a list consists of two parts. The first part holds the data. The first part holds the data. The
data can be a simple variable or, more generally a structure ( or pointer to a structure) of some type.
The second part is a pointer that indicatesthe location of the next node of the list. The nodes of a list
can be implemented by a recursive structure.
Consider the case where a single integer varaible is to be stored at each node of the list.
In this case the integer variable item holds the actual data in each node, and the pointer variable next
holds the address of the next node.
In general, the item part may contain several pieces of data defined in a strcut type. We can generalize
the list definition above as:
We assume that info_type has been defined before.
The problem with this kind of implementation is that for each type of list you must assign the
individual components one by one when transfering information to and from the nodes.
Ex. if s1 and s2 are both structures. In C, you cannot say s1=s2. You can assign only a single structure
component in each statement. You can however write a function called assign and call it whenever
needed.
assign(s1,s2);
info_type s1,s2;
{
/* assign each component of struct 2 to struct 1 */
}
This function carries out the detailed transfer of the individual structure components from 21 to s1.
You can then build a series of generic list manipulation functions that use assign.
struct simplenode {
int item;
struct simplenode *next;};
typedef struct simplenode simple_t;
class simplenode {
int item;
simplenode *next;
...
};
struct generalList {
infoType item;
struct generalList
*next; };
typedef struct generalList glist_t;
template <class infoType>
class generalList {
infoType item;
generalList *next;
...
};
item nextThe problem with this approach is the performance. You will be transfering data among different
memory locations. However you only need to transfer the address of the blocks of data corresponding
to the structures.
Solution: use a structure composed of a pair of pointers. The first pointer points to the data, the second
points to the next item.
Manipulation of Linked Lists
The manipulation of linked lists involves allocating storage for nodes (in C using malloc) and
assigning data and/or pointers so that the list is properly formed. List's boundary conditions must be
decided. In other words how are the head and tail of a list will be indicated. For the representation of
the tail NULL pointer can be used (show it on the example) but for the head different approaches are
possible.
•
Define a pointer variable to hold the starting adress of the list.
•
Define a special header node with the same structure. In this case the next element of the header
node indicates the beginning of the list. (item part is unused - a small waste)
•
Define a header node with a separate structure which holds exactly the information required. In this
case other information related to the list such as number of elements in the list can be added to the
header.
struct node {
infoType
* item;
struct node
*next; };
typedef struct node node_type;
template <class infoType>
class Node {
infoType * item;
Node
*next;
...
};
item nextIf a symmetric header is used:
If a custom head is used: (length, last, first)
Operations on One-way Lists and their Implementations
Allocating Memory
When an array is defined, storage is automatically allocated by the compiler. When you are using
pointers, however you must allocate the storage. In C, two standard library functions are provided for
this purpose.
malloc--- allocates a single piece of storage space and leaves it uninitialized
calloc() -- allocates entire set of storage locations and fills the total storage with zero
char *malloc(size) --- allocates size number of bytes and returns a pointer to the first
Although it returns a character pointer it can be used to allocate space for any type of element not just
characaters by applying a simple type casting
Size is the value returned by the sizeof operator
•
sizeof expression
sizeof(type name)
struct head{
int length;
node_type *first, *last; }
typedef struct head head_type;
template <class infoType>
class LinkedList{
int length;
Node<infoType> *first,*last;
int insert(infoType *data)
int append(infoType *data)
int delete(Node *ptr)
… }
•
Type casting:
new_variable = (type_name) old_variable
We will define a fucntion called MALLOC and use it in the algorithms
Creating a new list
1.
allocate memory for header node
2.
set length to 0
3.
set first and last nodes to NULL
Inserting a new element at the beginning of a list
A) Create a storage for the new node
1.
Create storage for a new list node;
2.
Assign appropriate pointer values to the new node
3.
Assign new values to the components of the header node.
Example given at the lecture.
#define MALLOC(x) ((x *) malloc(sizeof(x)))
float *place; /* A float pointer */
place = (float *) malloc (sizeof(float));
place = MALLOC(float);
place = new float[1];
head_type *create(){
head_type *new_node;
if (new _node = MALLOC(head_type) ) {
new_node ->length = 0;
new_node -> first = new_node -> last = NULL;
}
return(new _node); /* adress of new list */
}
/* create a storage for the new node */
node_type *alloc_node(info_type * val, node_type * ptr)
{
node_type *new_node;
if (new_node = MALLOC(node_type)) {
new_node -> item = val;
new_node -> next = ptr;}
return(new_node);
B) Inserting a new element at the beginning of a list
1.
Allocate memory for the new node and set it (if possible)
2.
Link the first pointer of header to the new node (?last)
3.
Increment length by 1
Appending a new item to the end of a list
Another common operation appends a new item to the end of a list. The before and after situations are
diagrammed below. You know where to find the current last node in the list by means of the last
pointer, which is maintained in the header block. Thus you only need to allocate memory for a new
node and rearrange pointers accordingly.
int insert_node (info_type *data
,
head_type *list) {
node_type *new_node;
if (new _node = alloc_node(data, list->first)) {
list->first = new_node; /* link in new node */
if (list->length == 0) /* if this is the first node */
list-> last = new_node; /* set last pointer to new */
list->length ++;
return(1); /* success */
} else return(0); /* error */ }
int append (info_type *data
, head_type *list
) {
node_type *new_node;/* append data to end of list */
if (new_node=alloc_node(data, NULL){
/* allocation OK, append data item */
if (
list ->
length)
/* if the list is not empty */
list->
last->next = new_node; /* link in new node */
else
/* otherwise */
list->
first = new_node;
/* set first pointer to new */
list ->
last = new_node;
/* update last pointer */
list ->
length++;
/* update list length */
return(1); /* success */
} else /* allocation problem */
return(0); /* error*/ }
BEFORE:
Deleting Nodes Study yourself.
Doubly Linked Lists
Finding the predecessor of a node in a one-way list requires a linear search. We can add a pointer to its
predecessor, to each node. It will consume more space but search will be faster. Sometimes you may
need to move through a list from the end to the beginning by following pointers. Inorder to be able to
do this we can define two pointers in each node;
One points to the next and one points to the previous node.
Length Last First
Previous
Item Next
(HEADER )
(NODE)
(a doubly linked list)
For doubly linked lists, you need to develop a new set of routines that are analogous to those for
ordinary (singly linked) lists. The function to create a new doubly linked list is nearly the same as that
for singly linked lists, in that you have to change only the data types of the header pointers.
Let us first define the doubly linked list node:
Header Node: It does not need to change. We only need to change the type of pointers from node_type
to doubly_type.
struct doubly{
info_type *item
struct doubly *next, *prev;
}
typedef struct doubly doubly_type;
template <infoType>
class doubly{
infoType *item
doubly *next, *prev;
... }
Operations on Doubly Linked Lists (Two-Way Lists)
Create operation
We also need to write a function which creates a new node.
/* create a storage for the new doubly linked node */
Using this function we can write insert and append functions:
/* Insert node at the beginning of a doubly linked list */
head_db_type *create_db()
{
head_db_type *new_node;
/* attempt to allocate memory */
if (new_node =MALLOC(head_db_type)) {
/* allocation OK, initialize the values */
new_node -> length
= 0;
new_node -> first = NULL;
new_node ->last
= NULL; }
/* return the address of the new lıst */
return(new_node);
}
DoublyLinkedList(){
first = NULL;
last
= NULL;
length = 0;
}
doubly_type
*alloc_db_node(val, prev, next)
info_type *val;
doubly_type *prev, *next;{
doubly_type *new_node;
if(new_node=MALLOC(doubly_type))
{
new_node -> item = val;
new_node -> prev = prev;
new_node -> next = next; }
return(new_node);
}
doubly(
info_type *val,
doubly *prv,
doubly *nxt)
{
item = val;
prev = prv;
next = nxt;
}
Previous field of the first node is 0
Next field of the last node is 0
(before insertion)
(after insertion)
Append a node to the end of the list Home study-- do it yourself
int insert_dbl ( info_type *data,
head_db_type *list ){
doubly_type *new_node;
if (new_node = alloc_db_node(data, NULL, list->first)) {
if (list->length == 0) /* if this is the first node */
list->last = new_node; /* set the last pointer to the new */
else {
/* if not the first node link it in */
list->first->prev = new_node; /* set prev field of first node */
list->first = new_node; /*set the first pointer to the new */ }
list->length++;
return(1); /* success */
}else return(0); } /* error */
Deleting first node from a doubly linked list
We assume that before calling the delete function you have checked the length of the list.
The function retıurns the data field of the deleted node and frees the memory used by this
Node.
(before deletion )
(after deletion)
LINKED LIST ADT IMPLEMENTATION IN C/ List ADT Type Defintions
typedef struct node {
void* dataPtr; struct node* link; } NODE;
info_type *del_dbl(head_db_type *list){
doubly_type *temp; info_type *data; /* delete first node */
temp= list->first;
/* save the pointer */
data = temp->item; /* save the data */
list->first = temp->next; /* reassign the first pointer */
list->length--;
/* update list length */
if (list->length )
/* if new list is not empty */
list->first->prev = NULL; /* set prev field of first item to NULL*/
else
/* otherwise */
list->last = NULL; /* set last pointer to NULL */;
free((char )temp); /* free node storage */
typedef struct { int count; NODE* pos; NODE* head; NODE* rear;
int (*compare) (void* argu1, void* argu2); } LIST;
// Prototype Declarations
LIST* createList (int (*compare)
(void* argu1, void* argu2)); LIST* destroyList (LIST* list);
int addNode (LIST* pList, void* dataInPtr);
bool removeNode (LIST* pList, void* keyPtr, void** dataOutPtr);
bool searchList (LIST* pList, void* pArgu, void** pDataOut);
bool retrieveNode (LIST* pList, void* pArgu,
void** dataOutPtr);
bool traverse (LIST* pList, int fromWhere, void** dataOutPtr);
int listCount (LIST* pList); bool emptyList (LIST* pList); bool fullList (LIST* pList);
static int _insert (LIST* pList, NODE* pPre,
void* dataInPtr);
static void _delete (LIST* pList, NODE* pPre, NODE* pLoc,
void** dataOutPtr); static bool _search (LIST* pList,
NODE** pPre, NODE** pLoc, void* pArgu);
// End of List ADT Definitions
/* =============== createList ==============
Allocates dynamic memory for a list head node and returns its address to caller
Pre compare is address of compare function used to compare two nodes.
Return head node pointer or null if overflow */
LIST* createList
(int (*compare) (void* argu1, void* argu2)) {
// Local Definitions
LIST* list;
// Statements
list = (LIST*) malloc (sizeof (LIST)); if (list) { list->head = NULL; list->pos = NULL; list->rear = NULL; list->count = 0; list->compare = compare; } // if return list; } // createList /* ================== addNode =================
Inserts data into list.
Pre pList is pointer to valid list dataInPtr pointer to insertion data Post data inserted or error
Return -1 if overflow 0 if successful 1 if dupe key */
int addNode (LIST* pList, void* dataInPtr) { // Local Definitions bool found; bool success; NODE* pPre; NODE* pLoc; // Statements
found = _search (pList, &pPre, &pLoc, dataInPtr); if (found)
// Duplicate keys not allowed return (+1);
success = _insert (pList, pPre, dataInPtr); if (!success) // Overflow return (-1); return (0); } // addNode /* =================== _insert ==================
Inserts data pointer into a new node. Pre pList pointer to a valid list
pPre pointer to data's predecessor dataInPtr data pointer to be inserted Post data have been inserted in sequence Return boolean, true if successful,
false if memory overflow */
static bool _insert (LIST* pList, NODE* pPre, void* dataInPtr)
{
// Local Definitions
NODE* pNew;
// Statements
if (!(pNew = (NODE*) malloc(sizeof(NODE)))) return false; pNew->dataPtr = dataInPtr; pNew->link = NULL; if (pPre == NULL) {
// Adding before first node or to empty list. pNew->link = pList->head;
pList->head = pNew; if (pList->count == 0)
// Adding to empty list. Set rear pList->rear = pNew;
} // if pPre else
{
// Adding in middle or at end pNew->link = pPre->link; pPre->link = pNew;
// Now check for add at end of list if (pNew->link == NULL) pList->rear = pNew; } // if else (pList->count)++; return true; } // _insert /* ================= removeNode ================
Removes data from list.
Pre pList pointer to a valid list
keyPtr pointer to key to be deleted dataOutPtr pointer to data pointer Post Node deleted or error returned. Return false not found; true deleted */
bool removeNode (LIST* pList, void* keyPtr, void** dataOutPtr)
{
// Local Definitions
NODE* pPre; NODE* pLoc;
// Statements
found = _search (pList, &pPre, &pLoc, keyPtr); if (found)
_delete (pList, pPre, pLoc, dataOutPtr);
return found;
} // removeNode
/* ================= _delete ================
Deletes data from a list and returns pointer to data to calling module. Pre pList pointer to valid list. pPre pointer to predecessor node pLoc pointer to target node
dataOutPtr pointer to data pointer Post Data have been deleted and returned Data memory has been freed
*/
void _delete (LIST* pList, NODE* pPre,
NODE* pLoc, void** dataOutPtr) {
// Statements
*dataOutPtr = pLoc->dataPtr; if (pPre == NULL)
// Deleting first node pList->head = pLoc->link; else
// Deleting any other node pPre->link = pLoc->link;
// Test for deleting last node if (pLoc->link == NULL) pList->rear = pPre; (pList->count)--; free (pLoc); return; } // _delete /* ================== searchList =================
Interface to search function.
Pre pList pointer to initialized list. pArgu pointer to key being sought
Post pDataOut contains pointer to found data -or- NULL if not found
Return boolean true successful; false not found */
bool searchList (LIST* pList, void* pArgu, void** pDataOut)
{
// Local Definitions
NODE* pPre; NODE* pLoc;
// Statements
found = _search (pList, &pPre, &pLoc, pArgu); if (found) *pDataOut = pLoc->dataPtr; else *pDataOut = NULL; return found; } // searchList /* ================== _search =================
Searches list and passes back address of node containing target and its logical predecessor. Pre pList pointer to initialized list pPre pointer variable to predecessor pLoc pointer variable to receive node pArgu pointer to key being sought
Post pLoc points to first equal/greater key -or- null if target > key of last node
pPre points to largest node < key -or- null if target < key of first node Return boolean true found; false not found
*/
bool _search (LIST* pList, NODE** pPre, NODE** pLoc, void* pArgu) {
// Macro Definition
#define COMPARE \
( ((* pList->compare) (pArgu, (*pLoc)->dataPtr)) )
#define COMPARE_LAST \
((* pList->compare) (pArgu, pList->rear->dataPtr))
// Local Definitions int result; // Statements *pPre = NULL; *pLoc = pList->head; if (pList->count == 0) return false;
// Test for argument > last node in list if ( COMPARE_LAST > 0) { *pPre = pList->rear; *pLoc = NULL; return false; } // if
while ( (result = COMPARE) > 0 ) {
// Have not found search argument location *pPre = *pLoc;
} // while if (result == 0) // argument found--success return true; else return false; } // _search /* ================== retrieveNode ================
This algorithm retrieves data in the list without changing the list contents.
Pre pList pointer to initialized list. pArgu pointer to key to be retrieved Post Data (pointer) passed back to caller Return boolean true success; false underflow */
static bool retrieveNode (LIST* pList, void* pArgu, void** dataOutPtr) { // Local Definitions bool found; NODE* pPre; NODE* pLoc; // Statements
found = _search (pList, &pPre, &pLoc, pArgu); if (found) { *dataOutPtr = pLoc->dataPtr; return true; } // if *dataOutPtr = NULL; return false; } // retrieveNode /* ================= emptyList ================
Returns boolean indicating whether or not the list is empty
Pre pList is a pointer to a valid list Return boolean true empty; false list has data */
bool emptyList (LIST* pList) {
// Statements
return (pList->count == 0);
} // emptyList
/* ================== fullList =================
Returns boolean indicating no room for more data. This list is full if memory cannot be allocated for another node.
Pre pList pointer to valid list Return boolean true if full
*/
bool fullList (LIST* pList) { // Local Definitions NODE* temp; // Statements if ((temp = (NODE*)malloc(sizeof(*(pList->head))))) { free (temp); return false; } // if
// Dynamic memory full return true;
} // fullList
/* ================== listCount ==================
Returns number of nodes in list.
Pre pList is a pointer to a valid list Return count for number of nodes in list */
int listCount(LIST* pList) {
// Statements
return pList->count;
} // listCount
/* ================== traverse =================
Traverses a list. Each call either starts at the beginning of list or returns the location of the next element in the list.
Pre pList pointer to a valid list fromWhere 0 to start at first element dataPtrOut address of pointer to data Post if more data, address of next node Return true node located; false if end of list */
bool traverse (LIST* pList, int fromWhere, void** dataPtrOut) { // Statements if (pList->count == 0) return false; if (fromWhere == 0) {
//Start from first node pList->pos = pList->head; *dataPtrOut = pList->pos->dataPtr; return true; } // if fromwhere else {
if (pList->pos->link == NULL) return false; else { pList->pos = pList->pos->link; *dataPtrOut = pList->pos->dataPtr; return true; } // if else } // if fromwhere else } // traverse /* ================== destroyList =================
Deletes all data in list and recycles memory Pre List is a pointer to a valid list. Post All data and head structure deleted Return null head pointer
*/
LIST* destroyList (LIST* pList) { // Local Definitions NODE* deletePtr; // Statements if (pList) { while (pList->count > 0) {
// First delete data
free (pList->head->dataPtr);
// Now delete node
deletePtr = pList->head; pList->head = pList->head->link; pList->count--; free (deletePtr); } // while free (pList); } // if return NULL; } // destroyList