递归打印字符串的所有排列(Javascript)

40

我看过其他语言的类似问题,但没有关于JavaScript的。

是否可能在一个函数中以递归方式执行此操作?

我理解我需要取字符串中的第一个元素,然后将其附加到对余下字符串进行递归的每个解决方案。 所以从逻辑上讲,我理解递归应该如何进行。 我只是不知道如何将第一个字符附加到每个递归解决方案上。

var myString = "xyz";

function printPermut(inputString){
    var outputString;
    if(inputString.length === 0){
        return inputString;
    }
    if(inputString.length === 1){
        return inputString;
    }
    else{
       for(int i = 0; i<inputString.length(); i++){
           //something here like: 
           //outputString = outputString.concat(printPermut(inputString.slice(1))??
           //maybe store each unique permutation to an array or something?
       } 
    }
}

1
提示:function foo(x) { return [ x.substring(0, 1) ].push(foo(x.substring(1))) } - Tibrogargan
@FuzzyTree 所有这些似乎都在函数外使用全局变量或其他函数(即两个返回值)。我很好奇是否可以在一个函数中使用一个输入参数完成。 - singmotor
@Tibrogargan 哈哈,不是的,那肯定会导致堆栈溢出。 - Mulan
11个回答

79

让我们编写一个函数,它将一个字符串的所有排列作为数组返回。由于您不希望有任何全局变量,因此关键是返回排列。

  function permut(string) {
  if (string.length < 2) return string; // This is our break condition

  var permutations = []; // This array will hold our permutations
  for (var i = 0; i < string.length; i++) {
    var char = string[i];

    // Cause we don't want any duplicates:
    if (string.indexOf(char) != i) // if char was used already
      continue; // skip it this time

    var remainingString = string.slice(0, i) + string.slice(i + 1, string.length); //Note: you can concat Strings via '+' in JS

    for (var subPermutation of permut(remainingString))
      permutations.push(char + subPermutation)
  }
  return permutations;
}

要打印它们,只需在数组后面迭代即可:

 var myString = "xyz";
 permutations = permut(myString);
 for (permutation of permutations)
   print(permutation) //Use the output method of your choice

希望我能帮助您解决问题。


1
出于性能考虑,您可能希望将排列存储在对象中,这样查找速度会更快,而不是存储在数组中并使用“indexof”。10000个项目的对象查找键:152,115 Operations/sec VS 10000个项目的Index:26,547 Operations/sec。 - Nicholas Porter
@NicholasPorter indexOf 不是用于排列数组,而是用于生成排列的字符串。 - Syntac
1
据我所知,第二行应该是 if (string.length < 2) return [string]; - loevborg

49
排列问题已经被深入研究过了。Heap's algorithm 是其中一个著名的解决方案。这里提供一个使用生成器的 JS 版本:

function *permute(a, n = a.length) {
  if (n <= 1) yield a.slice();
  else for (let i = 0; i < n; i++) {
    yield *permute(a, n - 1);
    const j = n % 2 ? 0 : i;
    [a[n-1], a[j]] = [a[j], a[n-1]];
  }
}

console.log(Array.from(permute("abcabad".split('')))
.map(perm => perm.join(''))
.filter((el, idx, self) => (self.indexOf(el) === idx)));

permute旨在接受和生成数组,而不是字符串,因此在调用它之前,我们将字符串拆分为字符,并在打印结果之前将字符粘贴回字符串。


1
这个算法的空间复杂度是多少? - zero_cool
如果我检查aaa,它会返回["aaa", "aaa", "aaa", "aaa", "aaa", "aaa"]而不是只有[aaa]。 解决方案是: console.log(Array.from(permute("abcabad".split(''))).map(perm => perm.join('')).filter((el, idx, self) => (self.indexOf(el) === idx))); - Chang

5

使用递归函数遍历字符串

    

    function getPermutations(string) {
      var results = [];

      if (string.length === 1) 
      {
        results.push(string);
        return results;
      }

      for (var i = 0; i < string.length; i++) 
      {
        var firstChar = string[i];
        var otherChar = string.substring(0, i) + string.substring(i + 1);
        var otherPermutations = getPermutations(otherChar);
        
        for (var j = 0; j < otherPermutations.length; j++) {
          results.push(firstChar + otherPermutations[j]);
        }
      }
      return results;
    }
    
    var permutation = getPermutations('YES').filter((el, idx, self) => (self.indexOf(el) === idx));
    console.log("Total permutation: "+permutation.length);
    console.log(permutation);


