The question I was answering was [rightfully] closed as a duplicate of this one. But I think this answer offers something that the others miss: it is both O(n) and correct in the face of digits among the characters, which many answers here miss.
A first and perhaps naive approach would be to iterate through the characters and collect two results:
- a list of the characters ordered by their first appearance in the string
- a mapping of characters to their counts
So we might have intermediate data like this:
{order: ["s","w","i"], counts: {s: 3, w: 1, i: 1}}
or
{order: ["a","b","c"], counts: {a: 2, b: 2, c: 2}}
With that we can search that list, checking each character against the mapping until we find the first one with a count of 1.
Here is one version of that approach:
const findFirstNonRepeatingChar = (s) => {
const {order, counts} = [...s].reduce(({order, counts}, c) => ({
order: c in counts ? order : order.concat(c),
counts: {...counts, [c]: (counts[c] || 0) + 1}
}), {order: [], counts: {}})
return order.find(c => counts[c] == 1) || null
}
console.log(findFirstNonRepeatingChar('swiss')) //=> 'w'
console.log(findFirstNonRepeatingChar('aabbcc')) //=> null
console.log(findFirstNonRepeatingChar('abcdefabcdeabdgabh')) //=> 'f'
console.log(findFirstNonRepeatingChar('abcdefabcdeabdgabhfgh')) //=> null
.as-console-wrapper {max-height: 100% !important; top: 0}
A sharp eye would note that we really seem to have all the information necessary in the counts Object. So it looks like we could use the intrinsic ordering of JS Objects to simplify this, with code something like this:
// WARNING: Broken implementation
const findFirstNonRepeatingChar = (s) =>
(Object.entries(
[...s].reduce((a, c) => ({...a, [c]: (a[c] || 0) + 1}), {})
).find(([k, v]) => v == 1) || [null])[0]
Instead of using a separate order object, we extract the entries from the generated counts and call find on that. It looks as though it works:
console.log(findFirstNonRepeatingChar('swiss')) //=> 'w'
console.log(findFirstNonRepeatingChar('aabbcc')) //=> null
console.log(findFirstNonRepeatingChar('abcdefabcdeabdgabh')) //=> 'f'
console.log(findFirstNonRepeatingChar('abcdefabcdeabdgabhfgh')) //=> null
But there's something broken here. The trouble is that Object iteration order is more complex than it first seems. According to the specification the first iteration keys are those strings that look like array indices, and they are iterated in numeric order, and only then the other keys are iterated, those in insertion order.
So introducing a digit will break this:
console.log(findFirstNonRepeatingChar('a0b')) //=> '0' -- OOPS!!
You can see this by expanding this snippet:
// WARNING: broken code
const findFirstNonRepeatingChar = (s) =>
(Object.entries(
[...s].reduce((a, c) => ({...a, [c]: (a[c] || 0) + 1}), {})
).find(([k, v]) => v == 1) || [null])[0]
console.log(findFirstNonRepeatingChar('swiss')) //=> 'w'
console.log(findFirstNonRepeatingChar('aabbcc')) //=> null
console.log(findFirstNonRepeatingChar('abcdefabcdeabdgabh')) //=> 'f'
console.log(findFirstNonRepeatingChar('abcdefabcdeabdgabhfgh')) //=> null
console.log(findFirstNonRepeatingChar('a0b')) //=> 'c' -- OOPS!!
.as-console-wrapper {max-height: 100% !important; top: 0}
We could fix this by using a Map rather than a plain object, with an implementation like this:
const findFirstNonRepeatingChar = (s) => {
const counts = [...s].reduce((counts, c) => {
counts.set(c, (counts.get(c) || 0) + 1); return counts
}, new Map())
return ([...counts.entries()].find(([k, v]) => v == 1) || [null])[0]
}
console.log(findFirstNonRepeatingChar('swiss')) //=> 'w'
console.log(findFirstNonRepeatingChar('aabbcc')) //=> null
console.log(findFirstNonRepeatingChar('abcdefabcdeabdgabh')) //=> 'f'
console.log(findFirstNonRepeatingChar('abcdefabcdeabdgabhfgh')) //=> null
console.log(findFirstNonRepeatingChar('a0b')) //=> 'a' -- Much better!
.as-console-wrapper {max-height: 100% !important; top: 0}
You can decide for yourself which implementation you like better. Even if that middle one worked, I would hesitate to use it. I prefer to think of objects as intrinsically unordered containers and never depend on their odd iteration order. The last one bothers me because of the mutation of the Map objects. I prefer to work with immutable data as much as possible. But you may have different priorities.
var charMap = {}; charMap['a'] = 0; charMap['b'] = 1;? You could iterate through the input string, adding every char into a charMap: jsfiddle.net/w7F87/14