TechByteByByte

Count Occurrences of Substrings/Words in Text (Log/Data-Relevant) - Java

A difficult QA/automation coding interview question: count Occurrences of Substrings/Words in Text (Log/Data-Relevant), with a full Java walkthrough, dry run, and common interviewer follow-ups.

#Strings#HashMap#Log Analysis#Difficult#Java

Why this is categorized as difficult: It deals with overlapping substring matches and edge-case word boundaries that are highly relevant to QA automation log parsers.


Category: Difficult | Concepts used: Word frequency counting, substring counting — very relevant for QA log analysis!


Problem Statement

Given a block of text (e.g., a log file), count how many times a specific word or substring appears — and, more generally, build a frequency count of all words.

Input : "ERROR: timeout ERROR: connection lost ERROR: timeout", find "ERROR"
Output: 3

Note: This problem is directly relevant to QA/automation work — counting error occurrences in logs, tracking how often specific events appear in test output, etc.

Examples (with edge scenarios)

#TextTargetOutputWhy
1"ERROR: timeout ERROR: connection""ERROR"2Appears twice as a distinct word
2"ERRORERROR" (no separators)"ERROR"Depends — is this 1 “word” or 2 substring occurrences?Clarify: whole-word matching vs. raw substring search
3"" (empty)"ERROR"0Nothing to search
4"error Error ERROR" (case variations)"error"3 (if case-insensitive) or 1 (if case-sensitive)Clarify case sensitivity
5Overlapping substrings, e.g., text="aaa", target="aa"2 (overlapping) or 1 (non-overlapping)?Substring counting has its own overlap ambiguity — clarify!

Common Fresher Mistake

MistakeWhat happensFix
Using simple substring search (String.contains() in a loop) without considering word boundaries"ERROR" would also match inside "ERRORCODE" — may not be intendedClarify: exact whole-word match, or any substring occurrence? Use regex word boundaries (\bword\b) for whole-word matching
Not handling overlapping substring occurrences correctlyMiscounted results for patterns like counting "aa" in "aaa"Decide and clearly state whether overlapping matches should be counted

Before You Code: Clarify the Contract

Before choosing an algorithm, confirm how null and empty strings should behave, whether comparison is case-sensitive, and whether spaces or punctuation count. Java char values are UTF-16 code units, not always complete human-visible Unicode characters, so international text may require code points or grapheme-aware libraries.

Analogy: Highlighter Matching

Imagine you are scanning a printed document looking for the word "cat":

  • Solution 1 (Whole Word Match - Scissors & Sorting): You take a pair of scissors and cut out every word individually. If a word has punctuation attached (like "cat!"), you trim the punctuation away so it is just "cat". You then go through the cut-out words and count how many times the card "cat" appears. "category" is ignored because it’s a completely different card.
  • Solution 2 (Substring Search - Highlighter Slide): You use a yellow highlighting tape of exactly 3 characters wide:
    • Non-overlapping: You place the tape over "cat". Once highlighted, you slide the tape completely past it (index += target.length()) to start highlighting from the next character. "category" gets highlighted once because it contains "cat".
    • Overlapping: If you were checking for "aa" in "aaa", after highlighting the first "aa" at indices 0-1, you slide the tape by just one letter (index += 1) to highlight the second "aa" at indices 1-2.

Solution 1 — Count a Specific Word (Whole-Word Match) Using Split + Frequency Map

Intuition

If we want to count a word as a distinct word (not just any substring occurrence), the safest approach is the same one used for “count words in a sentence” (Q15) and “duplicate words” (Q43): split the text into individual words (normalizing case/punctuation), then simply count how many of those words match our target exactly.

import java.util.HashMap;

public class CountSpecificWord {
    public static int countWord(String text, String target) {
        String[] words = text.toLowerCase().replaceAll("[^a-zA-Z0-9\\s]", "").split("\\s+");
        String normalizedTarget = target.toLowerCase();

int count = 0;
        for (String word : words) {
            if (word.equals(normalizedTarget)) {
                count++;
            }
        }
        return count;
    }

public static void main(String[] args) {
        String log = "ERROR: timeout ERROR: connection lost ERROR: timeout";
        System.out.println(countWord(log, "ERROR")); // 3
        System.out.println(countWord(log, "timeout")); // 2
    }
}

Output:

3
2

Dry Run (text = “ERROR: timeout ERROR:”, target = “ERROR”)

Lowercased & punctuation stripped: "error timeout error"
Split: ["error", "timeout", "error"]
normalizedTarget = "error"

