Learn/DSA Patterns
DSA PatternsHashingeasy11 min read

Frequency Counting

A hash map that counts occurrences transforms 'who appears most?' or 'which character is missing?' from an O(n²) scan into a single O(n) pass. Learn to tally, query, and combine frequency maps across classic interview problems.

#hashmap#frequency#counting#top-k#anagram#beginner#interview
Table of contents

Before we start

By the end of this page you will be able to:

  • See a frequency map as a tally sheet — built in one pass, queried instantly.
  • Explain out loud how to find the top-K frequent elements without sorting the full array.
  • Recognise the frequency-counting skeleton in anagram checking, character problems, and majority element detection.

Stop at every Pause & Think box.


Picture this first (no code yet)

A real-life story

It's election night at a small village. Ballots arrive one by one. A clerk at a table has a sheet of paper with every candidate's name and a tally counter next to each name. As each ballot arrives, she finds the candidate's row and adds one tick mark. She doesn't sort anything. She doesn't scan the pile twice.

When the last ballot is counted, she glances at the tally sheet: whoever has the most ticks wins. She announces the winner in seconds — not because the pile is sorted, but because she kept a running count the whole time.

That tally sheet is a frequency map. The clerk's one-pass counting is the algorithm.

The insight: counting is free during a single scan. You never need to "look back" to know how many times something appeared — the map remembers for you.


The actual problem

The core operation is simple:

Given a list of items, build a map from each item to the number of times it appears.

from collections import Counter
freq = Counter([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])
# freq = {3:2, 1:2, 4:1, 5:3, 9:1, 2:1, 6:1}

That one line solves a dozen interview problems. The hard part isn't building the map — it's knowing what to do with it after. This chapter covers the most important use-cases.


First, the slow way (so you feel the pain)

Problem: find the most frequent element in [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].

Naïve approach: for each element, count its occurrences by scanning the whole array.

max_count = 0
result = -1
for x in arr:
    count = arr.count(x)     # O(n) scan for every element
    if count > max_count:
        max_count = count
        result = x

arr.count(x) is O(n). Called n times → O(n²) total. For n=100,000: 10 billion operations.

One-pass frequency count: O(n). Same result, no repeated scanning.


The turning point

The key realisation: counting and querying are separate operations. Do the counting in one pass up front. Then answer any number of queries for free.

Pause & think

You have built freq = {3:2, 1:2, 5:3, ...} in one pass. Now you are asked:

  1. "Does 7 appear?"
  2. "How many times does 5 appear?"
  3. "What is the most frequent element?"

How fast can you answer each, and what data structure would make question 3 fastest?

  1. 7 in freq → O(1). Answer: No.
  2. freq[5] → O(1). Answer: 3.
  3. max(freq, key=freq.get) → O(k) where k is distinct elements. Or use a heap for top-K.

The frequency map makes all three instant after the single O(n) counting pass.


The one idea to remember

The entire pattern in one sentence

Build a {element → count} map in one pass, then query it for frequency-based decisions — existence, maximum, minimum, threshold filtering, or top-K — all in O(1) or O(k) per query.


Watch it happen, frame by frame

Problem: find the first non-repeating character in "aabbcde".

Build the frequency map in one pass:

Process 'a': freq={'a':1}
Process 'a': freq={'a':2}
Process 'b': freq={'a':2,'b':1}
Process 'b': freq={'a':2,'b':2}
Process 'c': freq={'a':2,'b':2,'c':1}
Process 'd': freq={'a':2,'b':2,'c':1,'d':1}
Process 'e': freq={'a':2,'b':2,'c':1,'d':1,'e':1}

Second pass — scan left to right, return first with count 1:

's'='a': freq['a']=2 → skip
's'='a': skip
's'='b': freq['b']=2 → skip
's'='b': skip
's'='c': freq['c']=1 → RETURN 'c'

Pause & think

Cover the answer below. What is the first non-repeating character in "leetcode"?

Build the frequency map in your head, then scan left to right for count=1.

Check your answer
freq = {'l':1, 'e':3, 't':1, 'c':1, 'o':1, 'd':1}
Scan: 'l' → count 1RETURN 'l'

'l' is the first non-repeating character.


Now, the code — line by line

