Chapter 5 : Linked Implementations of Collection ADTs

🎯 Learning Objectives

By the end of this study session, you’ll be able to:

  • Explain why we need linked structures instead of just using arrays for everything
  • Describe what linked structures are and how they solve array limitations
  • Understand the basic building blocks: nodes, references, and the three fundamental rules of linked lists
  • Implement the List ADT using linked structures with all major operations (add, remove, traverse)
  • Implement the Stack ADT using linked structures for push/pop operations
  • Implement the Queue ADT using multiple approaches (linear, circular, doubly-linked)
  • Compare the efficiency trade-offs between array-based and linked implementations
  • Recognize when to use linked structures vs. arrays in real programming scenarios
  • Apply concepts like head/tail references and traversal techniques in your own code

🌟 The Big Picture

Here’s the fundamental question this chapter answers: What happens when arrays just aren’t flexible enough?

Think about it - arrays are great when you know exactly how much data you’ll have, but what if your data grows and shrinks unpredictably? What if inserting or removing items in the middle requires shifting hundreds of elements? This is where linked structures come to the rescue.

The core concept is brilliant in its simplicity: instead of storing all your data in one big block (like an array), you create individual containers (called nodes) that can live anywhere in memory, and you connect them with β€œpointers” or β€œreferences” that tell you where to find the next piece of data. It’s like having a treasure hunt where each clue leads you to the next location!

This approach gives us dynamic flexibility - our data structure can grow as large as needed and shrink when items are removed, all without the overhead of shifting elements around.

πŸ“š Core Concepts

Why Arrays Sometimes Fall Short

Let’s start with understanding the problems we’re trying to solve:

Problem 1: Fixed Size Inflexibility

  • If your array is too small, you have to create a bigger one and copy everything over (expensive!)
  • If your array is too big, you’re wasting memory for unused space

Problem 2: Shifting Overhead

  • Want to insert something in the middle? You have to shift all elements to the right
  • Want to remove something? You have to shift all elements to the left
  • This can be very time-consuming for large arrays

The Solution Vision Instead of these problems, wouldn’t it be great if we could:

  • Allocate space for each item only when we actually need it
  • Remove space when we delete items
  • Add and remove items by just adjusting connections, not shifting everything

This is exactly what linked structures give us!

The Desk Analogy: Making Linked Structures Concrete

The textbook uses a fantastic analogy that makes this concept crystal clear. Imagine a classroom where students can request desks as needed:

Visual Reference (Slides 6-13):

The figures show a teacher with a reference to desk addresses, creating chains of desks where each desk has the address of the next desk written on it.

How It Works:

  • Each desk (like a data container) has a unique address
  • Each desk can hold a piece of paper with the address of another desk
  • The teacher only needs to remember the address of the first desk
  • To find any desk, you start at the first one and follow the chain of addresses

Adding New Desks:

  • You can add to the beginning (fastest - just update the teacher’s reference)
  • You can add to the end (need to walk to the end first)
  • You can add in the middle (need to find the right spot and adjust connections)

This desk analogy perfectly illustrates how linked structures work in computer memory!

Basic Linked List Concepts

Now let’s translate that desk analogy into programming terms:

What is a Node? A node is like one of those desks - it’s a container that holds two things:

  1. Data part: The actual information you want to store
  2. Link part: The address (reference) to the next node in the chain

Visual Reference (Slide 20):

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Data  β”‚  Next   β”‚  ← This is one node
β”‚   Part  β”‚  Link   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

What is a Linked List? A linked list is a collection of nodes where:

  • Each node points to the next node in the sequence
  • The last node points to null (indicating the end)
  • We keep a reference to the first node (called the β€œhead”)

Visual Reference (Slide 19):

head β†’ [Data|β€’] β†’ [Data|β€’] β†’ [Data|β€’] β†’ [Data|X]
                                              ↑
                                            null

The Three Sacred Rules of Linear Linked Lists

These are absolutely crucial - memorize them!

  1. External Reference Rule: There MUST be an external reference (pointer) to the first node so you can access the linked list
  2. Last Node Rule: The last node’s next field MUST be null
  3. Connection Rule: Every other node’s next field MUST contain the address of the next node

Break any of these rules, and your linked list won’t work correctly!

Node Class Implementation

Here’s how we implement a node in Java (this becomes an inner class):

private class Node {
    private T data;        // The actual data
    private Node next;     // Reference to next node
 
    // Constructor for node with just data
    private Node(T data) {
        this.data = data;
        next = null;
    }
 
    // Constructor for node with data and next reference
    private Node(T data, Node next) {
        this.data = data;
        this.next = next;
    }
}

Key Insight: We make Node a private inner class because it’s an implementation detail that clients shouldn’t see or manipulate directly.

Traversing a Linked List

Traversal means visiting each node in sequence from first to last. Here’s the essential pattern:

Node currentNode = firstNode;  // Start at the beginning
while (currentNode != null) {
    // Do something with currentNode.data
    currentNode = currentNode.next;  // Move to next node
}

Memory Trick: Think of traversal like following a trail of breadcrumbs - you start at the first crumb and follow each one to the next until you reach the end!

