Skip to content

Beginner: foundations · Hashing, two pointers and sliding window · topic 12 of 61 · 35 min

Counting and grouping with hashing

Explanation

A frequency map counts how often each item appears, in one O(n) pass. The same idea groups items that share a key (anagrams share their sorted letters), ranks the top k most frequent items with a clear tie rule, and, together with prefix sums, counts the subarrays that add up to k in O(n), even with negative numbers.

Counting is the most common use of a map

A frequency map stores each distinct item with the number of times it appears. One pass builds it: O(n) time and O(d) space, where d is the number of distinct items. Python gives you three ways to write it.

python
words = ["upi", "card", "upi", "cash", "upi", "card"]

counts = {}
for w in words:
    counts[w] = counts.get(w, 0) + 1
print(counts)              # Output: {'upi': 3, 'card': 2, 'cash': 1}

from collections import defaultdict, Counter
dd = defaultdict(int)      # a missing key starts at int(), which is 0
for w in words:
    dd[w] += 1

c = Counter(words)
print(c.most_common(2))    # Output: [('upi', 3), ('card', 2)]
print(c["wallet"])         # Output: 0  (a missing key counts as 0)

Counter is a dict made for counting. Reading a missing key gives 0 instead of a KeyError, and most_common(k) returns the k most common items; items with equal counts come in the order they were first seen.

First unique character

To find the first character that appears exactly once, count everything in a first pass, then walk the string again and return the first index whose count is 1. Two passes are still O(n). The map holds one entry per distinct character, so for lowercase English letters it never has more than 26 entries: O(1) extra space in that case.

Grouping: same key, same list

Grouping puts items that share some property into one list. Choose a key function so that items in the same group get the same key, then keep a dict from key to list. For anagrams ("eat", "tea", "ate") two keys work well:

  • The sorted word: "".join(sorted(w)) turns all three into "aet". Cost O(m log m) for a word of length m.
  • A count tuple: 26 counts, one per letter, stored as a tuple because a list cannot be a key. Cost O(m) per word, better for long words.
python
from collections import defaultdict

groups = defaultdict(list)
for w in ["eat", "tea", "tan", "ate", "nat", "bat"]:
    groups["".join(sorted(w))].append(w)
print(list(groups.values()))
# Output: [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]
n words of length up to m: O(n × m log m) time, O(n × m) space for the groups.

Top-k frequent, with a tie rule

"Return the k most frequent words" needs a rule for ties, or two correct programs can print different answers. A common rule: higher count first, and for equal counts, alphabetical order. Sorting by the key (−count, word) does both at once. Always ask about ties in an interview.

python
from collections import Counter

def top_k(words, k):
    counts = Counter(words)
    ranked = sorted(counts, key=lambda w: (-counts[w], w))
    return ranked[:k]

print(top_k(["b", "a", "c", "a", "b", "d"], 2))   # Output: ['a', 'b']
a and b both appear twice; the tie rule puts a first. Sorting d distinct words is O(d log d).

A heap can do this in O(d log k) (Module 8), and grouping words into buckets by count can do it in O(n). The sorting version is clear and usually fast enough.

Subarray sum equals k: prefix sums plus a map

Module 2 previewed this pattern; here is why it works. Let running be the sum of all items so far. A subarray that ends at the current item sums to k exactly when some EARLIER running total equals running − k. So walk once, and at each step add the number of earlier prefixes equal to running − k. A dict from "prefix sum" to "times seen" answers that in O(1).

Item xrunningrunning − kSeen beforeCount
(start)0——0
11−200
23011
−12−101
24112

The trace is for nums = [1, 2, −1, 2] and k = 3. The two subarrays found are [1, 2] and [2, −1, 2]. The dict starts as {0: 1} for the empty prefix, which is how a subarray starting at index 0 gets counted.

python
def subarray_sum_count(nums, k):
    seen = {0: 1}
    running = count = 0
    for x in nums:
        running += x
        count += seen.get(running - k, 0)    # look up first...
        seen[running] = seen.get(running, 0) + 1   # ...then store
    return count

print(subarray_sum_count([1, 2, -1, 2], 3))   # Output: 2
O(n) time and O(n) space. Works with negative numbers, unlike the sliding window of Topic 5.

Look up BEFORE storing the current running total. Otherwise, when k = 0, every prefix would match itself and count an empty subarray.

Common mistakes

  • counts[w] += 1 on a plain dict for a new key raises KeyError. Fix: counts.get(w, 0) + 1, defaultdict(int) or Counter.
  • Using a list or a set as the group key. Fix: a sorted string or a tuple of counts.
  • No tie rule in top-k, so the output can change between programs or languages. Fix: sort by (−count, item) and say the rule aloud.
  • In subarray-sum, forgetting seen = {0: 1}, or storing running before looking it up. Fix: start with the empty prefix; look up first, then store.
  • Returning groups in whatever order the dict gives when the task asks for a fixed order. Fix: sort exactly as the task says.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Counting and grouping with hashing · 1/8

Counting and grouping

This lesson uses hash maps to count, to group, and to count subarrays with a given sum.

Prefer a video by a teacher? Find free videos on “group anagrams top k frequent subarray sum equals k prefix sum hashmap” on YouTube ↗

