【问题标题】:How can I prevent a RangeError while running an algorithm that finds all permutations of a given word?如何在运行查找给定单词的所有排列的算法时防止 RangeError?
【发布时间】:2022-10-17 23:51:53
【问题描述】:

我在 React 中构建了一个词库应用程序,它从网络词典 API 获取数据,并在用户搜索单词时将定义、同义词和反义词呈现为可折叠列表。我想添加一个功能来显示所有有效的搜索的单词的字谜(但这不是现在的问题)。

我已经编写了一个递归算法,它根据该单词的可能排列数来查找任何给定输入的所有可能排列。唯一的问题是,当输入长度超过 6 个字母时,我遇到了 RangeError。我知道我的算法能够将要查找长度大于 6 个字符的输入的所有排列,但受到调用堆栈的最大大小的阻碍。

我尝试使用多种不同的非递归算法,这些算法从我偶然发现的各种其他来源中实现相同的目的,但除了一个之外,所有人都遇到了同样的问题。但是,如果可能的话,我想重构我的解决方案以使其可行,而不是复制我找到的一个可行的解决方案。我将展示我的解决方案和工作解决方案以供参考。

我的解决方案:

/* 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);
};

同样,这对于长度少于 7 个字符的输入非常有效。但是,任何更长的时间,都会超出最大调用堆栈大小。我在下面包含了我发现的解决此问题的一个解决方案,希望它能够阐明我的解决方案的可能解决方法。话虽这么说,我不明白这个解决方案是如何工作的或为什么工作,只是它确实如此。我将在我的应用程序中使用它作为最后的手段,但我更喜欢使用我自己的工作而不是其他人的工作。

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;
}

【问题讨论】:

    标签: javascript recursion permutation infinite-loop callstack


    【解决方案1】:

    您应该使用一个名为nextPermutation 的函数,它只返回按词汇顺序排列的下一个排列。这将节省大量内存。

    考虑到this answer,只需将数字转换为字母。我们试试看。

    var nextPermutation = function(word) {
    
      var nums = word.split('');
    
      function swap(index1, index2) {
        var temp = nums[index1]
        nums[index1] = nums[index2]
        nums[index2] = temp;
      }
    
      function next_bigger_from(pos) {
        var current = nums[pos];
        var result = -1;
        var min = null;
        for (var i = pos + 1; i < len; i++) {
          if (nums[i] > current) {
            result = i;
            min = nums[i];
          }
        }
        return result;
      }
    
      function sort_from(pos) {
        for (var i = pos; i < len - 1; i++) {
          for (var j = i + 1; j < len; j++) {
            if (nums[i] > nums[j]) {
              swap(i, j)
            }
          }
        }
      }
    
      var len = nums.length;
    
    
      if (len < 2) {
        console.log("" + nums)
        return;
      }
    
      var rotator = 2; // from right
      while (rotator <= len) {
        var pos = len - rotator;
        var pos2 = next_bigger_from(pos);
        if (pos2 == -1) {
          rotator += 1;
          continue;
        }
        swap(pos, pos2);
        sort_from(pos + 1);
        return nums.join("");
      }
    
      nums = nums.sort();
    
    
      return nums.join("");
    };
    
    
    var str = "ABCDEFGH"
    for (var i = 0; i < 100; i++) {
      console.log(str)
      str = nextPermutation(str)
    }
    .as-console-wrapper {max-height: 100% !important}

    【讨论】:

    • 我不确定我是否遵循,这会被集成到我当前的算法中还是完全单独使用?
    • 可能是我误读了这个问题。这只是一个逐一进行排列的函数,因此不会导致内存问题。这是一个替代方案。所以现在你有 3 个选项可供选择。如果您的内存不足,请考虑使用我的。
    • 我明白了,谢谢你的替代品!
    【解决方案2】:

    您正在尝试通过带替换的随机抽样来计算所有排列。这可以通过Coupon Collector's Problem 建模。通过这种方法获得它们的预期试验次数比n * log (n) 增长得更快,其中n 是您要获取的项目总数。如果您有7(所有不同的)字符,则排列数将为7!5040,这意味着在您全部命中之前的预期尝试次数是

    [1, 2, 3, ... 5040] .reduce ((n, f) => n + 5040 / f, 0)
    

    这有点超过45876。当然,如果有重复的字母,这个值会更小。

    递归执行此操作意味着您可能会遇到递归深度限制。如果您切换到迭代循环,这可能适用于七次,但您仍然会很快通过这种技术达到显着限制。

    如果您想要所有排列,但以随机顺序,我建议您全部生成它们并shuffle 结果。

    请注意,您绝对可以同时简化计数函数和生成函数。要计算它们,我们可以使用数学事实,如果有 a A's,b B's,... 和 k K's,则总排列数为

    (a + b + ... + k)! / (a! * b! * ... * k!)
    

    因此,使用一个简单的count 函数(例如,将"ABCDBBC" 转换为{A: 1, B: 3, C: 2, D: 1})和一个简单的factorial 函数,我们可以简单地编写它。

    我知道你更喜欢使用自己的代码,但是如果你想看看我对这个想法的实现,你可以扩展这个 sn-p:

    const factorial = (n) => n < 2 ? 1 : n * factorial (n - 1)
    
    const count = ([...s]) => 
      s .reduce ((a, c) => ((a [c] = (a[c] || 0)  + 1), a), {})
    
    const numPermutations = (s) =>
      Object .values (count (s)) .reduce ((n, f) => n / factorial (f), factorial (s .length))
    
    console .log (numPermutations ('ABCDBBC')) //=> 420
    
    
    // We probably don't call it enough to matter, but `factorial` is a good candidate for 
    // memoization.  If you want to do that, it's simple enough:
    //
    // const factorial = ((memo = {}) => 
    //   (n) => n in memo ? memo [n] : memo [n] = n < 2 ? 1 : n * factorial (n - 1)
    // )()

    为了直接找到所有排列,我们可以使用我在another answer 中使用的技术的变体。那个没有考虑多重性。我们可以调整它以在我们可能开始遇到重复时停止收集。同样,如果您想查看我的实现,请展开 sn-p,并在 cmets 中进行解释。

    // Here `excluding` returns a copy of an array with the element at a particular index 
    // removed.  Our `permutations` function returns a result containing only an empty array if 
    // the input is empty.  Otherwise it loops through the letters, and, if it's the first 
    // instance of that letter removing it from consideration, recurring with the remaining 
    // letters and for each result, prepending this first letter to it.  If it's not the first 
    // instance, we just return an empty result.  This last is the twist from the original which 
    // blindly did the recursive step, and thus for `"AAB"` would return all six possible 
    // permutations, even though there are duplicates (`["AAB", "ABA", "AAB", "ABA", "BAA", 
    // "BAA"]`).  By testing `xs .indexOf (x) == i`, we eliminate large swaths of the 
    // potential output.
    
    
    const excluding = (i) => (xs) => 
      [... xs .slice (0, i), ... xs .slice (i + 1)]
    
    const permutations = ([...xs]) => 
      xs .length == 0 
        ? [[]] 
        : xs .flatMap ((x, i) => 
            xs .indexOf (x) == i ? permutations (excluding (i) (xs)) .map (p => x + p) : []
          )
    
    console .log (permutations ('ABCDBBC'))
    .as-console-wrapper {max-height: 100% !important; top: 0}

    【讨论】:

      猜你喜欢
      • 2011-04-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多