Core pattern — build and query:

from collections import Counter, defaultdict

def firstUniqChar(s):
    freq = Counter(s)              # one-pass count: {'l':1, 'e':3, ...}

    for i, ch in enumerate(s):    # second pass — scan in original order
        if freq[ch] == 1:
            return i               # first index with count exactly 1

    return -1                      # all characters repeat

Top K frequent elements (LC #347):

def topKFrequent(nums, k):
    freq = Counter(nums)           # build frequency map

    # Option 1: sort by frequency — O(n log n)
    return sorted(freq, key=freq.get, reverse=True)[:k]

But we can do better with a bucket sort idea — O(n):

def topKFrequent_linear(nums, k):
    freq = Counter(nums)

    # Bucket: index = frequency, value = list of numbers with that frequency
    buckets = [[] for _ in range(len(nums) + 1)]
    for num, count in freq.items():
        buckets[count].append(num)

    # Collect from highest-frequency bucket downward
    result = []
    for count in range(len(buckets) - 1, 0, -1):
        result.extend(buckets[count])
        if len(result) >= k:
            return result[:k]
    return result

Why O(n): frequencies are bounded by n, so the bucket array has at most n+1 slots — filling it is O(n), reading it top-down is O(n).


Why is building the map O(n)?

Each element is visited exactly once. For each visit, a hash map insert/update takes O(1) amortised. Total: n × O(1) = O(n).

Reading back is O(k) for top-K queries, O(1) for single lookups, O(distinct elements) for iterating all entries.

ApproachBuildQuery (single)Top-K
Scan-for-eachO(n)O(n²)
Sort firstO(n log n)O(log n)O(k)
Frequency mapO(n)O(1)O(n log k) with heap
Freq map + bucket sortO(n)O(1)O(n)

When should I reach for this? (the trigger list)

Reach for a frequency map when:

  • Asked about how many times something appears.
  • Asked for the most/least frequent element, or top K frequent.
  • Checking if two things are anagrams (same characters, same counts).
  • Checking if a string/array is a permutation of another.
  • Counting elements to detect majority (appears > n/2 times) before Moore's Voting.
  • The brute-force answer involves a nested scan: "for each element, count how many times it appears."

The same trick in five disguises

Disguise 1 — Valid Anagram (LC #242)

Two strings are anagrams if they have the same character frequencies.

def isAnagram(s, t):
    return Counter(s) == Counter(t)
    # Or: Counter(s) - Counter(t) == Counter()  (same thing)

Alternatively, subtract counts and check all reach zero:

def isAnagram(s, t):
    if len(s) != len(t): return False
    freq = Counter(s)
    for ch in t:
        freq[ch] -= 1
        if freq[ch] < 0: return False   # t has more of this char than s
    return True

Disguise 2 — Majority Element (LC #169)

Find element appearing > n/2 times. Either use a frequency map and check, or use Moore's Voting (pattern 1.09) for O(1) space.

def majorityElement(nums):
    freq = Counter(nums)
    return max(freq, key=freq.get)    # works because majority always exists

Disguise 3 — Check if Array is Permutation of 1..N

def isPermutation(nums):
    n = len(nums)
    return Counter(nums) == Counter(range(1, n + 1))
    # Or: set(nums) == set(range(1,n+1)) and len(nums)==n  (if all distinct)

Disguise 4 — Ransom Note (LC #383)

Can you build the note from the magazine? Check if every character's count in the note ≤ its count in the magazine.

def canConstruct(ransomNote, magazine):
    mag = Counter(magazine)
    for ch in ransomNote:
        mag[ch] -= 1
        if mag[ch] < 0:
            return False    # magazine ran out of this character
    return True

Disguise 5 — Find All Anagrams in a String (LC #438)

Sliding window of fixed size k = len(p). Maintain a frequency count of the current window; compare with the target frequency count.

def findAnagrams(s, p):
    target = Counter(p)
    window = Counter(s[:len(p)])
    result = []
    if window == target: result.append(0)

    for i in range(len(p), len(s)):
        window[s[i]] += 1                      # add new character
        old = s[i - len(p)]
        window[old] -= 1
        if window[old] == 0: del window[old]   # remove zeroed-out entry
        if window == target: result.append(i - len(p) + 1)

    return result
Level up — Top K with a min-heap (O(n log k), better than sorting all)

When n is huge but k is small, a min-heap of size k beats sorting:

import heapq

def topKFrequent_heap(nums, k):
    freq = Counter(nums)
    # heap stores (count, num); min-heap keeps the k largest counts
    return [num for count, num in heapq.nlargest(k, freq.items(), key=lambda x: x[0])]

heapq.nlargest is O(n log k) — much better than O(n log n) sort when k ≪ n.


Traps that catch beginners

Watch out for these

  • Querying a key that doesn't exist. freq[x] on a plain dict raises KeyError if x was never seen. Use freq.get(x, 0) or defaultdict(int) or Counter (which returns 0 for missing keys automatically).
  • Comparing Counters with == when order matters. Counter({'a':1,'b':2}) == Counter({'b':2,'a':1}) is True — Counter ignores order. Good for anagram checks; don't use if order is part of your condition.
  • Not deleting zero-count entries in a sliding-window comparison. When subtracting from a window Counter and comparing with a target Counter, leaving {'a': 0} in the window causes it to mismatch {} (empty). Always del window[ch] when the count hits zero.
  • Using a list to "count" (counting by index). count = [0] * 26 for lowercase letters is perfectly valid and O(1) space — sometimes better than a dict for fixed alphabets. Recognise when a fixed array beats a hash map.
BugFix
KeyError on missing keyUse Counter, defaultdict(int), or dict.get(key, 0)
Sliding window mismatchDelete zero-count entries: if window[ch]==0: del window[ch]
O(n log n) for top-KUse bucket sort (O(n)) or heap (O(n log k))

Say it like a pro (interview one-liner)

"I'll build a frequency map in one O(n) pass. Each element is counted in O(1). Then I query the map — existence checks, maximum, or top-K — in O(1) per query or O(n) total for top-K with bucket sort. The key insight is that counting is free during the scan; you never need to revisit elements."


Remember this forever

Frequency Counting

One pass: freq = Counter(iterable). Then query: freq[x] (count), max(freq, key=freq.get) (most frequent), [x for x in freq if freq[x]==1] (unique elements).


Trigger: how many times · most/least frequent · anagram check · permutation check · sliding window character match

Top-K options: Counter.most_common(k) · heapq.nlargest(k, freq.items()) · bucket sort (O(n))

Cost: O(n) build · O(1) per query · O(n) for top-K with bucket sort

Pitfall: delete zero-count entries in sliding window; use Counter/defaultdict to avoid KeyError


Check yourself

Why does Counter return 0 for a missing key instead of raising KeyError?

Counter subclasses dict and overrides __missing__ to return 0 instead of raising. This makes it safe to do freq['z'] -= 1 without checking existence first — missing keys default to 0 and go negative. A plain dict would raise KeyError.

Why do we delete zero-count entries when comparing two Counters in a sliding window?

Counter({'a': 0}) is NOT equal to Counter() in Python — the key 'a' still exists with value 0, making the comparison fail. When a character leaves the window and its count hits 0, we must delete it from the window Counter so the comparison window == target works correctly.

Top K frequent elements: when would you use bucket sort vs. a heap?

Use bucket sort when you want O(n) time — frequencies are bounded by n, so the bucket array is O(n). Use a min-heap of size k when n is very large but k is small — O(n log k) time but O(k) extra space, which is better when k ≪ n. Use Counter.most_common(k) for quick code — it internally uses a heap.


Practice problems

ProblemDifficultyWhat to noticeLink
First Unique Character in a StringEasyTwo-pass: build freq, scan for count=1LC #387
Valid AnagramEasySame frequency map from both stringsLC #242
Ransom NoteEasySubtract note chars from magazine; check no negativesLC #383
Majority ElementEasyMost frequent element (> n/2); Counter.most_common(1)LC #169
Top K Frequent ElementsMediumBuild freq → bucket sort or heap for top KLC #347
Find All Anagrams in a StringMediumSliding window + Counter comparisonLC #438
Sort Characters By FrequencyMediumBuild freq → sort by count descendingLC #451

Next up: Grouping by Key — where the hash map doesn't count, it sorts into bins — and the key is a computed signature, not the element itself.