Stacks & Queues · 37 of 38
Queue based approach or first non-repeating character in a stream
As letters arrive one by one, say which letter so far has shown up only once and arrived the earliest.
- Simple way
- time O(n^2)
room O(n) - Clever way
- time O(n)
room O(1)
The problem, simply
Letters come in one at a time, like a stream. After each new letter, answer this: among all the letters so far, which is the first one (the earliest to arrive) that has appeared exactly once? If no letter has appeared exactly once, answer #.
Give back all the answers as one string, one character per arriving letter. The examples put spaces between the answers only to make them easy to read.
A story to remember it
Picture it
After a, b, c, b the letter b repeats, but a and c are still seen once. a arrived first, so a is the answer right now.
- answer
- rejected
Examples
Repeats in the middle
- In
stream = 'abcbad'- Out
a a a a c c
b repeats but a is still unique, so a stays the answer. When a repeats too, both a and b leave and c is next.
Everything clears out
- In
stream = 'aabcbcd'- Out
a # b b c # d
After the second c, every letter seen so far repeats, so the answer is #. Then d arrives fresh.
The short classic
- In
stream = 'aabc'- Out
a # b b
The second a kills the only candidate. Then b arrives and stays first even after c.
One letter over and over
- In
stream = 'zzz'- Out
z # #
After the first z, z always repeats.
One letter
- In
stream = 'q'- Out
q
The only letter is unique.
Way 1: the simple way
Keep every letter of the stream and a count for each letter. After each new letter, walk the stream from the start and stop at the first letter whose count is 1.
- Append the letter to
seenand add 1 to its count. - Walk
seenfrom the start. The first letter with count 1 is the answer. - If none, the answer is
#.
Way 1 after a, b, c, b, a: walk the whole stream from the start and stop at the first letter whose count is 1. a and b repeat, so the walk stops at c.
- checking
- answer
class Solution {
public String firstNonRepeating(String stream) {
int[] count = new int[26];
StringBuilder seen = new StringBuilder();
StringBuilder out = new StringBuilder();
for (char c : stream.toCharArray()) {
count[c - 'a']++;
seen.append(c);
char ans = '#';
for (int j = 0; j < seen.length(); j++) { // walk from the very start
if (count[seen.charAt(j) - 'a'] == 1) { ans = seen.charAt(j); break; }
}
out.append(ans);
}
return out.toString();
}
}How long it takes: after the ith letter we may walk all i letters, so O(n^2) in total. Extra room: the stored stream, O(n).
Way 2: the clever way
Keep a waiting line, a queue, of letters in the order they first arrived, plus a count for each letter.
- A letter arrives. Add 1 to its count. If this is its first time, put it at the back of the queue.
- Clean the front: while the letter at the front has a count above 1, remove it. Repeated letters in the middle can wait; they are removed when they reach the front.
- The front of the queue is the answer. If the queue is empty, the answer is
#.
A queue of once-seen letters, cleaned from the front
01 / 08
Letters arrive one at a time. The queue keeps letters seen exactly once, oldest at the front. The front is always the answer.
- looking now
- done
- answer
- rejected
- just added
import java.util.*;
class Solution {
public String firstNonRepeating(String stream) {
int[] count = new int[26];
Queue<Character> line = new ArrayDeque<>();
StringBuilder out = new StringBuilder();
for (char c : stream.toCharArray()) {
if (++count[c - 'a'] == 1) line.offer(c); // first time: join the back
while (!line.isEmpty() && count[line.peek() - 'a'] > 1) {
line.poll(); // the front repeats: send it away
}
out.append(line.isEmpty() ? '#' : line.peek());
}
return out.toString();
}
}How long it takes: each letter joins the queue at most once and leaves at most once, so all the cleaning together is O(n), and the whole stream is O(n). Extra room: the queue holds each letter at most once, so at most 26 letters, O(1) for a fixed alphabet. The answer string itself is O(n).
Another way: a linked set with no waiting
If you want every step to be O(1) even in the worst case, keep the unique letters in a LinkedHashSet, which remembers insertion order and can remove any letter right away. Keep a second set of letters that have repeated.
import java.util.*;
class Solution {
public String firstNonRepeating(String stream) {
Set<Character> unique = new LinkedHashSet<>(); // seen once, in arrival order
Set<Character> repeated = new HashSet<>();
StringBuilder out = new StringBuilder();
for (char c : stream.toCharArray()) {
if (repeated.contains(c)) {
// already out of the race
} else if (unique.remove(c)) {
repeated.add(c); // second time: drop it now
} else {
unique.add(c); // first time
}
out.append(unique.isEmpty() ? '#' : unique.iterator().next());
}
return out.toString();
}
}Inside, LinkedHashSet is a hash map plus a doubly linked list, the same pair used by an LRU cache.
Watch out for
- Putting a letter into the queue every time it arrives. It still works, but the queue grows to
ninstead of at most 26. - Forgetting the
#case when the queue is empty. - Letters outside
atoz. Use anint[256]or aHashMapfor counts if the stream can have any character.
What interviewers ask next
- The stream is huge and has any Unicode character. Use a
HashMapfor counts. The queue idea stays the same. - First unique character in a fixed string, not a stream. Count everything first, then walk the string once and return the first letter with count 1.
- Return the k-th unique letter at each step. With the linked set, walk
ksteps from its start. With a doubly linked list plus a map, you can keep a pointer for it. - Why is Way 2
O(n)if there is a loop inside a loop? Each letter is removed at most once in the whole run. Count total removals, not removals per step. This is amortized analysis.
Remember this
Grown-up words: the stream is an online input, the line of unique kids is a queue, the tally marks are a frequency array, cleaning only the front is lazy deletion, and "each letter leaves at most once" is amortized O(1) per step.