[算法]字符全排列

260 阅读3分钟

[算法]字符全排列

今天介绍一下字符全排列的算法。

问题

给出(没有重复的)字符数组,求出所有由这些字符组合的排列,每个字符使用且仅使用一次。例如:

getAll(['a', 'b', 'c']);
// ['abc', 'acb', 'bac', 'bca', 'cab', 'cba']

递归

首先讲一下递归的想法:

  • 当字符数组长度为1时,输出该字符
  • 当字符数组长度大于1时,取其首字母,求出长度-1的串的全排列,将首字母插入每一个排列的任意位置

因为今天要讲的不是这样的做法,具体代码就不贴了。

我们不难发现这里解空间的规模是n!。以上算法会需要多个((n-1)!)解同时存在于内存中,因此空间复杂度非常高。

下一个最小的数

因此我们需要一个空间复杂度较低的算法,即我们需要可以『逐一』求出所有解的方法。

让我们将字母替换为数字来考虑这个问题:

getAll([1, 2, 3]);
// [123, 132, 213, 231, 312, 321]

在所有结果中,最小的值为123,即所有数字顺序排列,最大的值为321,即所有数字逆序排列(如果输入个数大于10,我们只要用n进制来想象问题就可以了)。

那接下来我们考虑这样一个问题,给出解中的任意一个,怎么求出恰好比它大的一个解?

我们想象数字4 3 7 5 1,比它恰好大的数字数4 5 1 3 7。我们从右开始寻找第一组相邻的正序的数字,即3 7,然后把3替换为其右侧仅比它大的数字5。然后把剩余的数字1 7以及3一起正序排序(即1 3 7数字的最小组合),之后连在一起,4 + 5 + 1 3 7

于是我们可以从123开始,逐一求下一个最小的数,直至达到最大数321

  • 为什么要从右向左开始搜索?因为我们要求的是最接近的一个解,因此应当尽量保持高位的数字不变。
  • 为什么要寻找正序的数字对?因为连续逆序的数字必然是最大的,例如43751中的后三位,751,是1、5、7这三个数字所能组成的最大值。

然后我们就可以实现这个算法,实现逐一求解,从而避免内存的问题:

const factorial = function factorial (num) {
  let rtn = 1;
  for (let i = 2; i <= num; i++) {
      rtn *= i;
  }
  return rtn;
};

const getAll = function getAll(list) {
  let count = 0;
  let next = list.slice();
  while (next) {
    count++;
    console.log(next.join(''));
    next = getNext(next);
  }
  console.log(count, factorial(list.length));
};

const getNext = function getNext(list) {
  const rest = [];
  for (let i = list.length - 1; i > 0; i--) {
    const pre = list[i - 1];
    const next = list[i];
    rest.push(next);
    if (pre < next) {
      const head = list.slice(0, i - 1);
      const max = list.length + 1;
      for (let j = pre; j < max; j++) {
        let index = rest.indexOf(j);
        if (index !== -1) {
          rest.splice(index, 1);
          rest.push(pre);
          head.push(j);
          return head.concat(rest.sort((a, b) => a - b))
        }
      }
    }
  }
  return null;
};