Chapter 4: Array Implementations of Collection ADTs

🎯 Learning Objectives

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

  • Understand the complete ADT creation process from specification to implementation
  • Implement Lists using arrays with full CRUD operations (Create, Read, Update, Delete)
  • Build Stack ADTs using arrays with proper LIFO (Last In, First Out) behavior
  • Create Queue ADTs using arrays with three different implementation approaches
  • Compare and contrast the strengths and weaknesses of each array implementation method
  • Analyze the time complexity and efficiency of different array-based operations
  • Implement dynamic array expansion to handle capacity limitations
  • Create and use iterators to traverse collection elements systematically
  • Apply generic types to make your implementations type-safe and reusable
  • Handle edge cases like empty collections, full arrays, and invalid operations

🌟 The Big Picture

Think of this chapter as your blueprint for building the fundamental storage containers that almost every program needs. You know how in real life you have different containers for different purposes - filing cabinets for documents, stacks of plates, and queues at the grocery store? In programming, we need similar organized ways to store and manage data.

Here’s what makes this topic crucial: Every application you’ll ever build will need to store collections of data. Whether it’s a social media app storing posts, a game tracking high scores, or a banking system managing transactions, you’ll need these fundamental data structures. Understanding how to implement them with arrays gives you the foundation to choose the right tool for each job and even create custom solutions when needed.

The beauty of what you’re about to learn is that we’re bridging two worlds: the abstract world (what we want our data structures to do) and the concrete world (how we actually make them work using arrays). This is like learning both the architectural design of a house and the actual construction techniques!

📚 Core Concepts

The Three-Step ADT Creation Recipe

Before we dive into specific data structures, let’s understand the systematic approach computer scientists use to create any Abstract Data Type. Think of this as your recipe for building robust data structures:

Step 1: Write the ADT Specification

  • Define what your data structure should do (the “what”)
  • List all the operations it needs to support
  • Specify how it should behave in different situations
  • Important: Don’t think about implementation yet - focus purely on behavior!

Step 2: Create the Implementation

  • 2a: Write a Java interface containing all your operations
  • 2b: Write a Java class that actually implements those operations
  • Choose your underlying data representation (like arrays)
  • Code all the methods from your interface

Step 3: Use Your ADT

  • Write client programs that use your implementation
  • Test edge cases and normal operations
  • Verify everything works as specified

Understanding Collection ADTs

Collection ADTs are your digital containers - they store multiple pieces of data and provide organized ways to access them. The three most fundamental types you’ll master are:

  • Lists: Flexible collections where you can add, remove, and access items at any position
  • Stacks: “Last in, first out” collections (like a stack of plates)
  • Queues: “First in, first out” collections (like a line at a coffee shop)

Each serves different purposes, and understanding when to use which one will make you a much more effective programmer!


đź“‹ Lists: Your Flexible Data Organizer

What Makes Lists Special

Lists are like super-powered arrays that can grow, shrink, and reorganize themselves. Think of your to-do list - you can add items anywhere, cross them off, rearrange them, and check what’s at any position.

Visual Reference (Slide 6):

The slide shows a person thinking about organizing tasks, with a thought bubble containing: “I have so much to do this weekend — I should make a list. To Do:

  1. Read Chapter 4
  2. Call home
  3. Buy card for Sue”

This perfectly illustrates how lists mirror our natural way of organizing information!

Core List Operations

Here’s what every list implementation needs to handle:

  1. Add new entries - at the end or at any specific position
  2. Remove items - from any position in the list
  3. Replace entries - update what’s stored at a specific position
  4. Get entries - retrieve what’s stored at any position
  5. Count entries - know how many items you have
  6. Check status - is it empty? is it full?

Handling Edge Cases Like a Pro

When you design any ADT, you need to decide how to handle unusual situations. Here are your main options:

  • Assume problems won’t happen (risky but simple)
  • Ignore invalid requests (sometimes appropriate)
  • Make reasonable assumptions (provide predictable fallback behavior)
  • Return success/failure indicators (let the caller decide what to do)
  • Throw exceptions (force the caller to handle problems)

The choice depends on your specific application needs and how much control you want to give to the users of your ADT.

Array Implementation of Lists

When you implement a list using an array, you need two key pieces of information:

T[] array;              // Stores your actual list entries
int numberOfEntries;    // Tracks how many items you currently have

Important Note: T represents a generic type - this makes your list work with any data type (Strings, Integers, custom objects, etc.). It’s like having a universal container!

Visual Reference (Slide 17):

The slide shows an array representation:

[0] [1] [2] [3] [4] [5] [6] [length]
apple|orange|durian|lemon|plum|  |  |  5

This shows how your array stores items sequentially, with numberOfEntries tracking the current count.

The add() Method - Two Flavors

Adding at the End (Simple Version):

  1. Place the new item at position numberOfEntries
  2. Increment numberOfEntries

Adding at a Specific Position (Complex Version):

  1. Shift all items from that position onwards one spot to the right
  2. Insert your new item at the desired position
  3. Increment numberOfEntries

