问题分类:您可以将此问题视为一个探索问题,即,给定一组输入字符,探索您可以如何安排它们的不同方式。
解决方案: 回溯算法擅长解决探索性问题,尽管它的时间复杂度很高。为了演示一个解决方案,想象一下您将如何针对一小组输入字符手动解决这个问题:[a, b, c]。
以下是步骤:
- 取最左边的字符。这是索引 0 处的字符并将其与索引 0 处的目标右侧字符交换,即与自身交换。这是因为[a, b, c]本身就是一个有效的排列,因此我们想保留它。交换字符通常需要两个指向每个字符的指针。所以我们可以说,我们将有一个左和右指针。
 
- 使用相同的最左边的字符(在索引 0 处)与在索引 0 + 1 = 1 处的目标右边字符进行交换,即,将目标右边的指针进一步移动 1 步。这将为您提供输出:[b, a, c]
 
- 使用同一个最左边的字符(在索引 0 处)与下一个目标右边字符(即索引 0 + 1 + 1 = 2)进行交换。这将为您提供输出:[c, b, a]
 
好的,现在我们需要停止,因为没有更多的目标右侧字符要与最左侧的字符交换。所以我们的右指针需要保持小于输入中的最大索引。一次一步移动右指针,我们可以使用从左索引开始并以输入长度 - 1 结束的for循环。
 
现在您需要从上面执行完全相同的步骤,但移动左指针,使其指向最左边的下一个字符。但是,保留第 2 步和第 3 步的输入。想象这种情况的另一种方法是说:'嘿,我已经完成了最左边的字符。现在我不想再使用它了,但我很想继续使用到目前为止我所拥有的结果中的第二个。
 
我们什么时候停止?当左指针已达到输入字符串的长度 - 1 时,' 因为此索引之后没有更多字符。在递归算法(例如回溯)中,需要停止的情况称为base case。在我们的示例中,基本情况是: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!)
 
- 我们通过在修改后的输入字符串中交换字符来恢复我们的选择