Linked Implementation of List ADT

Now let’s see how to implement all the List operations using linked structures:

Class Structure

public class LinkedList<T> implements ListInterface<T> {
    private Node firstNode;      // Reference to first node
    private int numberOfEntries; // Count of items in list
 
    // Node class goes here as inner class
    // Methods go here
}

Adding Elements: Three Scenarios

Scenario 1: Adding to an Empty List

Visual Reference (Slide 31):

Before:  firstNode β†’ null
         newNode β†’ [Data|null]

After:   firstNode β†’ [Data|null]

This is the simplest case - just make firstNode point to the new node.

Scenario 2: Adding to the Beginning

Visual Reference (Slide 33):

Before:  firstNode β†’ [A|β€’] β†’ [B|β€’] β†’ [C|null]
         newNode β†’ [NEW|null]

After:   firstNode β†’ [NEW|β€’] β†’ [A|β€’] β†’ [B|β€’] β†’ [C|null]

Steps:

  1. Make the new node point to the current first node
  2. Make firstNode point to the new node

Scenario 3: Adding to the End

Visual Reference (Slide 32):

Before:  firstNode β†’ [A|β€’] β†’ [B|β€’] β†’ [C|null]
         newNode β†’ [NEW|null]

After:   firstNode β†’ [A|β€’] β†’ [B|β€’] β†’ [C|β€’] β†’ [NEW|null]

Steps:

  1. Traverse to find the last node (the one pointing to null)
  2. Make the last node point to the new node

Adding in the Middle

Visual Reference (Slide 34):

Before:  firstNode β†’ [A|β€’] β†’ [B|β€’] β†’ [C|null]
         newNode β†’ [NEW|null]

After:   firstNode β†’ [A|β€’] β†’ [NEW|β€’] β†’ [B|β€’] β†’ [C|null]

Steps:

  1. Traverse to find the node BEFORE where you want to insert
  2. Make the new node point to the node that comes after
  3. Make the previous node point to the new node

Removing Elements: Three Scenarios

Removing the First Node

Visual Reference (Slide 42):

Before:  firstNode β†’ [A|β€’] β†’ [B|β€’] β†’ [C|null]

After:   firstNode β†’ [B|β€’] β†’ [C|null]
         (A node becomes unreachable and gets garbage collected)

Removing a Middle Node

Visual Reference (Slide 43):

Before:  β€’ β†’ [A|β€’] β†’ [B|β€’] β†’ [C|β€’] β†’ β€’

After:   β€’ β†’ [A|β€’] β†’ [C|β€’] β†’ β€’
         (B node becomes unreachable)

Key insight: You need to find the node BEFORE the one you want to remove, then β€œbridge over” it.

Removing the Last Node Similar to middle removal, but you’re making the second-to-last node point to null instead of another node.

Performance Analysis: Linked vs. Array Lists

Visual Reference (Slide 47):

Here’s the efficiency comparison table:

OperationArray-BasedLinked
add(newEntry)O(1)O(n)
add(position, entry)O(n)O(n)
remove(position)O(n)O(n)
getEntry(position)O(1)O(n)

Key Insights:

  • Arrays are faster for direct access (getting an element at a specific position)
  • Linked lists are better when you frequently add/remove at the beginning
  • Both have similar performance for adding/removing in the middle

Improving Performance with Tail References

One optimization is keeping a reference to the last node:

private Node firstNode;
private Node lastNode;  // New: reference to last node
private int numberOfEntries;

Visual Reference (Slide 49):

firstNode β†’ [A|β€’] β†’ [B|β€’] β†’ [C|null] ← lastNode

Benefits:

  • Adding to the end becomes O(1) instead of O(n)
  • No need to traverse the entire list to find the last node

Trade-offs:

  • Need to maintain both references during add/remove operations
  • Slightly more complex code

Linked Implementation of Stack ADT

Stacks are perfect for linked implementation because we only ever add/remove from one end!

Design Decision: Which End is the Top?

Smart Choice: Make the first node represent the top of the stack.

Why? Because adding/removing from the beginning of a linked list is O(1) - exactly what we want for push/pop operations!

Visual Reference (Slide 60):

topNode β†’ [Top|β€’] β†’ [Middle|β€’] β†’ [Bottom|null]
          ↓
    Stack conceptually

Push Operation

Visual Reference (Slide 61):

Before:  topNode β†’ [A|β€’] β†’ [B|β€’] β†’ [C|null]
         newNode β†’ [NEW|null]

After:   topNode β†’ [NEW|β€’] β†’ [A|β€’] β†’ [B|β€’] β†’ [C|null]

Push Algorithm:

  1. Create a new node with the data
  2. Make the new node point to the current top
  3. Make topNode point to the new node

This is identical to β€œadding to the beginning” of a linked list!

Pop Operation

Visual Reference (Slides 63-64):

Before:  topNode β†’ [A|β€’] β†’ [B|β€’] β†’ [C|null]

After:   topNode β†’ [B|β€’] β†’ [C|null]
         Return A to the client

Pop Algorithm:

  1. Save reference to the data in the top node
  2. Make topNode point to the second node
  3. Return the saved data