Visual Reference (Slide 18):

The slide illustrates adding “Carla” as the third entry, showing how existing entries (Doug, Haley) get shifted right to make room.

The remove() Method - Cleaning Up Gaps

Removing items requires careful gap management:

  1. Save the item you’re removing (to return it)
  2. Shift all items to the right of that position one spot left
  3. Decrement numberOfEntries
  4. Pro tip: Set the last position to null to help garbage collection

Visual Reference (Slide 21):

Shows removing “Bob” from position 2, with all subsequent entries shifting left to fill the gap.

Dynamic Array Expansion - Never Run Out of Space!

Here’s where things get really cool! When your array fills up, you don’t just give up - you create a bigger array and move everything over.

The Expansion Process:

  1. Create a new array (typically double the size)
  2. Copy all existing elements to the new array
  3. Update your array reference to point to the new array
  4. The old array gets garbage collected automatically

Visual Reference (Slides 26-27):

Shows the step-by-step process of expanding from a 5-element array to a 10-element array, maintaining all existing data.

List Implementation Strengths and Weaknesses

Strengths:

  • âś… Fast retrieval - accessing any element is O(1)
  • âś… Fast addition at end - just drop it in the next spot
  • âś… Memory efficient - no wasted space between elements
  • âś… Simple to understand - straightforward array indexing

Weaknesses:

  • ❌ Slow insertion/deletion in middle - requires shifting elements
  • ❌ Expansion overhead - occasionally need to copy entire array
  • ❌ Fixed capacity - (unless you implement dynamic expansion)

🥞 Stacks: The “Last In, First Out” Champion

Understanding Stack Behavior

Imagine a stack of plates in a cafeteria - you always add new plates to the top, and you always take plates from the top. That’s exactly how a stack ADT works! This LIFO (Last In, First Out) behavior is incredibly useful for many programming scenarios.

Real-world applications where stacks shine:

  • Function calls - your program uses a stack to remember where to return after each function
  • Undo operations - each action gets pushed onto a stack
  • Expression evaluation - converting mathematical expressions
  • Web browser history - back button functionality

Core Stack Operations

Every stack needs these fundamental operations:

  • push(newEntry) - Add a new item to the top
  • pop() - Remove and return the top item
  • peek() - Look at the top item without removing it
  • isEmpty() - Check if the stack has any items
  • clear() - Remove all items

Smart Array Implementation Strategy

Here’s a crucial design decision: Where should the “top” of your stack be in the array?

Best Practice: Make the top of your stack the last occupied position in the array. Why? Because this avoids the need to shift elements every time you push or pop!

T[] array;        // Your stack storage
int topIndex;     // Index of the current top element (-1 means empty)

Visual Reference (Slide 36):

The slide compares two approaches - having the top at index 0 vs. having the top at the last position. The second approach (highlighted in red) is much more efficient.

Stack Operations Implementation

push() Method:

  1. Increment topIndex
  2. Place new item at array[topIndex]

pop() Method:

  1. Save array[topIndex] to return later
  2. Set array[topIndex] = null (good memory management)
  3. Decrement topIndex
  4. Return the saved item

Visual Reference (Slides 40-41):

Shows the step-by-step process of popping an element, including setting the old position to null and updating the topIndex.

Exercise: Binary Conversion with Stacks

Here’s a perfect example of stack usefulness! Converting a decimal number to binary:

Algorithm:

  1. Create a stack of integers
  2. While number > 0:
    • Push (number % 2) onto stack
    • Set number = number / 2
  3. Pop all digits and concatenate them into a string

Why use a stack? Because the division process gives us digits in reverse order, and the stack flips them back to the correct order!


🚶‍♀️ Queues: The “First In, First Out” Organizer

Understanding Queue Behavior

Think of a queue like a line at your favorite coffee shop - the first person in line gets served first. This FIFO (First In, First Out) behavior is essential for fair scheduling and processing.

Real-world applications:

  • Print job scheduling - documents print in the order they were submitted
  • CPU task scheduling - fair processing of computer tasks
  • Network packet handling - data packets processed in arrival order
  • Customer service systems - first caller gets first assistance

Core Queue Operations

  • enqueue(newEntry) - Add item to the back of the queue
  • dequeue() - Remove and return item from the front
  • getFront() - Look at the front item without removing it
  • isEmpty() - Check if queue is empty
  • clear() - Remove all items

Three Array Implementation Approaches

This is where queues get interesting - there are multiple ways to implement them with arrays, each with different trade-offs!

Approach 1: Linear Array with Fixed Front

The Setup:

  • Front of queue always at index 0
  • backIndex tracks the last item (-1 means empty)

Operations:

  • enqueue(): Increment backIndex, add item there
  • dequeue(): Remove item at index 0, shift everyone left, decrement backIndex

Pros and Cons:

  • âś… Easy to understand and visualize
  • ❌ Major weakness: dequeue() requires shifting all remaining elements!

Approach 2: Linear Array with Dynamic Front

