COMP2521

Data Structures & Algorithms

Week 2.2

Abstract Data Types (ADTs)

Author: Sim Mautner 2023
Adapted from slides by: Hayden Smith 2021
https://slides.com/haydensmith/comp2521-21t2-2-2

In this lecture

Why?

  • ADTs are a fundamental concept of writing robust software, and of being able to work with other people

What?

  • ADT definition
  • ADT usage
  • ADT implementation

 

ADTs

  • Data type:
    • Set of values (atomic or structured)
    • Collection of operations on those values

Can you think of some examples?

What is a data type?

  • int
    • set of value(s): an integer
    • operations: addition, subtraction, multiplication, etc
  • array:
    • set of values(s): a repeat of any data type (e.g. int)
    • operations: index lookup, index assignment, etc

Abstraction

  • Abstraction: Hiding details of a how a system is built in favour of focusing on the high level behaviours, or inputs and outputs, of the system
  • Examples?
    • C abstracts away assembly/MIPS code.
    • Python abstract away pointer arithmetic and memory allocation.
    • Web browsers abstract away the underlying hardware that they're run on.

Abstract Data Type

ADT is a description of a data type that focuses on it's high level behaviour, without regard for how it is implemented underneath. This means:

  • There is a separation of interface from implementations
  • Users of the ADT see only the interface
  • Builds of the ADT provide an implementation
  • Both parties need to agree on the ADTs interface
  • Interface allows people to agree at the start, and work separately.

Programming by Contract

  • When we define our interface, we also need to include information about:
    • Pre-conditions: What conditions hold at the start of the function call
    • Post-conditions: What conditions will hold at the end of the function
  • Add them via comments
  • Can sanity check with asserts

Abstract Data Type

  • Step 1: Determine the interface of your ADT in a .h file
  • Step 2: The "developer" builds a concrete implementation for the adt in a .c file
  • Step 3: The "user" uses the abstract data type in their program
    • They have to compile with it, even though they might not understand how it is built underneath

List data type: ordered collection of integer values.

 

What will we figure out first?

  • What behaviour does this ADT need? (interface)
  • How are we going to code for it? (implementation)

List ADTs

  • create an empty collection
  • add one item to the collection
    • to the start of the list
    • to the end of the list
  • remove one item from the collection
    • from the start of the list
    • from the end of the list

List ADTs

Let's brainstorm!

  • find an item in the collection
  • check the size of the collection
  • drop the entire collection
  • display the collection

Now we start to write this as C code!

Notice that we aren't implementing anything yet?

List ADTs

List ListCreate() // create a new list
void ListAddStart(List, int) // add number to the start
void ListAddEnd(List, int) // add number to the end
void ListDeleteStart(List) // remove number from start
void ListDeleteEnd(List) // remove number from end
int ListMember(List, int) // membership test
int ListSize(List) // size of list
void ListDestroy(List) // destroy a created list

List ADTs

#ifndef LIST_H
#define LIST_H

#include <stdio.h>

#define TRUE 1
#define FALSE 0

typedef struct ListRep *List;

// ADT functions go here

#endif

Three key principles of ADTs in C:

  • The "List" (or equivalent) is usually a pointer of some sort
  • That pointer is usually the first argument in every ADT function, with the exception of the create function
  • When we write .h files, we use header guards to prevent re-definition

 

Notice how we haven't defined "struct List"? That's not our job.

List.h

List ADTs

// List.h ... interface to List ADT

#ifndef LIST_H
#define LIST_H

#include <stdio.h>

#define TRUE 1
#define FALSE 0

typedef struct ListRep *List;

List ListCreate(); // create a new list
void ListAddStart(List, int); // add number to the start
void ListAddEnd(List, int); // add number to the end
void ListDeleteStart(List); // remove number from start
void ListDeleteEnd(List); // remove number from end
int ListMember(List, int); // membership test
int ListSize(List); // size of list
void ListDestroy(List); // destroy a created list

List ListCopy(List);          // make a copy of a set
void ListShow(List);         // display set on stdout

#endif

Completed List.h

 

But what's missing?

Programming by contract

  • Pre and post conditions now added.
  • Helps both developers and users manage expectations

List ADTs

// create new empty list
// pre:  
// post: Valid list returned, list is empty
List ListCreate();

// add value to the start of the list
// pre: Valid list provided
// post: New element "newval" is now at the start of list l,
//       the rest of list l remains unchanged
void ListAddStart(List l, int newval);

// list size
// pre: Valid list provided for l
// post: Response is the number of elements in the list
int ListSize(List l);

List Usage

  • How do we actually work with a set though?
    • We write our "main" file, and compile it with the set library that the ADT developer has implemented.
    • While we need their .c file to build with, we never need to look at it or make sense of it, because we have the ADT (i.e. .h file)
    • In fact, we could even just work with the .o file!

my_list.c

List.c

my_list.o

List.o

./my_list

List.h

List Usage

#include "List.h"

#include <stdio.h>

int main() {
    List l = ListCreate();
    // Could use Scanf instaed
    for (int i = 1; i < 26; i += 2) {
        ListAddEnd(l,i);
    }
    ListShow(l);
    printf("\n");
}

testList1.c

It's time to implement the list! The "user" of our list doesn't need to worry about this.

 

We will implement 3 different types of list:

  1. That uses an array
  2. That uses a simple linked list (like in COMP1511)
  3. That uses a doubly linked list with:
    • A node holding pointers to the first node (head) and last node (tail) of the list, and a count of the number of nodes in the list
    • Each node linked to both the next element as well as the previous element

 

List Implementation

List Implementation (array)

