A map (also called a dictionary or associative array) stores (key, value) pairs. Each key maps to exactly one value. Keys must be unique.
A map associates keys with values:
KEY (unique) VALUE
+-----------+ +-----------+
| "cat" |---->| 5 |
+-----------+ +-----------+
| "dog" |---->| 3 |
+-----------+ +-----------+
| "fish" |---->| 8 |
+-----------+ +-----------+
| "bird" |---->| 2 |
+-----------+ +-----------+
Given a key, you can quickly find its value.
Keys are UNIQUE -- no two entries share the same key.
Values CAN be duplicated (5, 3, 8, 2... or 5, 5, 5).
Real-World Analogies
Dictionary: word --> definition
Phone book: name --> phone number
Student ID: ID number --> student record
DNS: domain name --> IP address
Core Insight
A map is all about lookup by key. You trade the ability to access by index (like a list) for the ability to access by a meaningful identifier. You don't ask "what is at position 3?" -- you ask "what is the value for key "banana"?"
2 / 16
The Map ADT
The abstract interface that any map implementation must support:
Core Operations
V get(K key)
Return the value associated with key,
or null if key is not found.
V put(K key, V value)
Insert (key, value). If key already
exists, replace old value and return it.
If new, return null.
V remove(K key)
Remove the entry with this key.
Return its value, or null if not found.
boolean containsKey(K key)
Does this key exist in the map?
int size()
Number of entries in the map.
boolean isEmpty()
Is the map empty?
Collection Views
Set<K> keySet()
Return a set of all keys.
Collection<V> values()
Return a collection of all values.
Set<Entry<K,V>> entrySet()
Return a set of all (key, value) pairs.
The Entry Object
Internally, each (key, value) pair is stored as an Entry object. An Entry simply bundles a key and a value together:
class Entry<K, V> {
K key;
V value;
}
Keys Must Be Unique
If you call put("apple", 5) and then put("apple", 9), the map will contain only one entry for "apple" with value 9. The old value 5 is replaced.
3 / 16
Map vs Set vs List
Three fundamental collection types -- each organizes data differently.
LIST (ordered by index):
+-----+-----+-----+-----+-----+
| 0 | 1 | 2 | 3 | 4 | <-- index
| "a" | "b" | "c" | "d" | "e" | <-- values
+-----+-----+-----+-----+-----+
Access: list.get(2) --> "c" Duplicates allowed. Ordered.
SET (unique elements, no index):
{ "a", "b", "c", "d", "e" } No duplicates. No index access.
Access: set.contains("c") --> true Just "is it in there?"
MAP (key-value pairs):
{ "name":"Alice", "age":"25", "city":"NYC" }
Access: map.get("age") --> "25" Unique keys. Values via key lookup.
Feature
List
Set
Map
Access by
Index (0, 1, 2...)
N/A (membership test)
Key
Duplicates
Allowed
Not allowed
Keys unique, values can repeat
Order
Maintained (insertion)
Depends on impl.
Depends on impl.
Use when
Ordered sequence needed
Uniqueness matters
Need key-based lookup
Quick Decision Guide
Need to find items by position? Use a List. Need to check membership? Use a Set. Need to look up a value by its key? Use a Map.
4 / 16
Implementation 1: Unsorted List of Entries
The simplest approach -- store entries in an unordered list (array or linked list).
Unsorted List Implementation:
entries[]
+----+-------------------+
| 0 | ("cat", 5) |
+----+-------------------+
| 1 | ("dog", 3) |
+----+-------------------+
| 2 | ("fish", 8) |
+----+-------------------+
| 3 | ("bird", 2) |
+----+-------------------+
get("fish"): scan from index 0... 1... 2 FOUND! --> 8
put("ant",6): scan all entries (no "ant")... append at end
remove("dog"):scan from 0... 1 FOUND! shift entries down
Complexity
Operation
Time
get(k)
O(n)
put(k,v)
O(n)
remove(k)
O(n)
containsKey(k)
O(n)
put is O(n) because we must first search for an existing key before inserting.
Why O(n) for Everything?
Without any ordering, the only way to find a key is linear scan -- check every entry one by one. For small maps this is fine, but for large datasets it becomes a bottleneck.
When Is This Acceptable?
When the map is very small (say, fewer than 20 entries), the simplicity of an unsorted list outweighs the O(n) cost. Cache-friendly sequential access can be fast in practice for tiny collections.
5 / 16
Implementation 2: Sorted Array
Keep entries sorted by key so we can use binary search for lookups.
Finding a key is now O(log n) -- much better than O(n). But insertion of a new key still requires shifting elements to maintain sorted order, which costs O(n).
The Shifting Problem
Insert ("cow", 4) into sorted array:
[ant] [bird] [cat] [dog] [fish]
^
cow goes here (index 3)
Shift dog and fish right:
[ant] [bird] [cat] [___] [dog] [fish]
Insert:
[ant] [bird] [cat] [cow] [dog] [fish]
6 / 16
Multimap
A variation where a single key can map to multiple values.
Standard Map (one value per key):
+----------+---------+
| "Alice" | "CS" | One value only!
+----------+---------+
Multimap (multiple values per key):
+----------+-------------------+
| "Alice" | ["CS", "Math", |
| | "Physics"] |
+----------+-------------------+
| "Bob" | ["CS", "English"]|
+----------+-------------------+
| "Carol" | ["Math"] |
+----------+-------------------+
Analogy: Student Course Enrollment
A student (key) can be enrolled in many courses (values). A regular map would only let you store one course per student. A multimap stores all of them.
How to Simulate a Multimap
Java does not have a built-in Multimap. Use a Map<K, List<V>>:
Map<String, List<String>> courses
= new HashMap<>();
// Add a course for Alice
courses.computeIfAbsent("Alice",
k -> new ArrayList<>()).add("CS");
courses.computeIfAbsent("Alice",
k -> new ArrayList<>()).add("Math");
// Get all of Alice's courses
List<String> aliceCourses =
courses.get("Alice");
// --> ["CS", "Math"]
When to Use a Multimap
One-to-many relationships
Grouping items by category
Index of words to their positions in text
Graph adjacency lists (vertex --> neighbors)
7 / 16
put() Operation
Two cases: update an existing key, or insert a brand new entry.
CASE 1: Key already exists --> UPDATE the value
Before put("banana", 10): After:
+-----------+---------+ +-----------+---------+
| "apple" | 3 | | "apple" | 3 |
+-----------+---------+ +-----------+---------+
| "banana" | 7 | <-- found! | "banana" | 10 | <-- updated!
+-----------+---------+ +-----------+---------+
| "cherry" | 2 | | "cherry" | 2 |
+-----------+---------+ +-----------+---------+
Returns: 7 (the old value)
CASE 2: Key does NOT exist --> INSERT new entry
Before put("date", 5): After:
+-----------+---------+ +-----------+---------+
| "apple" | 3 | | "apple" | 3 |
+-----------+---------+ +-----------+---------+
| "banana" | 7 | | "banana" | 7 |
+-----------+---------+ +-----------+---------+
| "cherry" | 2 | | "cherry" | 2 |
+-----------+---------+ +-----------+---------+
Key "date" not found! | "date" | 5 | <-- new!
+-----------+---------+
Returns: null (no previous value)
// Pseudocode for put(key, value):
public V put(K key, V value) {
// Step 1: Search for existing key
for (Entry e : entries) {
if (e.key.equals(key)) {
V old = e.value;
e.value = value; // update
return old;
}
}
// Step 2: Key not found, insert new
entries.add(new Entry(key, value));
size++;
return null;
}
put() Always Searches First
Even for insertion, put() must scan all entries to check if the key exists. This is why put() in an unsorted list is O(n), not O(1). The search dominates the cost.
The return value tells you whether it was an update (returns old value) or an insert (returns null).
8 / 16
get() Operation
Search for a key and return its associated value, or null if not found.
// Pseudocode for get(key):
public V get(K key) {
for (Entry e : entries) {
if (e.key.equals(key)) {
return e.value; // found!
}
}
return null; // key not in map
}
null Can Be Ambiguous
If get("grape") returns null, does that mean the key is absent, or that the key exists with a null value? Use containsKey() to disambiguate:
if (map.containsKey("grape")) {
// key exists, value might be null
V val = map.get("grape");
} else {
// key truly does not exist
}
9 / 16
remove() Operation
Find the entry by key, remove it, and return the old value.
// Pseudocode for remove(key):
public V remove(K key) {
for (int i = 0; i < size; i++) {
if (entries[i].key.equals(key)) {
V old = entries[i].value;
// Swap with last entry
entries[i] = entries[size - 1];
entries[size - 1] = null;
size--;
return old;
}
}
return null; // key not found
}
Swap-with-Last Trick
Since an unsorted list has no required order, we can replace the removed entry with the last entry in O(1), avoiding the O(n) shifting cost. The search is still O(n), but the actual removal step is O(1).
This trick does NOT work for sorted arrays -- swapping would break the sorted order.
10 / 16
Ordered Map / Sorted Map
A map that maintains keys in sorted order, enabling range queries and ordered traversal.
K firstKey()
Smallest key in the map.
K lastKey()
Largest key in the map.
SortedMap subMap(K from, K to)
All entries with keys in [from, to).
SortedMap headMap(K to)
All entries with keys < to.
SortedMap tailMap(K from)
All entries with keys >= from.
Why Maintain Sorted Order?
Range queries: "Give me all entries between 'C' and 'M'"
Ordered iteration: process keys from smallest to largest
Min/max: find smallest or largest key in O(log n)
Floor/ceiling: find nearest key to a given value
Analogy: Filing Cabinet
An unsorted map is like tossing files into a box -- fast to add, slow to find. A sorted map is like an alphabetized filing cabinet -- slightly slower to insert (you must find the right slot) but finding files and browsing ranges is fast.
Requirement: Comparable Keys
Keys must implement Comparable (or you must provide a Comparator) so the map knows how to sort them. You can't sort keys that have no natural ordering.
11 / 16
Implementation Preview: What's Coming Next
The unsorted list and sorted array are just the beginning. Two powerful implementations are ahead:
By computing an index directly from the key, hash tables bypass the need to search. The trade-off: keys are unordered, and worst case is O(n) due to collisions.
BSTs give O(log n) for all operations and maintain sorted order. This is why Java's TreeMap uses a Red-Black Tree (a self-balancing BST) internally.
Implementation
get()
put()
remove()
Ordered?
Unsorted List
O(n)
O(n)
O(n)
No
Sorted Array
O(log n)
O(n)
O(n)
Yes
Hash Table
O(1) avg
O(1) avg
O(1) avg
No
Balanced BST
O(log n)
O(log n)
O(log n)
Yes
12 / 16
Application: Word Frequency Counter
Count how many times each word appears in a text. A classic map use case.
Input text: "the cat sat on the mat the cat"
Processing word by word:
Word | Action | Map State
----------|--------------------------------|----------------------------------
"the" | not in map, put("the", 1) | { the:1 }
"cat" | not in map, put("cat", 1) | { the:1, cat:1 }
"sat" | not in map, put("sat", 1) | { the:1, cat:1, sat:1 }
"on" | not in map, put("on", 1) | { the:1, cat:1, sat:1, on:1 }
"the" | already in map! put("the", 2) | { the:2, cat:1, sat:1, on:1 }
"mat" | not in map, put("mat", 1) | { the:2, cat:1, sat:1, on:1, mat:1 }
"the" | already in map! put("the", 3) | { the:3, cat:1, sat:1, on:1, mat:1 }
"cat" | already in map! put("cat", 2) | { the:3, cat:2, sat:1, on:1, mat:1 }
Final result: "the" appears 3 times, "cat" appears 2 times, rest appear once.
// Java implementation
Map<String, Integer> freq = new HashMap<>();
String[] words = text.split(" ");
for (String word : words) {
if (freq.containsKey(word)) {
freq.put(word, freq.get(word) + 1);
} else {
freq.put(word, 1);
}
}
// Cleaner with getOrDefault:
for (String word : words) {
freq.put(word,
freq.getOrDefault(word, 0) + 1);
}
Why a Map Is Perfect Here
We need to associate each unique word (key) with its count (value). A map gives us:
Fast lookup to check if word seen before
Fast update to increment the count
Automatic uniqueness of keys
Analogy: Tally Sheet
Imagine reading words aloud and keeping a tally sheet. Each row has a word and tally marks. Seen a new word? Add a row. Seen it again? Add a tally mark. The map IS the tally sheet.
13 / 16
Application: Two-Sum Problem
Given an array and a target sum, find two numbers that add up to the target. A classic interview question solved elegantly with a map.
Problem: nums = [2, 7, 11, 15], target = 9
Find two numbers that add up to 9. Return their indices.
BRUTE FORCE (O(n^2)): Check every pair. Slow!
MAP APPROACH (O(n)): For each number, check if (target - number) is in the map.
Step | num | complement | Map contains | Action
| | (target-num) | complement? |
-------|-----|---------------|----------------------|-----------------------------
1 | 2 | 9 - 2 = 7 | {} --> No | Store 2 at index 0: {2:0}
2 | 7 | 9 - 7 = 2 | {2:0} --> YES! | Found! indices [0, 1]
| | | map.get(2) = 0 | return [0, 1]
Answer: nums[0] + nums[1] = 2 + 7 = 9
// Java solution
public int[] twoSum(int[] nums,
int target) {
// Map: value --> index
Map<Integer, Integer> seen
= new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement =
target - nums[i];
if (seen.containsKey(complement)) {
return new int[] {
seen.get(complement), i
};
}
seen.put(nums[i], i);
}
throw new IllegalArgumentException(
"No two-sum solution");
}
The Key Insight
Instead of checking every pair (O(n^2)), we store each number we've seen so far in a map. For each new number, we compute its complement (target - num) and check if we've already seen it. One pass through the array = O(n).
Analogy: Looking for a Dance Partner
Imagine people entering a room one at a time, each wearing a number. Each person asks: "Is someone here whose number plus mine equals the target?" If yes -- pair found! If no -- sit down and wait. The map is the room's registry of who's already inside.
14 / 16
Java's Map Interface
Java provides several Map implementations, each with different trade-offs.
// HashMap -- most common
Map<String, Integer> map
= new HashMap<>();
map.put("apple", 3);
map.put("banana", 7);
map.get("apple"); // 3
map.containsKey("cherry"); // false
map.size(); // 2
// Iterate over entries
for (Map.Entry<String, Integer> e
: map.entrySet()) {
System.out.println(
e.getKey() + ": " + e.getValue());
}
Decision Flowchart
Need fastest lookups and don't care about order? Use HashMap.
Need keys in sorted order or range queries? Use TreeMap.
Need to remember insertion order (e.g., LRU cache)? Use LinkedHashMap.
Common Mistake
Don't assume HashMap entries come out in any particular order. If you iterate over a HashMap, the order can change when entries are added or removed. If you need ordering, use TreeMap or LinkedHashMap.
15 / 16
Summary & Cheat Sheet
MAP = collection of (key, value) pairs
Keys must be UNIQUE
Values can repeat
CORE OPERATIONS:
get(key) --> value or null
put(key, val) --> old value or null
remove(key) --> old value or null
containsKey(k) --> boolean
size() --> int
isEmpty() --> boolean
VIEWS:
keySet() --> Set of all keys
values() --> Collection of all values
entrySet() --> Set of Entry objects
IMPLEMENTATIONS:
Unsorted List O(n) / O(n) / O(n)
Sorted Array O(log n) / O(n) / O(n)
Hash Table O(1) avg / O(1) / O(1)
Balanced BST O(log n) all ops, sorted
JAVA CLASSES:
HashMap --> fast, unordered
TreeMap --> sorted by key
LinkedHashMap --> insertion order
When to Use a Map
Need to look up values by a key (not by index)
Need to count occurrences (word frequency)
Need to associate related data (ID to record)
Need to check for existence quickly
Need to eliminate duplicates while tracking info
The Big Picture
Lists are about ordering. Sets are about uniqueness. Maps are about association -- connecting a key to its value. Maps are arguably the most widely used data structure in real-world programming.
Coming Next
Hash Tables -- how to achieve O(1) average-case performance using hash functions and buckets. We'll see how collisions are handled and why the load factor matters.