Microsoft OA | Longest Substring Without 3 Contiguous Occurrences of Letter For the following question, does my JS Solution seem correct?
I'm looking for some peer review.

Q: Given a string s containing only a and b, find longest substring of s such that s does not contain more than two contiguous occurrences of a and b.

Examples: 
&quot;aabbaaaaabb&quot; =&gt; &quot;aabbaa&quot; 
&quot;aabbaabbaabbaa&quot; =&gt; &quot;aabbaabbaabbaa&quot;
function maxLenSubStr(s) {
    let start = 0, end = 0;
    let numOfConsecutiveChars = 0;
    let res = &quot;&quot;;
    while (end &lt;= s.length) {
        numOfConsecutiveChars++;
        if (numOfConsecutiveChars &gt; 2 || end == s.length) { // time to end our substring is these two cases
            if (end-start &gt; res.length) res = s.substring(start,end); // if it is not longer, there is no need to store it
            start = end-1; // slide our window so we can look for a new valid substring
            numOfConsecutiveChars--; // now we have a valid substring with two contiguous occurrences instead of three
        }
        if (s[end] !== s[end+1]) numOfConsecutiveChars = 0;
        end++;
    }
    return res;
    // Time Complexity: O(n)
    // Space Complexity: O(1)
}
After some tests, the code does look correct, but the logic takes some time to parse and understand, which is a significant disadvantage; when writing good, maintainable code, readability is usually the most important factor to consider.
If a script is running too slowly for your liking, you can run a performance test to identify the bottleneck(s) and then fix those bottlenecks (possibly with hard-to-read code like this), but before then, better to write easy-to-write, easy-to-understand code first.
If it were me, I'd use a regular expression. It'll have many fewer &quot;moving parts&quot; than going through the string manually and counting up lengths, and REs are quite widely used for general string matching and manipulation across most programming languages; it's something a programmer will probably already be familiar with, so hopefully using them won't present a problem.
The one complication with a regular expression is that when there may be overlapping matches, the code can get somewhat ugly, see here for examples. Here, with a string of abbba, you'd have to consider both abb and bba, where the middle b is contained in both. Rather than deal with overlapping matches, I'll replace all 3+-contiguous-occurrences with 4-contiguous-occurrences beforehand. Then it's trivial to use .split to split along the middle of each 4-contiguous section, and then return the longest string in the resulting array:


const maxLenSubStr = s =&gt; s
  // Expand 3 or more contiguous occurrences to 4 contiguous occurrences
  // eg 'abbba' -&gt; 'abbbba'
  .replace(/(.)\1{2,}/g, '$&amp;$1')
  // Split along the middle of each 4-contiguous subsequence
  // eg 'abbbba' -&gt; ['abb', 'bba']
  .split(/(?&lt;=\1(.))(?=\1{2})/)
  .reduce((a, b) =&gt; a.length &gt; b.length ? a : b);

console.log(maxLenSubStr("aabbaaaaabb")); //  =&gt; "aabbaa"
console.log(maxLenSubStr("aabbaabbaabbaa")); //  =&gt; "aabbaabbaabbaa"
console.log(maxLenSubStr("baaa")); //  =&gt; "baa"



For the regular expressions, the same technique is being used in both. Capture a character in a group, then backreference that group to match the same character again as many times as needed.

(.)\1{2,} - (.) captures the character, \1{2,} matches it again 2 or more times, then replaces with 4 occurrences of the character.
(?&lt;=\1(.))(?=\1{2}) - (.) captures the character in the lookbehind, preceded by \1, another occurrence of the character. Then (?=\1{2}) looks ahead for 2 more occurrences of the character.

I'd consider this an order of magnitude easier to understand than your original code. It's much easier to see at a glance that it'll carry out the desired logic properly, at least for those with a bit of experience with regular expressions. Whether it's worth replacing your original code with it would depend on you and your team's familiarity with REs.
If you had to do it without lookbehind (which is not supported on out-of-date browsers), then split with another capturing group around the whole left match, or use .match instead, to match characters until encountering 3 in a row:


const maxLenSubStr = s =&gt; s
  // Expand 3 or more contiguous occurrences to 4 contiguous occurrences
  // eg 'abbba' -&gt; 'abbbba'
  .replace(/(.)\1{2,}/g, '$&amp;$1')
  // Split along the middle of each 4-contiguous subsequence
  // eg 'abbbba' -&gt; ['abb', 'bba']
  // Lazily match characters until encountering the end of the string
  // or until 3 of the same character are in a row
  .match(/.*?(?:$|(.)\1(?=\1))/g)
  .reduce((a, b) =&gt; a.length &gt; b.length ? a : b);

console.log(maxLenSubStr("aabbaaaaabb")); //  =&gt; "aabbaa"
console.log(maxLenSubStr("aabbaabbaabbaa")); //  =&gt; "aabbaabbaabbaa"
console.log(maxLenSubStr("baaa")); //  =&gt; "baa"
console.log(maxLenSubStr("aaab")); //  =&gt; "aab"



If you were to keep using your original code, I'd suggest:

Use strict comparison === instead of sloppy comparison == (which has weird coercion rules a reader of the code shouldn't have to worry about)

Use spaces between non-unary operators and operands, for readability, eg end - start &gt; res.length instead of end-start &gt; res.length

Rather than numOfConsecutiveChars--;, since the new consecutive count will always be 2, better to assign 2 to it directly: numOfConsecutiveChars = 2

Increment numOfConsecutiveChars only when a subsequent consecutive character is found, and not unconditionally. (Your current logic works, it's just confusing)