The Setup:

  • frontIndex and backIndex both move dynamically
  • Initially: frontIndex = 0, backIndex = -1

Operations:

  • enqueue(): Increment backIndex, add item there
  • dequeue(): Remove item at frontIndex, increment frontIndex
  • isEmpty(): backIndex < frontIndex

Visual Reference (Slide 59):

Shows how frontIndex moves right as items are dequeued, leaving empty spaces behind.

Pros and Cons:

  • âś… No shifting required - much faster dequeue!
  • ❌ Problem: “Rightward drift” - array appears full even with empty spaces at the beginning

Approach 3: Circular Array (The Elegant Solution!)

The Brilliant Idea: When you reach the end of the array, wrap around to the beginning! It’s like the array forms a circle.

The Setup:

  • Use modulo arithmetic to wrap indices: (index + 1) % array.length
  • Need a way to distinguish between empty and full conditions

Operations:

// enqueue()
backIndex = (backIndex + 1) % array.length;
array[backIndex] = newEntry;
 
// dequeue()
T frontEntry = array[frontIndex];
array[frontIndex] = null;
frontIndex = (frontIndex + 1) % array.length;
return frontEntry;

Visual Reference (Slides 64-66):

Shows how queue entries can wrap around from the end of the array back to the beginning, maximizing space utilization.

Solving the Empty vs. Full Problem

With circular arrays, you face a tricky situation: when frontIndex == backIndex + 1, is the queue empty or full? Here are two solutions:

Solution 1: Use a Counter

  • Keep track of how many items are in the queue
  • Empty when count == 0, full when count == array.length

Solution 2: Leave One Space Unused

  • Always keep one array position empty
  • This makes empty vs. full conditions distinguishable
  • isEmpty(): (backIndex + 1) % array.length == frontIndex

Visual Reference (Slides 74-75):

Demonstrates how leaving one unused space allows you to clearly distinguish between empty and full conditions.

Queue Implementation Comparison

Approachenqueue() Speeddequeue() SpeedSpace EfficiencyComplexity
Fixed FrontFastSlow (shifting)GoodLow
Dynamic FrontFastFastPoor (drift)Medium
Circular ArrayFastFastExcellentHigher

Winner: Circular array implementation gives you the best of all worlds!


🔄 Iterators: Your Collection Navigator

Why Iterators Matter

Sometimes you need to go through every item in your collection one by one. Iterators provide a standardized, safe way to traverse any collection without needing to know its internal structure.

Think of an iterator as: A bookmark that moves through your collection, always knowing where it is and what comes next.

Java’s Iterator Interface

Java provides a standard Iterator<T> interface with three key methods:

  • hasNext() - Returns true if there are more elements to visit
  • next() - Returns the next element and advances the iterator
  • remove() - Removes the last element returned by next()

Implementing Iterators with Inner Classes

The Pattern:

  1. Create a private inner class that implements Iterator<T>
  2. Give your collection a public getIterator() method
  3. The inner class has access to your collection’s private data

Visual Reference (Slide 86):

Shows the complete implementation pattern with an inner ArrayQueueIterator class.

Example Usage:

QueueInterface<String> myQueue = new ArrayQueue<>();
// ... add some items ...
 
Iterator<String> iterator = myQueue.getIterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    System.out.println(item);
}

🔄 Connections and Review

The Big Picture Connections

Now that you’ve mastered all three collection types, let’s see how they work together:

Lists are your Swiss Army knife - flexible and powerful, perfect when you need random access and frequent insertions/deletions at various positions.

Stacks are your specialist tool for LIFO scenarios - perfect for managing function calls, undo operations, and any situation where “most recent first” makes sense.

Queues are your fairness enforcers - essential for any system where order of arrival matters, like task scheduling or print job management.

Implementation Strategy Summary

Array-based implementations offer:

  • Excellent memory locality (fast access)
  • Predictable memory usage
  • Simple implementation for basic operations

Key challenges you’ve learned to handle:

  • Dynamic expansion when arrays fill up
  • Efficient gap management during deletions
  • Circular indexing for optimal space utilization
  • Generic type safety for reusable code

Performance Characteristics

OperationListStackQueue (Circular)
Access by indexO(1)N/AN/A
Add at endO(1)*O(1)O(1)
Add at beginningO(n)N/AN/A
Remove from endO(1)O(1)N/A
Remove from frontO(n)N/AO(1)

*O(1) amortized when using dynamic expansion

Your Implementation Toolkit

You now have the fundamental building blocks for creating robust data structures:

  1. Generic type support for type safety and reusability
  2. Dynamic expansion for handling growth
  3. Proper error handling for edge cases
  4. Iterator support for safe traversal
  5. Efficient algorithms for core operations

These skills will serve as the foundation for more advanced data structures like linked lists, trees, and hash tables that you’ll encounter later in your programming journey!

Remember: choosing the right data structure for your specific problem is often more important than optimizing the implementation. Understanding the strengths and weaknesses of each approach will make you a much more effective problem solver.