AlgoMaster Logo

Design a Parallel Word Counter

medium9 min readUpdated August 31, 2026
Listen to this chapter
Unlock Audio

Design a reusable word counter that processes independent documents concurrently without losing updates or creating too many threads.

Implement the ParallelWordCounter class:

  • ParallelWordCounter(maxThreads) creates a counter that may use at most maxThreads participating threads, including the thread that calls count.
  • count(documents, onDocument) returns the total frequency of every word across all documents.

Each document contains lowercase English words separated by one or more ASCII spaces. Ignore leading, trailing, and repeated spaces. Empty documents contain no words.

Call onDocument(index) exactly once immediately before processing documents[index]. Every valid index from 0 through documents.length - 1 must be reported once. Callbacks may overlap when maxThreads > 1, but no more than maxThreads callbacks may be active simultaneously.

The callback is an observation hook used by the judge to verify exactly-once processing, real parallel progress, and the worker limit. It returns normally and does not access or modify documents.

Do not modify or reorder the input collection. count must return only after every document and callback has completed. The same counter may be reused for multiple completed calls, and different counter instances must be independent. The judge does not call count concurrently on the same instance.

The judge supplies the documents and callbacks. Standard concurrency, callback, collection, lock, and thread APIs are preloaded, so you do not need import, include, package, or using statements.

Example 1:

Input:

Output:

Explanation: All three document indices are reported once. The empty document contributes no words.

Example 2:

Input:

Output:

Explanation: Extra spaces are ignored, and counts from every worker are combined after processing completes.

Constraints

  • 1 <= maxThreads <= 8
  • 0 <= documents.length <= 1000
  • 0 <= documents[i].length <= 2000
  • Each word contains only lowercase English letters.
  • Words are separated only by ASCII space characters.
  • The total number of characters across one call is at most 100000.
  • onDocument returns normally and does not access documents.
  • The judge does not call count concurrently on the same counter instance.

Starter Code

Understanding the Problem

Documents are independent during the counting phase, so several workers can tokenize and count them at the same time. The main hazard is the shared result: unsynchronized read-modify-write operations on one global map can overwrite each other and lose counts.

A coarse lock around every word update would be correct, but it would force workers to contend on nearly all useful work. A better MapReduce-style design gives each participating thread its own local frequency map. Local maps require no synchronization because each belongs to exactly one thread.

After all workers finish, the calling thread reduces those local maps into the final result.

Bounded Work Distribution

For a nonempty input, use min(maxThreads, documents.length) participating threads. The calling thread is one participant, so create at most one fewer additional workers.

Maintain an atomic next-document index. Each participant repeatedly:

  1. Claims the next index atomically.
  2. Stops if the index is outside the input.
  3. Invokes onDocument(index) exactly once.
  4. Tokenizes that document and updates only its own local map.

Atomic claiming guarantees that no index is skipped or processed twice. Dynamic claiming also balances uneven document sizes better than permanently assigning the same number of documents to every worker.

Local Counting and Reduction

Each worker increments counts in a private map. No lock is needed during tokenization because no other thread accesses that map until the worker finishes.

The caller processes documents too, then waits for every additional worker. Only after all joins complete does it merge the local maps into one result. This ordering provides three guarantees:

  • all callback and counting work is finished before count returns;
  • local maps are no longer being modified during reduction;
  • the returned totals include every document exactly once.

The result is created inside each call, so completed calls and different counter instances cannot leak counts into one another.

Correctness

Every document index is issued by one atomic increment. Atomic increments produce distinct values, so at most one participant receives an index. Participants continue until all valid indices have been issued, so every document is processed exactly once.

For each document, every nonempty space-delimited word increments the corresponding local count once. Reducing all local maps adds those disjoint contributions, so the final count for each word equals its total number of occurrences across the input.

There are at most maxThreads - 1 additional workers plus the caller, so the participation limit is respected. Joining every worker before reduction prevents early return and makes local-map access race-free.

Code

Complexity Analysis

Let c be the total number of characters and u the number of distinct words.

  • Time: O(c) total counting work, plus the cost of merging local maps.
  • Space: O(u * min(maxThreads, documents.length)) in the worst case for thread-local maps.
  • Callbacks: exactly documents.length invocations.
  • Participating threads: at most min(maxThreads, documents.length).