Quick Overview

This question evaluates skills in frequency counting, top-k and bottom-k selection, streaming algorithms, and data-structure design for maintaining order, tie-breaking, and dynamic updates.

Find top/bottom-k words in list or stream

Company: Google

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Technical Screen

You are given words (strings) either as a finite list or as an unbounded stream. 1) **List version**: Given an array `words` and an integer `k`, return: - the `k` most frequent distinct words, and - the `k` least frequent distinct words. Specify how you break ties (e.g., lexicographically, or any order). 2) **Stream version (online)**: Design a data structure that supports: - `ingest(word)`: process the next word in the stream - `topK(k)`: return the `k` most frequent distinct words seen so far - `bottomK(k)`: return the `k` least frequent distinct words seen so far Discuss time/space complexity and how you would implement these operations efficiently. 3) **Variant (discuss/design)**: Extend the stream design to support returning the **first `k`** and/or **last `k`** words that are currently **non-repeating** (i.e., words with frequency exactly 1) according to stream arrival order. ### Precise Practice Contracts The original design above permits discussing tie policies. For these practice variants, use the existing deterministic policy: higher frequency first for `topK`, lower frequency first for `bottomK`, and lexicographically smaller words first when frequencies tie in either result. Return distinct words, not one entry per occurrence. Do not reverse the tie order when switching from top to bottom. **Finite list.** Implement `solution(words, k)` and return `[most_frequent, least_frequent]`. The bounds are `0 <= len(words) <= 200000` and `0 <= k <= 200000`; each word is a nonempty string. Comparison is case-sensitive. If `k` exceeds the number of distinct words, each result contains all distinct words in its own ranking order. An empty input or `k = 0` produces `[[], []]`. For `words = ["pear", "ant", "pear", "bee", "ant", "cat"]` and `k = 2`, return `[["ant", "pear"], ["bee", "cat"]]`. **Online ranking.** Support `ingest(word)`, `topK(k)` and `bottomK(k)` on the prefix received so far. A bounded operation-list form contains between 1 and 200000 operations. Each is a two-string list: `['ingest', word]`, `['topK', str(k)]` or `['bottomK', str(k)]`, with `0 <= k <= 200000`. Return one word list for each query, in query order; ingestion produces no result entry. Queries before any ingestion return `[]`. Oversized `k` returns all words seen so far, using the requested ranking. The original unbounded-stream design has no fixed prefix length; analyze its storage over a finite prefix. No lowercase or ten-character limit is imposed on the original stream design. **Current non-repeating words.** Extend the same stream behavior with `firstK(k)` and `lastK(k)`. A word qualifies only while its total count in the entire received prefix is exactly one. A second occurrence removes it from consideration; it cannot become non-repeating later in an insertion-only stream. Order qualifying words by their occurrence's arrival time, not lexically. `lastK` selects the last `k` qualifying words but returns those selected words in forward arrival order, not reversed. If fewer than `k` qualify, return all of them. A bounded operation list uses `['ingest', word]`, `['firstK', str(k)]` or `['lastK', str(k)]` with the same operation-count and `k` bounds, returning results only for queries. After ingesting `"a"`, `"b"`, `"c"`, `"b"` in that order: | Query | Result | | --- | --- | | `topK(2)` | `["b", "a"]` | | `bottomK(2)` | `["a", "c"]` | | `firstK(1)` | `["a"]` | | `lastK(2)` | `["a", "c"]` | | `lastK(1)` | `["c"]` | The written Python reference uses Python's case-sensitive string order for lexical comparisons, without case folding or locale-specific normalization. The saved practice interfaces remain unchanged. ### Bounded Top-k Method Implement `Solution.topKFrequent(words, k)` (C++: `vector<string> topKFrequent(vector<string>& words, int k)`) and return only the most frequent word list, ordered by frequency descending and then lexicographically ascending. This method has separate input guarantees: - `1 <= len(words) <= 500`. - Each word has between 1 and 10 characters, all lowercase English letters. - `1 <= k <=` the number of distinct words. For `words = ["i", "love", "study", "i", "love", "coding"]`, `k = 2`, return `["i", "love"]`. Both occur twice, and `"i"` comes first lexically. For `words = ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"]`, `k = 4`, return `["the", "is", "sunny", "day"]`, with frequencies 4, 3, 2 and 1. The method does not return a bottom-k list, and its nonempty, lowercase, bounded-word guarantees do not replace the broader list and stream contracts above. ### Clarifying Questions - Does frequency alone determine the boundary of the selected `k` words? No; lexical order resolves ties at that boundary too. - Is the bottom list the reverse of the top list? Not generally; equal-frequency words still appear in lexical ascending order. - Do singleton queries mean words that were once unique? No; they must have count exactly one now. ### What a Strong Answer Covers - Implements both finite-list outputs, online frequency queries, and current-singleton queries under their respective order rules. - Handles empty prefixes, zero or oversized `k`, frequency ties, and a singleton's second occurrence. - Preserves the bounded method's exact signature and one-list result. - Explains update costs, query costs and output size separately, and accounts for growth in distinct words and exact counts over an unbounded stream. ### Follow-up Questions For the bounded top-k method, can you achieve `O(n log(k + 1))` time and `O(n)` extra space, where `n = len(words)`? This is the usual `O(n log k)` target for `k >= 2`, with a nonzero bound at `k = 1`.