5
问题分类:您可以将此问题视为探索性问题,即给定一组输入字符,探索不同的排列方式。 解决方案:回溯算法擅长解决探索性问题,尽管它具有高时间复杂度。为了演示解决方案,请想象一下如何手动解决这个小型输入字符集的问题:[a, b, c]
以下是步骤:
  1. 取最左边的字符。这是索引为0的字符,并将其与目标右字符在索引0交换,即与自身交换。这是因为[a, b, c]本身就是一个有效的排列,因此我们要保留它。交换字符通常需要两个指针,分别指向每个字符。所以我们假设有一个leftright指针。
  2. 使用相同的最左边的字符(索引为0),将其与目标右字符在索引0 + 1 = 1交换,即将目标右指针向前移动1步。这将给出输出:[b, a, c]
  3. 使用相同的最左边的字符(索引为0),将其与下一个目标右字符(即索引0 + 1 + 1 = 2)交换。这将给出输出:[c, b, a]
  4. 好的,现在我们需要停止,因为没有更多的目标右字符可以与最左边的字符交换了。因此,我们的right指针需要小于input中的最大索引。使用for循环逐步移动right指针,从left索引开始,以输入长度-1结尾。

  5. 现在,您需要执行与上述相同的步骤,但移动左指针,使其指向下一个最左边的字符。但保留步骤2和3中的输入。另一种想象这种情况的方式是说:“嘿,我已经完成了最左边的字符。现在我不想再使用它,但我很想继续使用到目前为止的第二个最左边的字符。”

  6. 何时停止?当左指针达到输入字符串长度-1时,因为此索引之后没有更多字符。在递归算法(如回溯)中,需要停止的情况称为基本情况。在我们的示例中,基本情况是:left === input.length - 1

这是一个图形化可视化:
left index|                         Input String:
-------------------------------------------------------------------------------

left = 0  |                                                              in=[a, b, c]

                    (swap in[0] with in[0])                         (swap in[0] with in[1])                         (swap in[0] with in[2])
left = 1  |               in=[a, b, c]                                   in=[b, a, c]                                      in=[c, b, a]

            (swap in[1] with in[1]) (swap in[1] with in[2]) (swap in[1] with in[1])(swap in[1] with in[2])  (swap in[1] with in[1])(swap in[1] with in[2])
left = 2  | [a, b, c]                   [a, c, b]               [b, a, c]               [b, c, a]                   [c, b, a]           [c, a, b]

概述:

  • 为了将左指针向右移动,我们将使用递归增量。
  • 为了将右指针向右移动,我们将使用一个for循环,但是我们需要始终从左指针开始,否则我们将探索我们已经探索过的东西。

回溯算法: 回溯算法的伪代码形式如下:

fun(input)
    if(base_case_check(input)) {
        //do final step
    } else {
        //choose
        fun(reduce(input)) //explore
        //un-choose
    }

我们的解决方案:

function permutate(string) {

  if(!string || string.length === 0)
    return new Set(['']);
  
  let left = 0;
  let result = new Set();
  permutationHelper(string, result, left);
  
  return result;
}

function permutationHelper(string, result, left) {
  
  if(left === string.length-1) {
    //base case
    result.add(string);
  } else {
    //recursive case
    for(let right=left; right < string.length; right++) {
      string = swap(string, left, right); //choose
      permutationHelper(string, result, left+1); // explore
      string = swap(string, left, right); //unchoose
    }
  }
}

function swap(string, left, right) {
  let tmpString = string.split('');
  let tmp = tmpString[left];
  tmpString[left] = tmpString[right];
  tmpString[right] = tmp;
  return tmpString.join('');
}
/* End of solution */

/* Tests */
let input = 'abc';
let result = permutate(input);
let expected = new Set(['abc', 'acb', 'bac', 'bca', 'cab', 'cba']);
if(setsEquality(result, expected)) {
console.log('Congrats, you generated all permuations');
} else {
console.log('Sorry, not all permuations are generated');
}