In real life: Most ordered dishes in each city

A food delivery app shows "popular near you". An analytics job can count orders by the pair (city, dish): a tuple is a valid dict key, so one Counter over a day's orders gives every count in one pass, and a sort with a tie rule picks the top dishes per city. Real systems run this over huge logs with SQL GROUP BY or big-data tools, but the idea is the same frequency map.

python
from collections import Counter

orders = [("Pune", "misal pav"), ("Pune", "vada pav"), ("Chennai", "dosa"),
          ("Pune", "misal pav"), ("Chennai", "dosa"), ("Chennai", "idli")]
counts = Counter(orders)
pune = sorted((dish for city, dish in counts if city == "Pune"),
              key=lambda d: (-counts[("Pune", d)], d))
print(pune)
Shows ['misal pav', 'vada pav']: misal pav was ordered twice in Pune.

Practice (0/4 done)

Task 1 · Code · Python

Write first_unique_char(s) that returns the index of the first character that appears exactly once in s, or -1 if there is none. Case matters ("a" and "A" are different) and spaces count as characters. O(n) time. Example: first_unique_char("loveleetcode") returns 2.

Task 2 · Code · Python

Write group_anagrams(words) that groups words that are anagrams of each other (same letters, same counts). For a fixed answer: sort the words inside each group alphabetically, then sort the list of groups (Python compares lists item by item, so groups are ordered by their first word). Example: group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"]) returns [["ate", "eat", "tea"], ["bat"], ["nat", "tan"]].

Task 3 · Code · Python

Write subarray_sum_count(nums, k) that returns how many subarrays (runs of one or more neighbouring items) have a sum exactly equal to k. Numbers can be negative or zero. Use prefix sums with a dict: O(n) time. Example: subarray_sum_count([1, 2, 3], 3) returns 2 ([1, 2] and [3]).

Task 4 · Code · Javascript

Write charFrequency(s) in JavaScript. Count every character of s with a Map, and return an array of [char, count] pairs sorted by count from high to low; for equal counts, put the smaller character first (compare with < and >, which uses character codes, so "A" comes before "a" and " " before letters). Example: charFrequency("banana") returns [["a", 3], ["n", 2], ["b", 1]]. An empty string gives [].

Quick quiz

Question 1

1.What does Counter("banana")["z"] return?

Question 2

2.Which key puts exactly the anagrams of "listen" into one group AND can be used as a dict key?

Question 3

3.For nums = [2, -2, 2] and k = 0, how many subarrays sum to k?

Question 4

4.In the subarray-sum pattern, why look up running − k BEFORE storing running?

Question 5

5.Top-k words with the rule "higher count first, then alphabetical". Which sort key?

Question 6

6.How much extra space does "first unique character" need for a string of lowercase English letters?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does Counter("banana")["z"] return?

Answer: 0

Counter treats a missing key as a count of 0 instead of raising KeyError.

Which key puts exactly the anagrams of "listen" into one group AND can be used as a dict key?

Answer: "".join(sorted("listen"))

The sorted string is the same for all anagrams and is hashable. A set loses counts and is unhashable, a list is unhashable, and length groups unrelated words.

For nums = [2, -2, 2] and k = 0, how many subarrays sum to k?

Answer: 2

[2, −2] and [−2, 2] both sum to 0; [2, −2, 2] sums to 2.

In the subarray-sum pattern, why look up running − k BEFORE storing running?

Answer: So a prefix is never matched with itself, which would count an empty subarray when k = 0

If running were stored first, running − 0 would find the prefix just stored.

Top-k words with the rule "higher count first, then alphabetical". Which sort key?

Answer: key=lambda w: (-counts[w], w)

The minus sign sorts counts from high to low; the word breaks ties alphabetically.

How much extra space does "first unique character" need for a string of lowercase English letters?

Answer: O(1), at most 26 counts

The count map has at most one entry per letter, and there are only 26 letters.

Why does the subarray-sum-equals-k pattern start with seen = {0: 1}?Related

Answer: So subarrays that start at index 0 are counted

A subarray from index 0 to r sums to k when the running total equals k, i.e. running − k = 0, the empty prefix.

From Prefix sums and difference arrays

Which pattern fits "find two numbers that add up to a target" in unsorted data?Related

Answer: Hash map

Store what you have seen and look up the complement.

From Data-structure questions in JavaScript

Why must the numbers be positive for the shortest-subarray-with-sum-at-least-target window?Related

Answer: So removing an item from the left always lowers the sum, which makes "shrink while valid" correct

With negatives, removing an item can raise the sum, so the window can stop shrinking too early or miss the answer.

From Sliding window: variable size

The window [4, 2, 7] has sum 13. The next item is 1 and k = 3. What is the next window sum?Related

Answer: 10

13 − 4 (leaves) + 1 (enters) = 10, the sum of [2, 7, 1].

From Sliding window: fixed size

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Hashing, two pointers and sliding window” before its exam: Two pointers, Sliding window: fixed size, Sliding window: variable size.
  2. Module 3 exam: Hashing, two pointers and sliding window · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Stacks, queues and linked lists. 61 topics left in the course before the final exam.

← Previous: Hash maps and hash sets