Evanalysis
1.1Embedded interactionEstimated reading time: 16 min

1.1 ADT operations: stack, queue, and function pointers

Study ADT contracts first, then follow stack and queue operations through concrete C implementations and function-pointer-based dispatch.

Course contents

CSCI2520: Data structures

Structured notes for CSCI2520 data-structure foundations with operation-level reasoning and selective interactive demonstrations.

Chapter 0Programming foundations1 sections
Chapter 2Lists and recursion1 sections
Chapter 4Trees and BSTs1 sections

Why this section starts with contracts

In CSCI2520, the first mistake is usually not a missing semicolon. It is a missing contract.

An abstract data type tells us four things before we commit to a particular representation:

  • what operations are legal,
  • what each operation guarantees,
  • what the client may assume after a call,
  • how empty-state and error cases are handled.

That separation matters because the same stack or queue interface can be backed by a fixed array, a dynamic array, or a linked list. The client code should not need to change when the representation changes.

Definition

ADT contract versus implementation

An ADT contract names the operations and their meaning. An implementation chooses the data fields, memory layout, and update strategy. The contract is the promise; the representation is the mechanism.

A stack is a disciplined top-only structure

The lecture slides define a stack as a pile that can be accessed only from the top. That is not just an analogy. It is the invariant that makes stack code predictable.

Definition

Stack core operations

For a stack SS:

  • push(x): place x on the top of SS,
  • pop(): remove and return the top element,
  • top(): inspect the top element without removing it,
  • isEmpty(): test whether the stack has no elements,
  • StackDepth(): return the number of stored elements.

The key law is LIFO, last in first out. If a is pushed before b, then b must come out first unless an error interrupts the process.

Worked example

Tracing a stack by state

Start with an empty stack.

  1. push(10) gives [10]
  2. push(20) gives [10, 20]
  3. top() returns 20 and leaves the state unchanged
  4. pop() returns 20 and the stack becomes [10]

The stack interface hides the representation

The lecture makes a point of the opaque type pattern:

typedef struct stackCDT *stackADT;
typedef int stackElementT;

stackADT EmptyStack(void);
void Push(stackADT stack, stackElementT element);
stackElementT Pop(stackADT stack);
int StackDepth(stackADT stack);
int StackIsEmpty(stackADT stack);

This is not cosmetic. stackADT is a pointer to an incomplete structure, so the client can hold and pass stacks around but cannot reach inside the structure. That means the implementation can change from:

  • a fixed array of 100 entries,
  • to a dynamic array with realloc,
  • to another internal layout later,

without rewriting client code.

Theorem

Representation independence

If the client can use an ADT only through its declared operations, then the implementation may change its internal representation without changing the client-visible behavior.

How the stack is implemented in practice

The source slides show three important implementation ideas.

First, a fixed array implementation stores values bottom to top and keeps a count field for the current depth:

struct stackCDT {
   stackElementT elements[100];
   int count;
};

This version is simple, but it has a hard capacity limit. If the depth grows beyond 100, the implementation hits stack overflow at the representation level, not the mathematical stack operation.

Second, a dynamic-array version stores an expandable pointer and a size field:

struct stackCDT {
   stackElementT *elements;
   int count;
   int size;
};

When the array fills, Push() expands the storage with realloc(). That gives the client a larger logical stack without changing the interface.

Third, the slides also refine Pop() to recover memory more carefully when the underlying array is no longer fully used. That is a reminder that a correct interface is not enough; memory policy still matters.

Worked example

A push/pop implementation sketch

void Push(stackADT stack, stackElementT element) {
   if (stack->count == stack->size) {
      stack->size += 10;
      stack->elements = realloc(
         stack->elements,
         stack->size * sizeof(stackElementT)
      );
   }
   stack->elements[stack->count] = element;
   stack->count++;
}

stackElementT Pop(stackADT stack) {
   stack->count--;
   return stack->elements[stack->count];
}

The code is intentionally direct:

  • count tracks the next free slot,
  • push stores at that slot and increments the count,
  • pop decrements the count and returns the old top value.

Queue semantics are different, even if the code looks similar

The queue ADT is the other side of the same design problem. The operations are similar in style, but the invariant is FIFO, first in first out.

Definition

Queue core operations

For a queue QQ:

  • enqueue(x): add x at the tail,
  • dequeue(): remove and return the head element,
  • front(): inspect the head element without removing it,
  • isEmpty(): test whether the queue has no elements,
  • QueueLength(): return the number of stored elements.

The lecture notes present the queue header in the same opaque-style pattern:

typedef struct queueCDT *queueADT;
typedef int queueElementT;

queueADT EmptyQueue(void);
void Enqueue(queueADT queue, queueElementT element);
queueElementT Dequeue(queueADT queue);
int QueueLength(queueADT queue);
int QueueIsEmpty(queueADT queue);

Again, the interface says what the user may do; it does not say how the queue is stored.

Queue implementations: array, circular array, linked list

The tutorial slides make the implementation tradeoffs explicit.

  • A fixed-size array is the simplest first version, but it wastes space when elements are dequeued from the front and new values are appended at the tail.
  • A circular array fixes that waste by wrapping front and rear around with the modulus operator.
  • A linked-list queue stores head and tail pointers and grows as long as the heap can provide memory.

The linked-list version is especially useful when we want the queue to keep accepting work items without shifting existing elements around.