#include "List.h"

#define MAX_ELEMS 10000

// concrete data structure
struct ListRep {
    int elems[MAX_ELEMS];
    int nelems;
};

List ListCreate() {...}
void ListAddStart(List, int) {...}
void ListAddEnd(List, int) {...}
void ListDeleteStart(List) {...}
void ListDeleteEnd(List) {...}
int ListMember(List, int) {...}
int ListSize(List) {...}
void ListDestroy(List) {...}
List ListCopy(List) {...}
void ShowList(List) {...}

List-array.c

We can represent this set using an array. This means we do have to do upper and lower bounds checks because there will be a theoretical limit on the size of the set.

 

A sample of the implemented list

List Implementation (array)

// create new empty list
List ListCreate()
{
    List l = malloc(sizeof(struct ListRep));
    if (l == NULL) {
         fprintf(stderr, "Insufficient memory\n");
         exit(1);
    }
    l->nelems = 0;
    // assert(isValid(s));
    return l;
}

// list membership test
int ListMember(List l, int n)
{   
    // assert(isValid(s));
    int i; 
    for (i = 0; i < l->nelems; i++) {
        if (l->elems[i] == n) {
            return TRUE;
        }
    }
    return FALSE;
}

List Implementation (array)

Data Structure addStart/
deleteStart
(time)
insertEnd/
deleteEnd
(time)
member
(time)
storage
(space)
array O(n) O(1) O(n) O(E)

Let's look at the time and space complexities:

  • n: Number of elements in the set
  • E: Maximum number of items able to be in set
  • Linked List
  • No extra information
  • The head of the list is passed around
  • Each node links only to the next node

List Implementation (Simple Linked List)

List Implementation (Simple Linked List)

Data Structure addStart/
deleteStart
(time)
insertEnd/
deleteEnd
(time)
member
(time)
storage
(space)
array O(n) O(1) O(n) O(E)
simple linked list O(1) O(n) O(n) O(n)

List Implementation (Doubly Linked List)

// concrete data structure
typedef struct Node {
   int  value;
   struct Node *next;
   struct Node *prev;
} Node;

struct ListRep {
    Node * head; // pointer to the first node
    Node * tail; // pointer to the last node
    int nelems; // number of elements
};

list-doubly-linked.c

List Implementation (Doubly linked list)

// list membership test
int ListMember(List l, int n)
{   
    // assert(isValid(s));
    Node * curr = l->head; 
    while (curr != NULL) {
        if (curr->value == n) {
            return TRUE;
        }
        curr = curr->next;
    }
    return FALSE;
}
// create new empty list
List ListCreate()
{
    List l = malloc(sizeof(struct ListRep));
    if (l == NULL) {
         fprintf(stderr, "Insufficient memory\n");
         exit(EXIT_FAILURE);
    }
    l->head = NULL;
    l->tail = NULL;
    l->nelems = 0;
    // assert(isValid(l));
    return l;
}

Set Implementation (Linked list)

Data Structure addStart/
deleteStart
(time)
insertEnd/
deleteEnd
(time)
member
(time)
storage
(space)
array O(n) O(1) O(n) O(E)
simple linked list O(1) O(n) O(n) O(n)
doubly linked list O(1) O(1) O(n) O(n)

Direct access - issues?

What happens if we try to access elements of the implementation directly?

 

We might receive a "dereferencing pointer to incomplete type" error

Benefits of Restricting Access

  • Allows design by contract
  • Keeps the data structure in a valid state (assuming the ADT is well tested, it cannot be manipulated from outside the .c file, in unauthorised ways)

 

... and often importantly...

  • Restricts unauthorised access and manipulation

Example 1:

  • Deck of cards: When created, it is shuffled. After that cards can only be removed from the top (and possibly inserted at the bottom).

Benefits of Restricting Access

Example 2:

  • In a role-playing game: A player can only buy from the shop if they have enough money to afford the item.

Benefits of Restricting Access

What other examples can you think of?

Benefits of Restricting Access

Stack:

  • A list of items which only allows items to be added and removed in a first-in-last-out (FILO) fashion.
  • Functions:
    • create
    • push (add to the top of the stack)
    • pop (remove from the top of the stack)
    • destroy

Some Simple Data Structures

Stack Examples:

  • Plates in a cafeteria
  • Wikipedia tabs: The oldest topic is of least interest now, so we only want to return to it once we have exhausted all our more recent discoveries.

Some Simple Data Structures

Queue:

  • A list of items which only allows items to be added and removed in a first-in-first-out (FIFO) fashion.
  • Functions: 
    • create
    • enqueue (add an item to the back of the queue)
    • dequeue (remove an item from the front of the queue)
    • destroy

Some Simple Data Structures

Queue Examples:

  • Waiting in line
  • Turn taking in a game
  • Don't want people to be able to "jump the queue", so there's no "pop" function. 

Some Simple Data Structures

ADT Summary

  • ADT interface:
    • A user-view of the data structure
    • Functions for all operations
    • Explanations of those operations
    • Any guarantees it provides ("Contract")
  • ADT implementation:
    • Concrete definition of the data structures
      • List, tree, graph, array, etc etc
    • Definition of functions that operate on the data structure
  • Why abstract the data structure?
    • Allows future iterations to remove or upgrade a data structure
    • Allows things like lists to actually have more intelligent implementations underneath
    • Restricts behaviour to a finite, specified, set of actions

ADT Summary

COMP2521 23T2 - 2.2 - Abstract Data Types (ADTs)

By Sim Mautner

COMP2521 23T2 - 2.2 - Abstract Data Types (ADTs)

  • 993