I've built a thesaurus-application in React that fetches data from a web-dictionary API and renders definitions, synonyms, and antonyms as collapsible lists when a user searches a word. I'd like to add a feature that will display all of the valid anagrams of a word that is searched (this isn't the problem right now, however).
I have written a recursive algorithm that finds all of the possible permutations of any given input, based on the number of possible permutations for that word. The only problem is, I encounter a RangeError
when the input is any greater than 6 letters long. I know that my algorithm can and will find all permutations of an input greater than 6 characters long, but is hindered by the maximum-size of the call-stack.
I've attempted to use multiple different non-recursive algorithms that accomplish the same purpose from various other sources I've stumbled upon, and all but one encounter the same issue. If possible, however, I would like to refactor my solution to be viable, rather than copying the one working solution that I have found. I will display both my solution and the working solution for informational purposes.
My solution:
/* These next two helper functions can be ignored, I've included them in case
of your curiosity. However, they are unimportant to the problem at hand.
Both functions merely determine the total number of possible permutations for a given
input, which I use to determine how many times my final function should recurse */
// Helper function 1
const hasDuplicates = (str) => {
const letters = {};
str.split('').forEach(letter => {
if (letters[letter] !== undefined) letters[letter]++;
if (letters[letter] === undefined) letters[letter] = 1;
});
for (let key in letters) {
let currLetter = letters[key];
if (currLetter > 1) return letters;
};
return false;
};
// Helper function 2
const numPermutations = (str) => {
if (hasDuplicates(str) === false) {
let multiplier = 1;
for (let i = 1; i <= str.length; i++) multiplier *= i;
return multiplier;
};
const letters = hasDuplicates(str);
let multiplier = 1;
let divisor = 1;
let visited = new Set();
for (let i = 1; i <= str.length; i++) {
let currLetter = str[i];
if (letters[currLetter] > 1 && !visited.has(currLetter)) {
for (let j = 1; j <= letters[currLetter]; j++) {
divisor *= j;
};
visited.add(currLetter);
};
multiplier *= i;
};
return (multiplier / divisor);
};
// Final recursive function
const permutations = (string, finalArray = [], i = 0, visited = new Set()) => {
/* If the input consists of only two values and both are identical, we know that
further evaluation is unnecessary. */
if (string.length === 2) {
if (string.split('')[0] === string.split('')[1]) {
finalArray.push(string);
return finalArray;
};
};
if (string.length <= 2 && finalArray.length === string.length) return finalArray;
// Call to previous helper function which determines number of times we must recurse
const maxPermutations = numPermutations(string);
if (i === maxPermutations) return finalArray;
const splitString = string.split('');
// Scramble the letters of the string and rearrange them in a random order
for (let i = splitString.length - 1; i > 0; i--) {
let randNum = Math.floor(Math.random() * (i + 1));
let replacement = splitString[i];
splitString[i] = splitString[randNum];
splitString[randNum] = replacement;
};
if (!visited.has(splitString.join(''))) {
/* If we don't already have this random variation of the string in our array,
push it into our final array, add it to the set of strings we've encountered,
and increment our index variable to work towards the base case */
finalArray.push(splitString.join(''));
visited.add(splitString.join(''));
return permutations(string, finalArray, i += 1, visited);
};
/* If we have encountered the latest random variation of our string,
recurse without incrementing our index (does not work toward base case) */
return permutations(string, finalArray, i, visited);
};
Again, this works great for inputs less than 7 characters long. However, anything longer, and the maximum call stack size is exceeded. I am including the one solution I've found that works around this problem below, in hope that it may be able to shed light on a possible workaround for my solution. That being said, I don't understand how this solution works or why it works, only that it does. I'll use it in my application as a last resort, but I prefer to use my own work over the work of someone else.
function permutes(string) {
var s = string.split('').sort();
var res = [s.join('')]
while(true) {
var j = s.length - 2;
while (j != -1 && s[j] >= s[j + 1])
j--;
if(j == -1)
break;
var k = s.length - 1;
while(s[j] >= s[k])
k--;
[s[j], s[k]] = [s[k], s[j]];
var l = j + 1, r = s.length - 1;
while (l<r) {
[s[l], s[r]] = [s[r], s[l]];
l++;
r--;
}
res.push(s.join(''));
}
return res;
}