Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Check whether the fixed word "hackerrank" appears as a subsequence (not substring) of the input. Walk one pointer through the input and a second pointer through "hackerrank"; whenever the current input character matches the current target character, advance the target pointer. If the target pointer reaches the end, every letter was matched in order → "YES", otherwise "NO". O(n) time, O(1) space.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you recognize subsequence (order-preserving, gaps allowed) versus substring (contiguous), and implement the greedy two-pointer match correctly including the double 'r'.
Where candidates slip
Treating it as a substring/contains check, or advancing the pattern pointer even when characters don't match, or testing the wrong success condition (end of s instead of end of pattern).
How to stand out
Say why greedy is correct: taking the earliest match for each pattern letter is never worse than waiting, because it preserves the maximum remaining suffix of s for the rest of the pattern.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewj = 0 (into 'hackerrank')c == pat[j]?YesNoExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Recursive Subsequence Check (Baseline)
1Hint 1Explore hintHide hint
Define match(i, j): can pat[j:] be a subsequence of s[i:]?
2Hint 2Explore hintHide hint
If pat[j] == s[i], you may consume both; you may also always skip s[i]. Recurse.
3Hint 3Explore hintHide hint
Base cases: j past the pattern → true; i past s with j remaining → false.
A · Core idea
Express the definition directly: pat is a subsequence of s starting at offsets i, j if either the pattern is exhausted (j == len(pat) → true), or s is exhausted first (false), or we match s[i] against pat[j] when equal and otherwise skip s[i].
Why it's correct. It mirrors the formal recursive definition of 'subsequence', so it cannot be wrong.
Why we don't ship it. Recursion adds call-stack overhead and risks deep frames for long inputs, all to express something a single for loop captures cleanly. It's useful as a conceptual bridge — note that whenever s[i] == pat[j] you can safely consume the match (no need to also explore skipping it), which is exactly the greedy insight the iterative version exploits.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s = "hackerworld"Array state
match hacker
h a c k e r consumed
NOC · Code with teaching comments
Reference implementation previewprivate static final String PAT = "hackerrank"; public String hackerrankInString(String s) { return match(s, 0, 0) ? "YES" : "NO"; }Explore the full codeHide the full code
D · Why it works and complexityO(n) timeO(n) recursion spaceOpen the invariant and proofExplore the proofHide the proof
The recursion has a special property: when characters match, you never need to explore not taking the match. That collapses the branching recursion into a deterministic single forward scan — the greedy two-pointer.
ComplexityO(n) time, O(n) recursion space — Because the pattern is matched greedily even in recursion (once a needed character is found, taking it is safe), the recursion depth is O(n) and each character is visited a constant number of times → O(n) time, O(n) stack space.
E · Edge cases and pitfalls5s = "hackerrank" — Exact match → YES.Explore edge casesHide edge cases
Edge cases handled
s = "hackerrank"Exact match → YES.s = ""Input exhausted immediately → NO.
Pitfalls
- Deep recursion stack for long inputs.
- Exploring the skip branch even on a match — unnecessary work.
- Wrong base-case order (check pattern exhaustion before input exhaustion).
Approach 2 of 2 · Optimal
Greedy Two-Pointer Scan (Optimal)
1Hint 1Explore hintHide hint
Keep an index j into 'hackerrank'. What do you do with j as you read each character of s?
2Hint 2Explore hintHide hint
If the current character equals pat[j], advance j; otherwise leave j alone and move on.
3Hint 3Explore hintHide hint
After scanning s, the answer is YES exactly when j reached the length of the pattern.
A · Core idea
Hold a pointer j into the target word "hackerrank". Scan s left to right; whenever the current character equals pat[j], advance j. After the scan, return "YES" iff j reached the end of the pattern.
Why greedy (earliest match) is optimal. Suppose pat[j] first occurs at position p in the remaining input. Matching it at p rather than some later position p' > p can only help: it leaves the longer suffix s[p+1:] available for the rest of the pattern, which is a superset of s[p'+1:]. So if any valid matching exists, the greedy earliest-match matching also exists. There's never a reason to pass up the first opportunity.
Why advance only on a match. j represents 'how much of the pattern is already placed'. Bumping it without a real match would claim progress that didn't happen, accepting strings that aren't actually supersequences.
Why the success test is `j == len(pat)`. Reaching the end of s proves nothing; what matters is whether all ten pattern letters got placed. The double r falls out naturally — after the first r matches, j points at the second r and waits for another one.
Why O(1) space. Just two indices; no recursion, no auxiliary structures.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s = "hhaacckkekraraannk"Array state
early
match h,a,c,k,e → j=5
YESC · Code with teaching comments
D · Why it works and complexityO(n) timeO(1) spaceOpen the invariant and proofExplore the proofHide the proof
Subsequence existence is the canonical greedy-pointer problem: a single forward sweep with a pattern cursor that only moves on a hit. The 'earliest match is safe' argument is the same exchange argument behind many greedy proofs.
ComplexityO(n) time, O(1) space — A single sweep over s with O(1) work per character → O(n) time. Only two integer pointers are kept → O(1) space.
E · Edge cases and pitfalls6s = "hackerrank" — Exact match, j reaches 10 → YES.Explore edge casesHide edge cases
Edge cases handled
s = "hackerrank"Exact match, j reaches 10 → YES.s = "hereiamstackerrank"Letters appear in order with gaps → j reaches 10 → YES.s = "rhbaasdndfsdskgbfefdbrdfintwrwwrgikjwtrenaqyznnative"Pattern can't be threaded in order → NO.
Pitfalls
- Using contains/indexOf — that tests substring, not subsequence.
- Advancing j on non-matches, accepting strings that aren't supersequences.
- Returning based on i reaching the end of s instead of j reaching the pattern length.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic String hackerrankInString(String s) { String pat = "hackerrank"; int j = 0; for (int i = 0; i < s.length() && j < pat.length(); i++) { if (s.charAt(i) == pat.charAt(j)) {Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Substring instead of subsequence
'hackerrank' rarely appears contiguously; the problem allows gaps. Using contains() returns NO for valid inputs like 'hhaacckk…'.
Explore the code comparisonHide the code comparison
Substring instead of subsequence
'hackerrank' rarely appears contiguously; the problem allows gaps. Using contains() returns NO for valid inputs like 'hhaacckk…'.
Don't write this
return s.contains("hackerrank") ? "YES" : "NO";Write this instead
int j = 0;
for (int i = 0; i < s.length() && j < 10; i++)
if (s.charAt(i) == "hackerrank".charAt(j)) j++;
return j == 10 ? "YES" : "NO";2Advancing the pattern pointer on mismatch
j tracks confirmed progress through the pattern. Bumping it without a match fabricates matches and accepts strings missing required letters.
Explore the code comparisonHide the code comparison
Advancing the pattern pointer on mismatch
j tracks confirmed progress through the pattern. Bumping it without a match fabricates matches and accepts strings missing required letters.
Don't write this
for c in s:
if c == pat[j]:
j += 1
else:
j += 1 # WRONGWrite this instead
for c in s:
if j < len(pat) and c == pat[j]:
j += 13Wrong success condition
The answer depends on whether the pattern was fully matched, not on consuming all of s. Test j == len(pattern).
Explore the code comparisonHide the code comparison
Wrong success condition
The answer depends on whether the pattern was fully matched, not on consuming all of s. Test j == len(pattern).
Don't write this
return "YES" if i == len(s) else "NO"Write this instead
return "YES" if j == len(pat) else "NO"Revise
Carry the pattern into the interview
Pattern to remember
Greedy subsequence matching with two pointers
Code pattern to memorize
j = 0; pat = "hackerrank"
for c in s:
if j < len(pat) and c == pat[j]: j++
return "YES" if j == len(pat) else "NO"Rules to remember
- 1It's a subsequence check — characters must appear in order but not contiguously.
- 2Advance the pattern pointer only on a match; never skip a needed letter.
- 3Greedy (take the earliest match) is provably optimal for subsequence existence.
- 4Success condition is
j == len(pattern)after the scan, not reaching the end ofs. - 5Note
hackerrankhas a doubler— both must be matched in sequence.
Use this pattern when
- Test whether a fixed pattern is a subsequence of a larger string
- Order matters but gaps are allowed between matched characters
- Single pass, O(1) extra space is desired
- Matching is greedy — earliest valid character is always safe to take
Choose another pattern when
- Pattern must be contiguous → substring search (KMP / indexOf), not subsequence
- Count the number of distinct subsequence matches → DP, not a single greedy pass
- Find the shortest window containing the pattern → sliding window
- Pattern is not fixed and very long → still O(n·m) greedy, fine, but think about it
Interview takeaway
Subsequence existence is a one-pass greedy: keep a pointer into the pattern and bump it on each match. Taking the earliest possible match never hurts — it leaves the most of the input for the remaining pattern letters. Reaching the pattern's end means success.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternGreedy subsequence matching. To test whether a fixed pattern is a subsequence, keep a cursor into the pattern and advance it only on matches during a single pass. The earliest-match-is-safe exchange argument makes greedy optimal — the same idea powers Is Subsequence and Number of Matching Subsequences.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Is SubsequenceExact same greedy two-pointer, but the pattern is an arbitrary input string.
- Number of Matching SubsequencesComing soonMany patterns against one s — bucket waiting patterns by their next-needed char.
- Distinct SubsequencesComing soonCount how many ways, not just existence → DP over both strings.
- Longest Word in Dictionary through DeletingComing soonSubsequence check applied to each dictionary word; keep the best.