Overview: This question evaluates skills in frequency counting, top-k and bottom-k selection, streaming algorithms, and data-structure design for maintaining order, tie-breaking, and dynamic updates.

Part 1: Top and Bottom K Frequent Words in a List

Given an array of strings words and a non-negative integer k, return two lists: the k most frequent distinct words and the k least frequent distinct words. Use deterministic tie-breaking: if two words have the same frequency, the lexicographically smaller word comes first. If k is larger than the number of distinct words, return all distinct words.

Constraints

  • 0 <= len(words) <= 2 * 10^5
  • 0 <= k <= 2 * 10^5
  • Each word is a non-empty string
  • Comparison is case-sensitive unless otherwise stated

Examples

Input: (['i', 'love', 'leetcode', 'i', 'love', 'coding'], 2)

Expected Output: [['i', 'love'], ['coding', 'leetcode']]

Explanation: `i` and `love` both appear twice, so lexicographical order gives `['i', 'love']`. `coding` and `leetcode` both appear once, so lexicographical order gives `['coding', 'leetcode']`.

Input: (['apple', 'banana', 'apple', 'cherry', 'banana', 'date'], 3)

Expected Output: [['apple', 'banana', 'cherry'], ['cherry', 'date', 'apple']]

Explanation: Frequencies are: apple=2, banana=2, cherry=1, date=1. Top 3 are ordered by frequency descending, then lexicographically: `apple`, `banana`, `cherry`. Bottom 3 are ordered by frequency ascending, then lexicographically: `cherry`, `date`, `apple`.

Hints

  1. Count how many times each distinct word appears before deciding the answer.
  2. You can sort the distinct words twice: once by (-frequency, word) and once by (frequency, word).

Part 2: Online Top/Bottom K Words from a Stream

You are given a sequence of stream operations. Each operation is a 2-element list of strings: ['ingest', word], ['topK', str(k)], or ['bottomK', str(k)]. Process the operations in order. For each query, return the k most frequent or k least frequent distinct words seen so far. Ties are broken lexicographically ascending. If k is larger than the number of distinct words seen so far, return all distinct words.

Constraints

  • 1 <= len(operations) <= 2 * 10^5
  • Each operation is one of ['ingest', word], ['topK', str(k)], or ['bottomK', str(k)]
  • 0 <= k <= 2 * 10^5
  • Queries may appear before any word is ingested

Examples

Input: [['ingest', 'apple'], ['ingest', 'banana'], ['ingest', 'apple'], ['topK', '2'], ['bottomK', '2']]

Expected Output: [['apple', 'banana'], ['banana', 'apple']]

