LinkedListProblem 36 of 36
Find the first non-repeating character from a stream of characters
Find the First Non-Repeating Character from a Stream of Characters
Problem Statement
Given a stream of characters, find the first non-repeating character at any point. If no non-repeating character exists, return a special character (like '#' or '-1'). Design a system that can efficiently process characters as they arrive.
Example
Stream: a, a, b, c, b, d, d
After 'a': First non-repeating = 'a'
After 'a': First non-repeating = '#' (no non-repeating)
After 'b': First non-repeating = 'b'
After 'c': First non-repeating = 'b'
After 'b': First non-repeating = 'c'
After 'd': First non-repeating = 'c'
After 'd': First non-repeating = 'c'
Output: [a, #, b, b, c, c, c]
Brute Force Approach
Intuition
For each query, scan through all characters seen so far and find the first one with count = 1.
Algorithm
- Maintain a string/list of all characters received
- For each character, also maintain its count
- When queried, scan from the beginning to find first character with count = 1
Code
java
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
class FirstNonRepeatingBrute {
private List<Character> stream;
private Map<Character, Integer> count;
public FirstNonRepeatingBrute() {
stream = new ArrayList<>();
count = new HashMap<>();
}
public void add(char ch) {
stream.add(ch);
count.put(ch, count.getOrDefault(ch, 0) + 1);
}
public char getFirstNonRepeating() {
for (char c : stream) {
if (count.get(c) == 1) {
return c;
}
}
return '#';
}
public static void main(String[] args) {
FirstNonRepeatingBrute solver = new FirstNonRepeatingBrute();
String input = "aabcbdd";
System.out.println("Stream processing:");
for (char c : input.toCharArray()) {
solver.add(c);
System.out.println("After '" + c + "': First non-repeating = "
+ solver.getFirstNonRepeating());
}
}
}Complexity Analysis
- Time Complexity: O(n) per query, O(n²) total for n characters
- Space Complexity: O(n) for storing the stream
Optimal Approach (Doubly Linked List + HashMap)
Intuition
Use a Doubly Linked List (DLL) to maintain the order of non-repeating characters. Use a HashMap to track:
- Whether a character is repeated
- Pointer to its node in the DLL (if it exists)
When a character arrives:
- If new: add to DLL tail and HashMap
- If second occurrence: remove from DLL, mark as repeated
- If already repeated: ignore
The head of DLL is always the first non-repeating character.
Algorithm
- Create an empty DLL and a HashMap
- For each character in stream:
- If not seen: create node, add to DLL tail, add to HashMap
- If seen once: remove from DLL, update HashMap to mark repeated
- If already repeated: do nothing
- Query: return DLL head's data (or '#' if empty)
Code
java
import java.util.HashMap;
import java.util.Map;
class DLLNode {
char data;
DLLNode prev;
DLLNode next;
DLLNode(char c) {
this.data = c;
this.prev = null;
this.next = null;
}
}
class FirstNonRepeatingOptimal {
private DLLNode head;
private DLLNode tail;
private Map<Character, DLLNode> charToNode;
private Map<Character, Integer> count;
public FirstNonRepeatingOptimal() {
head = null;
tail = null;
charToNode = new HashMap<>();
count = new HashMap<>();
}
private void removeNode(DLLNode node) {
if (node.prev != null) {
node.prev.next = node.next;
} else {
head = node.next;
}
if (node.next != null) {
node.next.prev = node.prev;
} else {
tail = node.prev;
}
}
private void addToTail(char c) {
DLLNode newNode = new DLLNode(c);
if (tail == null) {
head = tail = newNode;
} else {
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
}
charToNode.put(c, newNode);
}
public void add(char ch) {
count.put(ch, count.getOrDefault(ch, 0) + 1);
if (count.get(ch) == 1) {
// First occurrence - add to DLL
addToTail(ch);
} else if (count.get(ch) == 2) {
// Second occurrence - remove from DLL
if (charToNode.containsKey(ch) && charToNode.get(ch) != null) {
removeNode(charToNode.get(ch));
charToNode.put(ch, null);
}
}
// If count > 2, already removed, do nothing
}
public char getFirstNonRepeating() {
if (head == null) {
return '#';
}
return head.data;
}
public static void main(String[] args) {
FirstNonRepeatingOptimal solver = new FirstNonRepeatingOptimal();
String input = "aabcbdd";
System.out.println("Stream processing:");
for (char c : input.toCharArray()) {
solver.add(c);
System.out.println("After '" + c + "': First non-repeating = "
+ solver.getFirstNonRepeating());
}
}
}Complexity Analysis
- Time Complexity: O(1) per add and O(1) per query, O(n) total
- Space Complexity: O(1) for 26 lowercase letters (or O(k) for k unique characters)
Alternative: Using Queue
Comparison
| Approach | Add Time | Query Time | Space | |----------|----------|------------|-------| | Brute Force | O(1) | O(n) | O(n) | | DLL + HashMap | O(1) | O(1) | O(k) | | Queue + HashMap | O(1) amortized | O(1) amortized | O(n) |
Key Takeaways
- DLL for Order: Doubly linked list maintains insertion order efficiently
- HashMap for Access: O(1) lookup and removal from DLL via pointers
- Three States: New, seen once (in DLL), repeated (removed)
- Stream Processing: System must handle continuous input efficiently
- Queue Alternative: Simpler but may have O(n) worst case cleanup
- Bounded Alphabet: If only lowercase letters, space is O(26) = O(1)
- Real-world Use: Log processing, network packet analysis
- Design Pattern: Combination of multiple data structures for optimal performance