π Arrays
Definition
An array is a sequenced collection of variables all of the same type. Each variable, or cell, in an array has an index, which uniquely refers to the value stored in that cell.
The cells of an array A are numbered 0, 1, 2, and so on.
Key Concepts
π Array Length and Capacity
- Length/Capacity: The maximum number of elements that can be stored in the array
- In Java: accessed using
a.length - Cells are numbered from
0toa.length - 1 - Access cell with index k:
a[k]
Declaring Arrays
Method 1: Literal Form
Method 2: Using 'new' Operator
Array Operations
βοΈ Adding an Entry at Index i
Process: To add entry e at index i, shift forward the n - i entries from board[i] to board[n-1]
Time Complexity: O(n) in worst case (when i = 0)
ποΈ Removing an Entry at Index i
Process: To remove entry at index i, shift backward the n - i - 1 entries from board[i+1] to board[n-1]
Time Complexity: O(n) in worst case (when i = 0)
Performance Summary
| Operation | Time Complexity | Notes |
|---|---|---|
| Access element at index i | O(1) | Direct access via a[i] |
| Insert at index i | O(n) | Need to shift elements |
| Remove at index i | O(n) | Need to shift elements |
| Search for element | O(n) | Must check each element |
π Singly Linked Lists
Definition
A singly linked list is a concrete data structure consisting of a sequence of nodes, starting from a head pointer. Each node stores:
- An element (the data)
- A link to the next node
The last node's link points to null.
β οΈ Key Characteristics
- Can only move forward through the list
- Each node has a reference to the next node only
- The last node points to null
- Requires a head pointer to access the list
- Often includes a tail pointer for efficient insertion at end
Node Class Implementation
Basic Operations
β Inserting at the Head
Steps:
- Allocate a new node
- Insert new element
- Have new node point to old head
- Update head to point to new node
Time Complexity: O(1)
β Inserting at the Tail
Steps:
- Allocate a new node
- Insert new element
- Have new node point to null
- Have old last node point to new node
- Update tail to point to new node
Time Complexity: O(1)
β Removing at the Head
Steps:
- Update head to point to next node
- Allow garbage collector to reclaim the former first node
Time Complexity: O(1)
β οΈ Removing at the Tail - NOT EFFICIENT!
Removing at the tail of a singly linked list is not efficient because:
- There is no constant-time way to update the tail to point to the previous node
- We would need to traverse the entire list to find the second-to-last node
- Time Complexity: O(n)
Performance Summary
| Operation | Time Complexity | Notes |
|---|---|---|
| Insert at head | O(1) | Just update head pointer |
| Insert at tail | O(1) | With tail pointer |
| Remove at head | O(1) | Just update head pointer |
| Remove at tail | O(n) | Must traverse to find previous node |
| Access element at index i | O(n) | Must traverse from head |
ππ Doubly Linked Lists
Definition
A doubly linked list provides a more versatile data structure that can be traversed forward and backward. Each node stores:
- An element (the data)
- A link to the previous node
- A link to the next node
Uses special header and trailer sentinel nodes (with no data).
β οΈ Key Advantages over Singly Linked Lists
- Can traverse both forward and backward
- Can efficiently remove at the tail - O(1)
- Can efficiently insert before a given node
- Easier to implement certain operations
Node Class Implementation
Sentinel Nodes
π Header and Trailer Nodes
- Header: Dummy node at the beginning (stores no data)
- Trailer: Dummy node at the end (stores no data)
- Purpose: Simplify implementation by eliminating special cases for empty lists
- An empty list has header.next = trailer and trailer.prev = header
Basic Operations
β Inserting Between Two Nodes
Steps:
- Create new node
- Link new node to predecessor and successor
- Link predecessor to new node
- Link successor to new node
Time Complexity: O(1)
β Removing a Node
Steps:
- Get predecessor and successor of node to remove
- Link predecessor to successor
- Link successor to predecessor
- Allow garbage collection of removed node
Time Complexity: O(1)
Performance Summary
| Operation | Time Complexity | Notes |
|---|---|---|
| Insert at head | O(1) | After header node |
| Insert at tail | O(1) | Before trailer node |
| Remove at head | O(1) | After header node |
| Remove at tail | O(1) | β NOW EFFICIENT! Can access previous node |
| Insert/Remove at given node | O(1) | If we have reference to the node |
βοΈ Arrays vs. Linked Lists Comparison
π Arrays
Advantages:
- β Fast random access - O(1)
- β Memory efficient (no extra pointers)
- β Better cache locality
- β Simple implementation
Disadvantages:
- β Slow insertion/deletion - O(n)
- β Fixed size (or expensive resizing)
- β Wasted space if not full
π Linked Lists
Advantages:
- β Fast insertion/deletion at known positions - O(1)
- β Dynamic size
- β No wasted space
- β Easy to split/merge lists
Disadvantages:
- β Slow random access - O(n)
- β Extra memory for pointers
- β Poor cache locality
When to Use Each
Use Arrays when:
- You need fast random access by index
- The size is known and relatively fixed
- Memory is limited (no extra pointers)
- You're doing lots of reading, little insertion/deletion
Use Linked Lists when:
- You need frequent insertions/deletions
- The size changes dynamically
- You don't need random access
- You're implementing queues, stacks, or other sequential structures
π Dynamic Array Lists (Growable Arrays)
The Problem with Fixed Arrays
Standard arrays have a fixed capacity. What if we don't know the size in advance? What if we need to grow the array?
Solution: Dynamic Arrays
When the array becomes full, we can:
- Create a new, larger array
- Copy all elements from the old array to the new array
- Replace the reference to point to the new array
Growth Strategies
Incremental Strategy
Increase size by a constant c
New size = old size + c
Analysis:
- Replace array k = n/c times
- Total time: n + c + 2c + 3c + ... + kc
- T(n) = O(n + kΒ²) = O(nΒ²)
- Amortized time per push: O(n)
β Not efficient!
Doubling Strategy
Double the size each time
New size = 2 Γ old size
Analysis:
- Replace array k = logβ n times
- Total time: n + 1 + 2 + 4 + 8 + ... + 2^k
- T(n) = n + 2^(k+1) - 1 = O(n)
- Amortized time per push: O(1)
β Much better!
Implementation
π― Key Takeaway: Amortized Analysis
Amortized time is the average time per operation over a sequence of operations.
With the doubling strategy:
- Most insertions are cheap (O(1))
- Occasionally we have an expensive operation (O(n)) when we resize
- But resizing happens so infrequently that the average is still O(1)!
Java's ArrayList
Java's java.util.ArrayList implements this dynamic array concept:
- Automatically grows as needed
- Provides O(1) amortized insertion at end
- Provides O(1) access by index
- Still O(n) for insertion/deletion in the middle
π Positional Lists
What is a Positional List?
A positional list is an ADT that provides a general abstraction of a sequence with the ability to identify the location of an element.
A position acts as a marker or token within the list, independent of the element's index.
π― Key Concepts
- A position is unaffected by changes elsewhere in the list
- Only becomes invalid if explicitly deleted
- Supports efficient insertion/deletion at any position
- Natural implementation: doubly linked list
Position Interface
Positional List ADT Methods
Accessor Methods
| Method | Description | Complexity |
|---|---|---|
first() |
Returns position of first element (or null if empty) | O(1) |
last() |
Returns position of last element (or null if empty) | O(1) |
before(p) |
Returns position immediately before p (or null) | O(1) |
after(p) |
Returns position immediately after p (or null) | O(1) |
isEmpty() |
Returns true if list contains no elements | O(1) |
size() |
Returns number of elements in the list | O(1) |
Update Methods
| Method | Description | Complexity |
|---|---|---|
addFirst(e) |
Insert element at front, return its position | O(1) |
addLast(e) |
Insert element at back, return its position | O(1) |
addBefore(p, e) |
Insert element just before position p | O(1) |
addAfter(p, e) |
Insert element just after position p | O(1) |
set(p, e) |
Replace element at position p, return old element | O(1) |
remove(p) |
Remove and return element at position p | O(1) |
Example Usage
π Iterators
What is an Iterator?
An iterator is a software design pattern that abstracts the process of scanning through a sequence of elements, one element at a time.
Iterator Interface
Iterable Interface
Collections implement the Iterable interface, which provides a method to obtain an iterator:
Using Iterators
Traditional Way
For-Each Loop (Syntactic Sugar)
Java's for-each loop is syntactic sugar for iterator usage:
π― Key Benefits of Iterators
- Abstraction: Same interface for different data structures
- Flexibility: Can have multiple iterators on same collection
- Safety: Can remove elements safely during iteration
- Simplicity: For-each loop provides clean syntax
Implementing an Iterator
Example: Iterator for ArrayList
π Complete Summary
Time Complexity Comparison
| Operation | Array | Singly Linked | Doubly Linked |
|---|---|---|---|
| Access by index | O(1) | O(n) | O(n) |
| Insert at head | O(n) | O(1) | O(1) |
| Insert at tail | O(n) | O(1) | O(1) |
| Remove at head | O(n) | O(1) | O(1) |
| Remove at tail | O(n) | O(n) | O(1) |
| Insert at middle | O(n) | O(n)* | O(n)* |
* O(1) if position is already known
π Key Takeaways
1. Arrays
- Best for: Random access and fixed-size collections
- Weakness: Slow insertion/deletion
- Memory: Contiguous, good cache locality
2. Singly Linked Lists
- Best for: Sequential access, frequent head operations
- Weakness: Can't efficiently remove at tail, no backward traversal
- Memory: Scattered, extra space for next pointer
3. Doubly Linked Lists
- Best for: Bidirectional traversal, operations at both ends
- Weakness: More memory (two pointers per node)
- Memory: Scattered, extra space for prev and next pointers
4. Dynamic Arrays (ArrayList)
- Combines array access speed with dynamic sizing
- Doubling strategy gives O(1) amortized insertion at end
- Best general-purpose list in Java
5. Positional Lists
- Abstract positions independent of indices
- Naturally implemented with doubly linked lists
- All operations in O(1) time if position is known
6. Iterators
- Provide uniform way to traverse collections
- Enable for-each loops
- Allow multiple simultaneous traversals
π‘ Study Tips & Practice Questions
π― What You Must Know
- Time complexity of operations for each data structure
- When to use arrays vs. linked lists
- How to implement basic operations (insert, delete, traverse)
- The difference between singly and doubly linked lists
- How dynamic arrays work (doubling strategy)
- What amortized analysis means
- How iterators work and why they're useful
Practice Questions
Question 1: Array Operations
Q: Why is inserting an element in the middle of an array O(n)?
A: Because we need to shift all elements after the insertion point one position to the right. In the worst case (inserting at index 0), we shift n elements.
Question 2: Linked List Advantage
Q: When is a linked list better than an array?
A: When you need frequent insertions/deletions and don't need random access. Linked lists can insert/delete in O(1) time if you have the position, while arrays require O(n) time for shifting elements.
Question 3: Doubly vs. Singly Linked
Q: What's the main advantage of a doubly linked list over a singly linked list?
A: Doubly linked lists can efficiently remove at the tail (O(1)) and traverse backward. Singly linked lists require O(n) time to remove at the tail because they must traverse to find the previous node.
Question 4: Dynamic Arrays
Q: Why is the doubling strategy for dynamic arrays better than incrementing by a constant?
A: Doubling gives O(1) amortized time per insertion (total O(n) for n insertions), while incrementing by a constant gives O(n) amortized time (total O(nΒ²) for n insertions). Doubling reduces the frequency of expensive resize operations.
Question 5: Iterator Usage
Q: What does "iterable" mean in Java?
A: A class is iterable if it implements the Iterable interface, meaning it provides an iterator() method that returns an Iterator object. This allows the class to be used in for-each loops.