Explanation: After the ingests, counts are apple=2 and banana=1. topK(2) returns the most frequent first, while bottomK(2) returns the least frequent first.

Input: [['topK', '3'], ['ingest', 'dog'], ['ingest', 'cat'], ['ingest', 'cat'], ['topK', '2'], ['bottomK', '2'], ['topK', '5']]

Expected Output: [[], ['cat', 'dog'], ['dog', 'cat'], ['cat', 'dog']]

Explanation: The first query happens before any words are seen, so it returns []. Later, cat has frequency 2 and dog has frequency 1.

Hints

  1. Keep an exact frequency map for all words seen so far.
  2. A max-heap and a min-heap with lazy deletion let you support online updates without fully resorting all words on every query.

Part 3: First/Last K Non-Repeating Words in a Stream

You are given a sequence of stream operations. Each operation is a 2-element list of strings: ['ingest', word], ['firstK', str(k)], or ['lastK', str(k)]. A word is non-repeating if its frequency is exactly 1 among all words seen so far. For each query, return the first k or last k currently non-repeating words according to stream arrival order. For lastK, return the selected words in arrival order, not reversed.

Constraints

  • 1 <= len(operations) <= 2 * 10^5
  • Each operation is one of ['ingest', word], ['firstK', str(k)], or ['lastK', str(k)]
  • 0 <= k <= 2 * 10^5
  • Queries may appear before any word is ingested

Examples

Input: ([['ingest', 'a'], ['ingest', 'b'], ['ingest', 'a'], ['ingest', 'b'], ['ingest', 'c'], ['ingest', 'd'], ['firstK', '2'], ['ingest', 'c'], ['ingest', 'e'], ['lastK', '2']],)

Expected Output: [['c', 'd'], ['d', 'e']]

Explanation: After the first six ingests, only 'c' and 'd' are non-repeating, so firstK 2 returns ['c', 'd']. After ingesting another 'c' and then 'e', the non-repeating words are ['d', 'e'], so lastK 2 returns them in arrival order.

Input: ([['firstK', '3'], ['ingest', 'x'], ['ingest', 'x'], ['firstK', '1'], ['lastK', '2']],)

Expected Output: [[], [], []]

Explanation: The first query happens before any ingest, so it returns []. After 'x' is ingested twice, it is repeating, so there are no non-repeating words for the remaining queries.

Hints

  1. When a word appears for the first time, it becomes non-repeating. When it appears for the second time, it must be removed from the non-repeating order.
  2. A doubly linked list plus a hash map from word to node supports O(1) insertion and removal while preserving arrival order.

Community answers

Answer by Coderka14

#include using namespace std; struct TopCmp { bool operator()(const pair& a, const pair& b) const { if (a.first != b.first) { return a.first > b.first; } return a.second < b.second; } }; struct BottomCmp { bool operator()(const pair& a, const pair& b) const { if (a.first != b.first) { return a.first < b.first; } return a.second < b.second; } }; class Solution { public: vector> solution(vector>& operations) { unordered_map freq; set, TopCmp> topSet; set, BottomCmp> bottomSet; vector> answer; for (auto& operation : operations) { string type = operation[0]; if (type == "ingest") { string word = operation[1]; int oldFreq = freq[word]; if (oldFreq > 0) { topSet.erase({oldFreq, word}); bottomSet.erase({oldFreq, word}); } int newFreq = ++freq[word]; topSet.insert({newFreq, word}); bottomSet.insert({newFreq, word}); } else if (type == "topK") { int k = stoi(operation[1]); vector result; auto it = topSet.begin(); while (it != topSet.end() && (int)result.size() < k) { result.push_back(it->second); it++; } answer.push_back(result); } else if (type == "bottomK") { int k = stoi(operation[1]); vector result; auto it = bottomSet.begin(); while (it != bottomSet.end() && (int)result.size() < k) { result.push_back(it->second); it++; } answer.push_back(result); } } return answer; } };

Loading coding console...