Stack Performance

All stack operations using linked implementation are O(1):

  • Push: O(1)
  • Pop: O(1)
  • Peek: O(1)
  • isEmpty: O(1)

This is excellent performance and one of the best use cases for linked structures!

Linked Implementation of Queue ADT

Queues are trickier because we add at one end (back) and remove from the other (front).

A. Linear Linked Implementation

Design Challenge: We need efficient access to both ends of the queue.

Solution: Use both head and tail references!

Visual Reference (Slide 69):

firstNode β†’ [Front|β€’] β†’ [Middle|β€’] β†’ [Back|null] ← lastNode
            ↓                        ↓
        Remove here              Add here

Enqueue (Adding to Back)

Case 1: Empty Queue

Visual Reference (Slide 70):

Before:  firstNode β†’ null    lastNode β†’ null
         newNode β†’ [Data|null]

After:   firstNode β†’ [Data|null] ← lastNode

Case 2: Non-empty Queue

Visual Reference (Slide 71):

Before:  firstNode β†’ [A|β€’] β†’ [B|β€’] β†’ [C|null] ← lastNode
         newNode β†’ [NEW|null]

After:   firstNode β†’ [A|β€’] β†’ [B|β€’] β†’ [C|β€’] β†’ [NEW|null] ← lastNode

Enqueue Algorithm:

  1. Create new node
  2. If queue is empty: make both firstNode and lastNode point to new node
  3. Otherwise: make current last node point to new node, then update lastNode

Dequeue (Removing from Front)

Case 1: Queue with Multiple Elements

Visual Reference (Slide 73):

Before:  firstNode β†’ [A|β€’] β†’ [B|β€’] β†’ [C|null] ← lastNode

After:   firstNode β†’ [B|β€’] β†’ [C|null] ← lastNode
         Return A to client

Case 2: Queue with One Element

Visual Reference (Slide 74):

Before:  firstNode β†’ [Data|null] ← lastNode

After:   firstNode β†’ null    lastNode β†’ null
         Return Data to client

Dequeue Algorithm:

  1. Save reference to front node’s data
  2. Update firstNode to point to second node
  3. If queue becomes empty: set lastNode to null too
  4. Return saved data

B. Circular Linked Implementation

Cool Idea: What if the last node points back to the first node instead of null?

Visual Reference (Slide 80):

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        ↓                     β”‚
    [A|β€’] β†’ [B|β€’] β†’ [C|β€’] β”€β”€β”€β”€β”˜
     ↑               ↑
   first           lastNode

Benefits:

  • Only need ONE reference (to the last node)
  • Can still find the first node via lastNode.next
  • Slightly more memory efficient

Trade-offs:

  • More complex logic for edge cases
  • Need to be careful about infinite loops during traversal

C. Doubly Linked Implementation

Advanced Concept: Each node has TWO references - one to the next node and one to the previous node.

Visual Reference (Slide 83):

firstNode β†’ [β€’|A|β€’] ⇄ [β€’|B|β€’] ⇄ [β€’|C|β€’] ← lastNode

Benefits:

  • Can traverse in both directions
  • Easier to remove nodes (don’t need to find the previous node)
  • More flexible for complex operations

Trade-offs:

  • Uses more memory (extra reference per node)
  • More complex maintenance code

Efficiency Comparisons and Trade-offs

When to Use Linked Structures

Great for:

  • Frequent insertions/deletions at the beginning
  • Data that grows and shrinks unpredictably
  • When you don’t need random access to elements
  • Stack and queue implementations

Not ideal for:

  • Applications requiring frequent random access (getting element at position i)
  • When memory usage is extremely tight
  • Mathematical operations requiring indexed access

Memory Considerations

Linked Structures:

  • Use more memory per element (need space for references)
  • Memory is allocated as needed (no waste)
  • Memory may be scattered throughout RAM

Arrays:

  • Less memory per element (just the data)
  • May have unused allocated space
  • Memory is contiguous (better cache performance)

πŸ”„ Connections and Review

Let’s tie everything together with the key insights:

The Big Innovation: Linked structures solve the fundamental limitations of arrays by trading direct access for dynamic flexibility.

The Core Pattern: Almost every operation on linked structures follows the same basic steps:

  1. Create nodes as needed
  2. Traverse to find the right position
  3. Adjust references to maintain connections
  4. Update head/tail references as needed

Implementation Strategy:

  • Lists: Use head reference, optionally add tail reference for efficiency
  • Stacks: Use head reference as top (perfect match!)
  • Queues: Need both head and tail references for efficiency

Performance Rule of Thumb:

  • If you need lots of random access: choose arrays
  • If you need lots of insertion/deletion: choose linked structures
  • If you’re implementing stacks/queues: linked structures are often ideal

Real-World Applications:

  • Web browsers use linked structures for browser history
  • Music players use them for playlists
  • Operating systems use them for process management
  • Graphics programs use them for undo/redo functionality

The beauty of linked structures is that once you understand the basic concept of nodes and references, you can build incredibly flexible and powerful data structures that adapt to your program’s needs in real-time. They’re a perfect example of how a simple idea (connecting things with pointers) can solve complex problems!