Problem overview
The method OnlyInFirst gets two lists of integers, a and b. It returns a new list with the values of a that are not in b. Each value goes into the result once, in the order it first appears in a. If every value of a is also in b, the result is an empty list.
For a = [1, 2, 3, 4] and b = [2, 4] the answer is [1, 3]. For a = [5, 6, 5, 7] and b = [7] it is [5, 6]: 7 is in b, and 5 appears twice in a but once in the result. For a = [1, 2] and b = [2, 3, 1] the result is empty.
Approach
The method has two jobs, and each gets a set:
- "Is this value in
b?" A set built fromb, namedsetB, answers this quickly. Repeats inbcollapse into one, which doesn't matter for a membership check. - "Is this value already in the result?" A second set,
seen, remembers what was added. This is the same idea as removing repeats from a list.
The list result keeps the order. Read a from left to right, and keep a value when it is not in setB and not in seen.
Step by step
- Build
setBwithnew HashSet<int>(b). - Create an empty set
seenand an empty listresult. - Loop over
awithforeach, so values are met in their first-appearance order. - Join two tests with
&&:!setB.Contains(num)and!seen.Contains(num). Both must be true. - Inside the
if, addnumtoresultand toseen. - Return
resultafter the loop. It may be empty.
The solution in C#
class Solution {
public List<int> OnlyInFirst(List<int> a, List<int> b) {
HashSet<int> setB = new HashSet<int>(b);
HashSet<int> seen = new HashSet<int>();
List<int> result = new List<int>();
foreach (int num in a) {
if (!setB.Contains(num) && !seen.Contains(num)) {
result.Add(num);
seen.Add(num);
}
}
return result;
}
}
The details worth knowing:
&&stops as soon as its left side is false, so a value found insetBnever reaches the second test.- An empty
List<int>is a valid return value; there is no need fornull. - The method doesn't change
aorb.
Why sets here
The same task works with only lists, by calling b.Contains(num) and result.Contains(num) for every number of a. That is correct, but each of those calls scans a list. A set finds a value directly, so each check stays fast however long the lists are. The lists in this lesson are small, but the habit of turning "is it in there?" into a set pays off as soon as the data grows.
Notice also that the two sets play different roles. setB never changes after it is built. seen starts empty and grows as the loop goes, always matching the values already in result.
Tracing the second example
Here setB holds only 7, and seen starts empty.
| num | kept? | result after |
|---|---|---|
| 5 | yes | [5] |
| 6 | yes | [5, 6] |
| 5 | no, already in seen | [5, 6] |
| 7 | no, in setB | [5, 6] |
Testing
On the statement's three examples, the method returns:
[1, 3]
[5, 6]
[]
Edge cases and common mistakes
- Forgetting
seen. Without it, the second example returns[5, 6, 5], because the second 5 is also not inb. - Using lists for the checks.
b.Contains(num)works, but it compares against the values ofbone by one for every number; a set avoids that. - Using
||instead of&&. With||a value inbbut not yet inseenwould be kept. - Losing the order. Building the answer from a set operation alone gives no promise about order; looping over
ain order does.
