Maps - Key-Value Storage

CS205 Data Structures

Map<String, Integer> +-------------------------------+ | KEY | VALUE | |-------------|-----------------| | "apple" | 3 | | "banana" | 7 | | "cherry" | 2 | | "date" | 5 | +-------------------------------+ get("banana") --> 7 put("fig", 4) --> adds new entry

Use arrow keys or buttons to navigate

1 / 16

What is a Map?

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.

Sorted Array (by key): entries[] (sorted alphabetically by key) +----+-------------------+ | 0 | ("ant", 6) | Binary search for "dog": +----+-------------------+ | 1 | ("bird", 2) | lo=0, hi=4, mid=2 "cat" < "dog" --> go right +----+-------------------+ | 2 | ("cat", 5) | lo=3, hi=4, mid=3 "dog" == "dog" --> FOUND! +----+-------------------+ | 3 | ("dog", 3) | Result: value = 3 +----+-------------------+ | 4 | ("fish", 8) | Only 2 comparisons instead of 4! +----+-------------------+

Complexity

Operation Time
get(k) O(log n)
put(k,v) - existing key O(log n)
put(k,v) - new key O(n)
remove(k) O(n)

Binary Search Wins 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.

Map contents: +-----------+---------+ | "apple" | 3 | +-----------+---------+ | "banana" | 7 | +-----------+---------+ | "cherry" | 2 | +-----------+---------+ get("banana"): +-----------+---------+ | "apple" | 3 | "apple" == "banana"? No. +-----------+---------+ | "banana" | 7 | "banana" == "banana"? YES! --> return 7 +-----------+---------+ | "cherry" | 2 | (never reached) +-----------+---------+ get("grape"): +-----------+---------+ | "apple" | 3 | "apple" == "grape"? No. +-----------+---------+ | "banana" | 7 | "banana" == "grape"? No. +-----------+---------+ | "cherry" | 2 | "cherry" == "grape"? No. +-----------+---------+ End of list --> return null (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.

Before remove("banana"): entries[] +----+-------------------+ | 0 | ("apple", 3) | +----+-------------------+ | 1 | ("banana", 7) | <-- found at index 1 +----+-------------------+ | 2 | ("cherry", 2) | +----+-------------------+ | 3 | ("date", 5) | +----+-------------------+ Option A: Shift entries up Option B: Swap with last entry +----+-------------------+ +----+-------------------+ | 0 | ("apple", 3) | | 0 | ("apple", 3) | +----+-------------------+ +----+-------------------+ | 1 | ("cherry", 2) | shifted | 1 | ("date", 5) | swapped +----+-------------------+ +----+-------------------+ | 2 | ("date", 5) | shifted | 2 | ("cherry", 2) | +----+-------------------+ +----+-------------------+ O(n) shifting O(1) swap (order doesn't matter!) Returns: 7 (the removed 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.

Sorted Map (keys in alphabetical order): +-----------+---------+ | "ant" | 6 | <-- firstKey() +-----------+---------+ | "bird" | 2 | +-----------+---------+ | "cat" | 5 | +-----------+---------+ subMap("bird","dog") | "dog" | 3 | returns {bird:2, cat:5} +-----------+---------+ | "fish" | 8 | <-- lastKey() +-----------+---------+

Extra Operations

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:

Hash Table (coming soon!)

Key --> hash function --> bucket index buckets[] +----+--------------------+ | 0 | --> ("cat",5) | +----+--------------------+ | 1 | | +----+--------------------+ | 2 | --> ("dog",3) | +----+--------------------+ | 3 | --> ("ant",6) | +----+--------------------+ | 4 | --> ("fish",8) | +----+--------------------+ Average case: get() O(1) put() O(1) remove() O(1) No ordering of keys.

The Dream: O(1) Average

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.

Binary Search Tree

(dog, 3) / \ (cat, 5) (fish, 8) / (ant, 6) Balanced BST: get() O(log n) put() O(log n) remove() O(log n) Keys are maintained in sorted order! Supports range queries.

Best of Both Worlds

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.

Map<K,V> (interface) / | \ / | \ HashMap TreeMap LinkedHashMap (Hash (Red-Black (Hash Table Table) Tree) + Linked List)
Implementation Ordering get/put/remove null keys? Use when
HashMap None (arbitrary) O(1) average Yes (one) Default choice. Fastest.
TreeMap Sorted by key O(log n) No Need sorted keys / ranges.
LinkedHashMap Insertion order O(1) average Yes (one) Need insertion order preserved.
// 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.

16 / 16