struct cellT {
   queueElementT element;
   struct cellT *next;
};

struct queueCDT {
   struct cellT *head;
   struct cellT *tail;
};

That representation makes enqueue and dequeue conceptually simple:

  • a new node is linked at the tail,
  • the head node is removed and freed,
  • QueueIsEmpty() is just head==NULLhead == NULL.

Worked example

Why a circular array is better than a plain array

Suppose a queue alternates enqueue and dequeue many times.

  • In a plain array, values near the front can leave empty space that is not reused efficiently.
  • In a circular array, the front and rear indices wrap around so the earlier slots can be reused.

That is why the tutorial explicitly asks what happens if we enqueue and dequeue repeatedly for millions of operations.

Function pointers let C simulate method dispatch and callbacks

The lecture on function pointers is not random extra material. It explains how C can defer behavior until runtime while still staying type-safe.

At a minimum, a function pointer stores the address of code:

int (*fp)(int);

The pointer may point to any function with the matching prototype. That means the type signature is the contract.

Definition

Function-pointer rule

A function pointer may point only to functions whose parameter list and return type match the declared function-pointer type.

The slides then move to a practical ADT example: extending a hash table with a callback-based traversal operation.

typedef void (*hashtableFnT)(char *, void *);

void ForEachEntryDo(hashtableFnT fp, hashtableADT table);

Client code can pass a callback:

void DisplayWordCount(hashtableADT table) {
   ForEachEntryDo(PrintEntry, table);
}

This pattern matters for ADTs because it lets the implementation perform the iteration while the client controls what happens to each entry. The traversal logic stays in one place, but the action can vary.

The same idea also appears in dispatch tables: a set of function pointers can select the correct operation for a specific structure or command without a long chain of if statements.

void ApplyOperator(char c, stackADT stack) {
   int y = Pop(stack);
   int x = Pop(stack);

   switch (c) {
   case '+': Push(stack, x + y); break;
   case '-': Push(stack, x - y); break;
   case '*': Push(stack, x * y); break;
   case '/': Push(stack, x / y); break;
   }
}

In this example, the stack stores operands while the operation table decides how to combine them. The code is small, but the concept is broad: function pointers turn C into a manually controlled dispatch system.

Read and try

Trace ADT operation semantics

The widget now pairs a C-style code sample with an editable command list, so readers can test stack and queue semantics against the stated ADT contract.

Code sample

typedef struct {
  int data[100];
  int top;
} Stack;

void push(Stack *s, int x) {
  s->data[++(s->top)] = x;
}

int pop(Stack *s) {
  return s->data[(s->top)--];
}

Try it yourself

Row operation: push(10)

stack: [10]

queue: []

Top now points to 10.

Try it yourself

push 10

Current state: [10]

Returned value: No returned value

push 20

Current state: [10, 20]

Returned value: No returned value

top

Current state: [10, 20]

Returned value: 20

pop

Current state: [10]

Returned value: 20

push 7

Current state: [10, 7]

Returned value: No returned value

Final state: [10, 7]

Testing an ADT contract with code

Once the interface is fixed, the next question is not “does it compile?” but “does every observable state transition still satisfy the contract?”

One practical method is to write tiny trace-based tests:

stackADT s = EmptyStack();
Push(s, 10);
Push(s, 20);
assert(StackDepth(s) == 2);
assert(Pop(s) == 20);
assert(Pop(s) == 10);
assert(StackIsEmpty(s));

This kind of test is valuable because it checks the contract at the ADT level, not the memory layout level. If you later replace the array-backed stack with a linked representation, the same test should still pass unchanged.

The same idea applies to queues: a short enqueue, enqueue, dequeue, dequeue trace should confirm that the first inserted element is still the first one removed, even after the representation changes.

Common mistakes

Common mistake

Mixing the stack and queue invariants

push/pop belong to a stack; enqueue/dequeue belong to a queue. Renaming the functions without preserving the access order breaks the ADT contract even if the program still compiles.

Common mistake

Forgetting error policy on empty structures

The slides explicitly treat empty-stack underflow and queue-empty cases as errors that must be handled. Decide whether the function returns a status code, a sentinel, or an error indicator before you code the interface.

Common mistake

Confusing interface stability with implementation stability

The interface should stay stable. The internal representation is what should be free to change when the data structure evolves.

Quick checks

Quick check

After push(1), push(2), pop(), what remains in the stack?

Apply LIFO strictly.

Solution

Answer

Quick check

After enqueue(4), enqueue(7), dequeue(), which element is returned?

Apply FIFO strictly.

Solution

Answer

Quick check

Why does ForEachEntryDo(PrintEntry, table) need a callback type, not just a generic pointer?

Focus on type checking and matching parameter lists.

Solution

Guided solution

Exercises

Quick check

Write a precondition and a postcondition for Pop(stack) on a non-empty stack.

Keep the contract precise.

Solution

Guided solution

Quick check

Explain why a queue implemented with a linked list can support enqueue without shifting existing elements.

Tie your answer to head and tail.

Solution

Guided solution

Quick check

Write one short paragraph explaining how function pointers help testing an ADT implementation.

Use the interface-versus-implementation distinction.

Solution

Guided solution

Takeaway

The main idea is not “stack and queue are two data structures.” The real idea is that ADT contracts define behavior, implementations choose storage, and function pointers let C separate those responsibilities without losing control over the types.

Prerequisites

This section can be read on its own.

Key terms in this unit