1

Let me preface with I AM A NEWBIE so take it easy on me please lol.

I need help writing a function that takes an array of words and returns an object with the letter and the length of the longest substring of that letter. Each word in the array consists of lowercase letters from a to z and has at least 1 character. You need to concatenate the words within the array to obtain a single word with the longest possible substring composed of one particular letter.

Example:

The input array is ["ccdd", "bbbb", "bbab"] the function should return an object of 6 and “b”. One of the best concatenations is words[1] + words[0] + words[2] = "ccddbbbbbbab"

Input:

longestSingleCharSubstring(["ccdd", "bbbb", "bbab"]);

Output:

{ "letter": "b", "length": 6 }

My issues with this problem are being able to permutate the elements of the array and then loop through the results to find the substring with the most repeating character. I keep getting "abcbundefined" when I return the result from my sample code. Any help would be great, thanks!

function longestSingleCharSubstring(arr) {
let result = [];
let cur_count = 1;     

if (arr.length === 0) return "";
if (arr.length === 1) return arr;

for (let i = 0; i < arr.length; i++) {
let currentChar = arr[i];

let remainingChar = arr.slice(0,i) + arr.slice(i + 1);

for (let j = 0; j < remainingChar.length; j++) {
result.push(currentChar + longestSubstring(remainingChar[j]);
}

let len = result.length;
let count = 0;

let finalRes = result[0];
for (let m=0; m<len; m++)  
{

for (let k = 0; k < result[m].length; k++) 
{
for (let l=k+1; l < result[m].length; l++)  
    if (k != l) 
        break;
    cur_count++;
}
if (cur_count > count) {
  count = cur_count;
  finalRes = result[m];
}
}
 return finalRes;
 }
}

console.log(longestSingleCharSubstring(['abcb','bbbc','aaaa']));
  • The answers here seem to be making an assumption that I think your text and code attempt don't support. You're not simply trying to combine the words in the input in the same order they are already in and then find the longest single-character run, right? The discussion of permutations is to say that you need to try various combinations, right? So in `['aa', 'ba', 'ac']` you need to try `'aabaac'`, `'aaacba'`, `'baaaac'`, `'baacaa'`, `'acaaba'`, and `'acbaaa'` to find that `'baaaac'` has a run of four `a`'s. Is this correct? – Scott Sauyet Aug 23 '21 at 15:00

4 Answers4

1

Here's an implementation that uses String.join to concatenate the original array into a long string. Then immediately calls String.split to split the long string into a char array. Next, we set up some variables for the loop and then loop through the char array. In the loop, we reset the current object with a new char and count of 0 when necessary. After incrementing the current count we update the longest result if current.count is longer than longest.count.

const longestSingleCharSubstring = (arr) => {
  const charArray = arr.join('').split('')

  let i = 0
  let longest = {
    char: '',
    count: 0
  }
  let current = {
    char: '',
    count: 0
  }
  while (i < charArray.length) {
    char = charArray[i]
    if (current.char != char) {
      current = {
        char,
        count: 0
      };
    }
    current.count += 1;
    if (current.count > longest.count) {
      longest = { ...current }
    }
    i += 1;
  }
  return longest
}

console.log(longestSingleCharSubstring(["ccdd", "bbbb", "bbab"]))
JasonB
  • 6,243
  • 2
  • 17
  • 27
1

Alternatively, you can also use regex here, It is clean and easy to read and maintain.

/([a-z])\1+/g

enter image description here

const longestSingleCharSubstring = (arr) => {
  const result = { char: "", count: 0 };

  return arr
    .join("")
    .match(/([a-z])\1+/g)
    .reduce((acc, curr) => {
      if (curr.length > acc.count) {
        acc.char = curr[0];
        acc.count = curr.length;
      }
      return acc;
    }, result);
};

console.log(longestSingleCharSubstring(["ccdd", "bbbb", "bbab"]));
DecPK
  • 24,537
  • 6
  • 26
  • 42
  • Very nice! What did you use to generate that Regex diagram? – Scott Sauyet Aug 23 '21 at 19:51
  • One other question: why the separate `acc` and `result` references to the same object? Why not drop `result`, use `acc` in its place, and replace the `result` argument to `.reduce` with `{ char: "", count: 0 }`? – Scott Sauyet Aug 23 '21 at 20:54
  • 1
    @ScottSauyet I'd used [regulex](https://jex.im/regulex/#!flags=&re=) – DecPK Aug 23 '21 at 23:32
  • Yeah, you are right but both are exactly the same AFAIK, and please correct me If I'm wrong. To simplify things I usually do declaration first and then pass the variables. usually reduce syntax is already complex to new ones, so to make it more readable I did that. – DecPK Aug 23 '21 at 23:37
  • I shouldn't use `result` in the `reduce` instead I should've used `acc`. In that case, you are correct. Thanks for pointing it out. – DecPK Aug 23 '21 at 23:39
  • 1
    The separate declaration is not a big deal, and you can pass it to the `.reduce` callback without a problem. But changing it inside the callback function seems weird. But it looks like you fixed that. Again, a very nice answer! I'm wondering if anyone else thinks that I caught a true requirement in my answer that was otherwise missed: considering all permutations of the original array. Have to wait for the OP, but it does seem to make more sense that way. – Scott Sauyet Aug 23 '21 at 23:44
1

An entirely reduce based approach which processes a splitted list of single characters while tracking the longest available single character sequence might look like the following one ...

function longestSingleCharSequence(value) {
  const {

    char,
    length,

  } = String(value) // e.g. 'ccddbbbbbbab'

    .split('') // ... ["c","c","d","d","b","b","b","b","b","b","a","b"]
    .reduce((collector, char, idx, arr) => {

      const { registry } = collector;
      const listOfCharSequences = registry[char] || (registry[char] = []);

      // if current character and the one before are not equal...
      if (char !== arr[idx - 1]) {

        // ...start a new char sequence.
        listOfCharSequences.push(char);
      } else {
        // ...continue concatenation of the most recent sequence.
        const recentIdx = listOfCharSequences.length - 1;
        const sequence = listOfCharSequences[recentIdx] + char;
        
        listOfCharSequences[recentIdx] = sequence;

        // ...`char`/`length` statistics is in need of an update.
        if (sequence.length >= collector.length) {

          collector.length = sequence.length;
          collector.char = char;
        }
      }
      return collector;

    }, { registry: {}, char: null, length: -1 });

  return { char, length };
}

console.log(
  "longestSingleCharSequence(['ccdd', 'bbbb', 'bbab'].join('')) ...",
  longestSingleCharSequence(['ccdd', 'bbbb', 'bbab'].join(''))
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

A regex based solution which matches letter sequences with any kind of letter from any language based on Unicode Property Escapes ...

function longestSingleLetterSequence(value) {
  // // [https://regex101.com/r/QFJKr5/1/]
  // const regXSingleCharSequence = (/(\w)\1{1,}/g);

  // [https://regex101.com/r/QFJKr5/2/]
  const regXSingleLetterSequence = (/(\p{L})\1{1,}/gu);

  const [ sequence ] = String(value)
    .match(regXSingleLetterSequence)
    .sort((a, b) => b.length - a.length);

  return {
    letter: sequence[0],
    length: sequence.length,
  };
}

console.log(
  "longestSingleLetterSequence(['ccdd', 'bbbb', 'bbab'].join('')) ...",
  longestSingleLetterSequence(['ccdd', 'bbbb', 'bbab'].join(''))
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
  • 1
    That regex approach is very interesting! I wouldn't have thought to use regex for this. Of course this is probably a toy problem and so performance wouldn't matter, but note that a `sort / head` technique is an inefficient way to find a maximum. – Scott Sauyet Aug 23 '21 at 19:49
  • @ScottSauyet ... The 2nd approach I meanwhile would even use in production for it is short and readable. But of cause in terms of amount of iteration cycles it mostly boils down to my most favored Swiss-knife `reduce` approach. With the cost of an additional `split`, the final result already is available by the end of having iterated a list of characters just once. – Peter Seliger Aug 23 '21 at 20:28
  • Indeed, most of the answers here have some variant of the `reduce`-for-maximum technique. Mine uses that as part of what I thought is the actual larger question. For that part of the question, I think decpk's answer is the most elegant, although with a caveat I'll post there. – Scott Sauyet Aug 23 '21 at 20:52
1

This answer assumes that my suggestion in the comments was correct, namely, that you want something that checks all permutations of the input array for a run of letters. That is, if we're given ['aa', 'ba', 'ac'], then we want to check 'aabaac', 'aaacba', 'baaaac', 'baacaa', 'acaaba', and 'acbaaa' to find that 'baaaac' contains a run of four a's. If this is not the case, then feel free to ignore this one.

Breaking it down

To do this, I would like to break it down in parts. I would like to write a function that takes the input strings, finds all permutations of these strings, concatenates the strings in each permutation into a single string, finds the longest single-letter streak in each one, and then selects the result with the longest streak.

I would like a main function, then, that looks like this:

const longestSingleLetterSequence = (arr) =>
  maximumBy (x => x .streak) (permutations (arr) .map (xs => xs .join ('')) .map (longestStreak))

which would be used like this:

longestSingleLetterSequence (['aa', 'ba', 'ac'])
//=> {"char": "a", "streak": 4, "word": "baaaac"}

But this means we need to write three helper functions, longestStreak, permutations, and maximumBy. Note that the last two might be genuinely reusable functions.

So let's look at creating these functions. We can write permutations this way:

const permutations = (xs) => 
  xs .length == 0 
    ? [[]] 
    : xs .flatMap ((x, i) => permutations (excluding (i) (xs)) .map (p => x + p))

by using a simple helper excluding, which takes an index and an array and returns a copy of that array excluding the value at that index.

permutations firsts checks if the input array is empty. If it is, we simply return an array containing just the empty array. Otherwise, for each element, we remove it from the list, recursively permute the remaining elements, and prepend that initial one to each result.

Our helper is simple. It could be inlined into permutations, but I think both are cleaner if they're separated this way. It might look like this:

const excluding = (i) => (xs) => 
  [... xs .slice (0, i), ... xs .slice (i + 1)]

Then we can write maximumBy, which takes a function and returns a function which takes an array and returns that element for which the function returns the highest value. It does this through a simple reduce call:

const maximumBy = (fn) => (xs) => 
  xs .reduce (({val, max}, x, i, _, xVal = fn (x)) => 
    (i == 0 || xVal > max) ? {max: xVal, val: x} : {val, max},
    {}
  ) .val

It's worth noting that this is not specific to numbers. It works with any values that can be compared with <, so numbers, strings, dates, or objects with valueOf methods.

These function so far are generic utility functions we can easily imagine reusing across projects. The next one, longestStreak, is more specific to this project:

const longestStreak = ([... chars]) => {
  let {streakChar, streak} = chars .reduce (
    ({currChar, count, streakChar, streak}, char) => ({
      currChar: char, 
      count: char == currChar ? count + 1 : 1,
      streakChar: (char == currChar ? count + 1 : 1) > streak ? currChar : streakChar,
      streak: Math .max (char == currChar ? count + 1 : 1, streak),
    }),
    {streak: -1}
  )
  return {char: streakChar, streak, word: chars .join ('')} 
}

Like many maxima problems, we can do this with .reduce. Here we restructure the input string into an array of characters with a parameter of [... cs]. Then as we fold our array, we keep returning an object with the structure {currChar, count, streakChar, streak}, which hold the currentChar we're tracking, the count of them seen so far, as well as the the character for the longest streak seen so far and that count. We start with an initial value of {streak: -1}.

When we're through the string, we return the streak character and its length (as well as the word that contains it; unnecessary here, but seemingly useful for the larger problem.)

Here is what it looks like altogether:

// Utility functions
const excluding = (i) => (xs) => 
  [... xs .slice (0, i), ... xs .slice (i + 1)]

const permutations = (xs) => 
  xs .length == 0 
    ? [[]] 
    : xs .flatMap ((x, i) => permutations (excluding (i) (xs)) .map (p => [x, ... p]))

const maximumBy = (fn) => (xs) => 
  xs .reduce (({val, max}, x, i, _, xVal = fn (x)) => 
    (i == 0 || xVal > max) ? {max: xVal, val: x} : {val, max},
    {}
  ) .val


// Helper function
const longestStreak = ([... chars]) => {
  let {streakChar, streak} = chars .reduce (
    ({currChar, count, streakChar, streak}, char) => ({
      currChar: char, 
      count: char == currChar ? count + 1 : 1,
      streakChar: (char == currChar ? count + 1 : 1) > streak ? currChar : streakChar,
      streak: Math .max (char == currChar ? count + 1 : 1, streak),
    }),
    {streak: -1}
  )
  return {char: streakChar, streak, word: chars .join ('')} 
}


// Main function
const longestSingleLetterSequence = (arr) =>
  maximumBy (x => x .streak) (permutations (arr) .map (xs => xs .join ('')) .map (longestStreak))


// Demos
console .log (longestSingleLetterSequence (['aa', 'ba', 'ac']))
console .log (longestSingleLetterSequence (["ccdd", "bbbb", "bbab"]))
.as-console-wrapper {max-height: 100% !important; top: 0}

Using additional helpers for clean-up

There is one thing I really don't like about the main function. You need to read very carefully to understand the order of operations:

const longestSingleLetterSequence = (arr) =>
  maximumBy (x => x .streak) (permutations (arr) .map (xs => xs .join ('')) .map (longestStreak))
//`-------- step 4 --------'  `---- step 1 ---'  `--------- step 2 -------' `------ step 3 ------'

I'm one of the primary authors of Ramda, and it provides some very useful tools to help manage such complexity. But those tools are easy to write ourselves. So with a few additional helpers, I would actually write the main function this way:

const longestSingleLetterSequence = pipe (
  permutations,
  map (join ('')),
  map (longestStreak),
  maximumBy (prop ('streak'))
)

And here the steps just run sequentially line after line. I won't go into detail about these helper functions, but if you want to see it in action, you can expand this snippet:

// Utility functions
const pipe = (...fns) => (...args) => 
  fns .slice (1) .reduce ((a, fn) => fn (a), fns[0] (...args))
const map = (fn) => (xs) => xs .map (x => fn (x))
const prop = (p) => (o) => o [p]
const join = (sep) => (xs) => xs .join (sep)
const excluding = (i) => (xs) => [... xs .slice (0, i), ... xs .slice (i + 1)]

const permutations = (xs) => 
  xs .length == 0 
    ? [[]] 
    : xs .flatMap ((x, i) => permutations (excluding (i) (xs)) .map (p => [x, ... p]))

const maximumBy = (fn) => (xs) => 
  xs .reduce (({val, max}, x, i, _, xVal = fn (x)) => 
    (i == 0 || xVal > max) ? {max: xVal, val: x} : {val, max},
    {}
  ) .val


// Helper function
const longestStreak = ([... chars]) => {
  let {streakChar, streak} = chars .reduce (
    ({currChar, count, streakChar, streak}, char) => ({
      currChar: char, 
      count: char == currChar ? count + 1 : 1,
      streakChar: (char == currChar ? count + 1 : 1) > streak ? currChar : streakChar,
      streak: Math .max (char == currChar ? count + 1 : 1, streak),
    }),
    {streak: -1}
  )
  return {char: streakChar, streak, word: chars .join ('')} 
}


// Main function
const longestSingleLetterSequence = pipe (
  permutations,
  map (join ('')),
  map (longestStreak),
  maximumBy (prop ('streak'))
)


// Demo
console .log (longestSingleLetterSequence (['aa', 'ba', 'ac']))
console .log (longestSingleLetterSequence (["ccdd", "bbbb", "bbab"]))
.as-console-wrapper {max-height: 100% !important; top: 0}
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103