word="error" -> matches -> count=1
word="timeout" -> no match
word="error" -> matches -> count=2

Final: 2

Interviewer’s take

This is the expected, correct approach for whole-word counting — reusing the same “normalize, split, count” pattern from earlier word-based problems. Stripping punctuation with regex ([^a-zA-Z0-9\\s]) prevents "ERROR:" and "ERROR" from being treated as different tokens, which is an important, easy-to-miss detail.

Follow-up questions you might get:

  • “What if you want to count occurrences of ALL words, not just one target?” → Build a full frequency map (HashMap<String, Integer>) exactly like Q43’s duplicate-words logic, then look up any word’s count directly, or list all counts.
  • “How does this be useful for QA/log analysis?” → Directly applicable to counting error types in a log file, tracking how often specific test failure messages appear, etc.

Solution 2 — Count Raw Substring Occurrences (Including Partial Matches, Not Just Whole Words)

Intuition

If we instead want to count every place the target text appears as a substring (even inside other words, like "ERROR" inside "ERRORCODE"), we scan through the text looking for the pattern starting at every position, and use Java’s indexOf() repeatedly, advancing our search start point past each found match.

public class CountSubstringOccurrences {
    public static int countSubstring(String text, String target) {
        if (target.isEmpty()) {
            return 0; // avoid infinite loop on empty target
        }

int count = 0;
        int index = 0;

while ((index = text.indexOf(target, index)) != -1) {
            count++;
            index += target.length(); // move past this match (non-overlapping count)
        }
        return count;
    }

public static void main(String[] args) {
        System.out.println(countSubstring("ERRORERRORCODE", "ERROR")); // 2
        System.out.println(countSubstring("aaa", "aa"));                   // 1 (non-overlapping)
    }
}

Output:

2
1

Dry Run (text = “aaa”, target = “aa”, non-overlapping)

index=0
text.indexOf("aa", 0) = 0 -> count=1, index=0+2=2
text.indexOf("aa", 2) = -1 (not enough characters left starting at index 2) -> loop ends

Final: 1  (non-overlapping count — the second possible "aa" at index 1 is skipped since it overlaps the first match)

Interviewer’s take

This is the correct approach for substring (not whole-word) counting — and it’s important to explicitly clarify (and mention in your answer) whether you’re counting overlapping or non-overlapping occurrences, since text.indexOf(target, index) with index += target.length() specifically produces non-overlapping counts. If overlapping matches should be counted instead, you’d only advance index by 1 each time instead of by the full target length.

Follow-up questions you might get:

  • “How would you count OVERLAPPING occurrences instead?” → Change index += target.length() to index += 1 — this re-checks starting from just one position later, allowing overlapping matches to be found.
  • “Which version is more relevant for log analysis — whole-word or substring?” → Depends on context: searching for a specific error code or exact word usually wants whole-word matching (Solution 1); searching for a general pattern that might appear as part of larger tokens might want substring matching (Solution 2).

📊 Visual Flowchart

graph TD
    Start["Input Text, Target T"] --> Decide{"Match Type?"}
    Decide -->|Whole Word Match| Path1["Normalize & strip punctuation<br>Split text by whitespace into words"]
    Path1 --> Loop1{"For each word"}
    Loop1 --> Check1{"word.equals(target)?"}
    Check1 -->|Yes| Inc1["count++"]
    Inc1 --> Loop1
    Check1 -->|No| Loop1
    Loop1 -->|Done| End1["Return count"]
    Decide -->|Raw Substring Search| Path2["Initialize index = 0, count = 0"]
    Path2 --> Loop2{"indexOf(T, index) != -1?"}
    Loop2 -->|Yes| Inc2["count++"]
    Inc2 --> DecideOverlap{"Allow Overlaps?"}
    DecideOverlap -->|Yes| Slide1["index = index + 1"]
    DecideOverlap -->|No| Slide2["index = index + T.length"]
    Slide1 --> Loop2
    Slide2 --> Loop2
    Loop2 -->|No| End2["Return count"]

Final Verdict — Which Solution Should You Give?

  • Clarifying whole-word vs. substring matching (and overlap behavior, if substring) is the most important step — both solutions are simple and correct once the exact requirement is understood.

Quick Recap

ApproachMatchesTimeSpace
Split + compareWhole words onlyO(n)O(n) for word array
indexOf() loopAny substring (overlap configurable)O(n·m) worst case (m = target length)O(1)
Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed