Algorithms with C# · Lesson 3

Frequency maps

Count repeated keys in one pass.

Count occurrences under a chosen equality rule

A frequency map associates each key with the number of times it appears. The sample receives already separated word tokens, not an arbitrary paragraph requiring a tokenizer. It uses StringComparer.Ordinal to define which strings are the same key. Token identity and occurrence count are separate parts of the task: punctuation, casing, and whitespace remain meaningful unless an earlier stage intentionally transforms them. A dictionary does not automatically know which differently written words a particular application wants to combine.

Account for three tokens and two keys

The input visits rune, code, and rune in order. The first rune lookup misses, giving an initial integer count of zero that is incremented to one. Code is also new and receives one. The final rune lookup finds one and stores two. Sorting the final pairs by key with ordinal comparison prints code:1 and then rune:2. The map has two distinct keys, while the sum of its counts is three, matching the number of accepted input occurrences.

counts.TryGetValue(word, out var count);

A missing lookup establishes the initial counter

TryGetValue returns a Boolean indicating whether the key existed and assigns the out value. For an int value in this sample, a missing lookup supplies zero, which works as the starting count. The next assignment uses the indexer to add a new key or replace its existing count. This compact pattern avoids reading a nonexistent key through an indexer that would throw. Its correctness still depends on the chosen default making sense for the accumulator; another summary type may need a different initial state.

Dictionary iteration is not a sorting contract

The first encountered key is rune, but the report prints code first because the program explicitly sorts keys before output. Do not rely on a dictionary’s observed enumeration arrangement as a general display-order promise. A second trap is changing comparison rules halfway through the pipeline: case-insensitive token cleanup followed by case-sensitive lookup, or the reverse, can produce confusing groups. Establish identity at the collection boundary and choose display order separately so both decisions remain visible.

Practice: vary case and punctuation separately

Add Rune with a capital R and predict a separate key under ordinal comparison. Then test a deliberate ordinal-ignore-case dictionary and explain how its count grouping changes. In a separate input, add rune followed by a comma and decide whether it should remain a distinct token or be normalized by an earlier tokenizer. Keep those experiments independent: a case comparer does not strip punctuation, and a punctuation policy does not automatically establish the intended case comparison.

  • Verify that total frequency equals the number of accepted tokens after any explicit preprocessing.
  • Write expected keys as well as counts; a correct grand total can still conceal incorrect grouping.

Practice: derive a most-frequent report

For an independent report change, add another code token so both existing keys have count two. Order by descending count and then by ordinal key, predicting code before rune in the tie. Next filter the final map to counts at least two and explain why this changes the report without changing the underlying occurrence map. These variations distinguish building facts from selecting a presentation, and they avoid corrupting the stored counts merely to obtain a convenient sorted output.

  • State the tie breaker explicitly when several tokens share the maximum frequency.
  • If counts can become very large, choose an accumulator range and overflow policy appropriate to the actual input bounds.

Preserve the relationship between observations and summary

A trustworthy frequency calculation defines tokenization, equality, counting, and presentation as distinct decisions. The baseline’s arithmetic is small enough to verify by hand, but its invariants scale: every accepted token increments exactly one key, and no output sort changes the stored count. The managed dictionary and ordinal comparer fit this browser example without linguistic services. If the application later needs language-aware word segmentation or culture-specific comparison, treat that as a separately supported requirement rather than assuming this identifier-like token policy already provides it.

  • Baseline prints code:1 followed by rune:2.
  • There are two keys but three occurrences; those counts answer different questions.
  • The explicit final sort, not the dictionary itself, establishes the displayed key order.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var counts = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var word in new[] { "rune", "code", "rune" })
{
    counts.TryGetValue(word, out var count);
    counts[word] = count + 1;
}
foreach (var pair in counts.OrderBy(x => x.Key, StringComparer.Ordinal)) Console.WriteLine($"{pair.Key}:{pair.Value}");

Expected output

code:1
rune:2
Try this
Dictionary lookup and report ordering solve different problems.

Further reading