function setsEquality(actualResult, expectedResult) {
  if (actualResult.size !== expectedResult.size) {
    return false;
  }
  for (let permutation of actualResult) {
    if (!expectedResult.has(permutation)) return false;
  }
  return true;
}

function assert(condition, desc) {
  if (condition) {
    console.log(`${desc} ... PASS`);
  } else {
    console.log(`${desc} ... FAIL`);
  }
}

总结和时间复杂度:

  • 我们通过交换现有输入字符串中的字符来进行选择
  • 当我们将左索引增加1时,我们会探索剩下要探索的内容。事实上,这意味着我们将所有后续递归的输入集合减少1。因此,我们需要完成的工作是:Nx(N-1)x(N-2)x(N-3)x...x1 = N!。然而,由于我们需要使用for循环来在输入中进行探索,所以总时间复杂度为:0(N*N!)
  • 我们通过在修改后的输入字符串中交换字符来恢复选择。

3
const permutation = (str, prefix) => {
  if (str.length === 0) {
    console.log(prefix);
  } else {
    for (let i = 0; i < str.length; i++) {
      let rem = str.substring(0, i) + str.substring(i + 1);
      permutation(rem, prefix + str[i]);
    }
  }
}
let str = 'ABC';
permutation(str, '');

0

半离题:

给定字符串的随机排列就像使用 rndperm 一样简单:

i = document.getElementById("word");
b = document.getElementById("butt");

rndperm = (z) => {
  return z.split("").sort(() => ((Math.random() * 3) >> 0) - 1).join("")
}

function scramble() {
  i.value = rndperm(i.value);
}

var z;

function sci() {
  if (z != undefined) {
    clearInterval(z);
    b.innerText = "Scramble";
    z=undefined;
  } else {
    z = setInterval(scramble, 100);
    b.innerText = "Running...";
  }
}
<center><input id="word" value="HelloWorld"></input><button id="butt" onclick=sci()>Scramble</button></center>


0

简单易读的方法,但仅限于3个字符

const stringPermutation = (str) => {
  let permutations = [];
  for (let i in str) {
    for (let j in str) {
      for (let k in str) {
        if (str[i] !== str[j] && str[j] !== str[k] && str[i] !== str[k]) {
          permutations.push(str[i] + str[j] + str[k]);
        }
      }
    }
  }
  return permutations;
};
console.log(stringPermutation("abc"));


0
const permut = (str) => {
  if (str.length <= 2) return str.length === 2 ? [str, str[1] + str[0]] : [str];
  return str
    .split("")
    .reduce(
      (acc, letter, i) =>
        acc.concat(
          permut(str.slice(0, i) + str.slice(i + 1)).map((val) => letter + val)
        ),
      []
    );
};

在这里找到这里


请注意,您的 reduce - concat 代码段可以使用 flatMap 更好地表达。参考链接 - Scott Sauyet

0

这个递归地完成了任务。

function printPermutations(str, res='') {
    if (!str.length){
        console.log(res);
    }
    
    for (let i = 0; i < str.length; i++) {
         let remStr = str.substr(0, i) + str.substr(i + 1);
         printPermutations(remStr, res + str.substr(i, 1));
    }
}

printPermutations("abc")

// result 
//  abc, acb, bac, bca, cab, cba

0

昨天我的面试官问了我同样的问题,但我没有得到正确的逻辑,然后我来到了stackoverflow,在这里找到了答案,现在我有了解决方案,想要与大家分享。

const str_Permutations = (str,ar = []) => {
  str = `${str}`; // ensure type **String**
  if(ar.indexOf(str)>-1 || str.length !== (ar.strlen || str.length)) return false; // Checking if value is alreay there or(||) on recursive call string length should not be provided string 
  ar.strlen = ar.strlen || str.length; // Setting str length of provided value(string)
  ar.push(str);    // Pushing to array
  for(let i = 0; i<str.length;i++){
    str_Permutations(str[i] + str.split('').filter(v=>v!==str[i]).join(''),ar);
  }
  return Array.from(ar); // Removing *strlen* from main result and return **Result** as array
}
str_Permutations("ABC")

//Result: (6) ["ABC", "BAC", "CBA", "BCA", "ACB", "CAB"]

使用Array的引用特性来通过传递在同一个Array中保存值。我希望你明白我的意思!!!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接